diff --git a/.github/workflows/kms-script-tests.yml b/.github/workflows/kms-script-tests.yml
index e53a32ea..60b58201 100644
--- a/.github/workflows/kms-script-tests.yml
+++ b/.github/workflows/kms-script-tests.yml
@@ -5,12 +5,14 @@ on:
paths:
- rustfs-kms-test.sh
- test_rustfs_kms_presign.py
+ - test_validation_kms.sh
- .github/workflows/kms-script-tests.yml
push:
branches: [main]
paths:
- rustfs-kms-test.sh
- test_rustfs_kms_presign.py
+ - test_validation_kms.sh
- .github/workflows/kms-script-tests.yml
permissions:
@@ -28,3 +30,5 @@ jobs:
run: bash -n rustfs-kms-test.sh
- name: Test presigned KMS case without a server
run: python3 -m unittest test_rustfs_kms_presign.py
+ - name: Test canonical KMS round trip without a server
+ run: bash test_validation_kms.sh
diff --git a/.github/workflows/s3-script-tests.yml b/.github/workflows/s3-script-tests.yml
index 64f74358..45aeec1e 100644
--- a/.github/workflows/s3-script-tests.yml
+++ b/.github/workflows/s3-script-tests.yml
@@ -6,6 +6,7 @@ on:
- rustfs-s3-compat-test.sh
- test_rustfs_s3_usage.py
- test_rustfs_s3_signing.py
+ - test_validation_s3_basic.sh
- .github/workflows/s3-script-tests.yml
push:
branches: [main]
@@ -13,6 +14,7 @@ on:
- rustfs-s3-compat-test.sh
- test_rustfs_s3_usage.py
- test_rustfs_s3_signing.py
+ - test_validation_s3_basic.sh
- .github/workflows/s3-script-tests.yml
permissions:
@@ -32,3 +34,5 @@ jobs:
run: python3 -m pip install botocore==1.43.19
- name: Test S3 cases and signatures without a cluster
run: python3 -m unittest test_rustfs_s3_usage.py test_rustfs_s3_signing.py
+ - name: Test validation S3 round trip without a cluster
+ run: bash test_validation_s3_basic.sh
diff --git a/.github/workflows/validation-catalog-tests.yml b/.github/workflows/validation-catalog-tests.yml
new file mode 100644
index 00000000..89f2ed9d
--- /dev/null
+++ b/.github/workflows/validation-catalog-tests.yml
@@ -0,0 +1,185 @@
+name: Validation Catalog Tests
+
+on:
+ pull_request:
+ paths:
+ - validation-cases.json
+ - validation-migration-review.json
+ - scripts/validation_migration_review.py
+ - scripts/validation_catalog.py
+ - scripts/validation_report.py
+ - scripts/validation_result.py
+ - test_validation_migration.py
+ - test_validation_ilm.py
+ - scripts/validation_ilm/**
+ - test_validation_pool.py
+ - scripts/validation_pool/**
+ - rustfs_pool_expand.sh
+ - test_validation_replication.py
+ - scripts/validation_replication/**
+ - test_validation_heal.py
+ - test_validation_heal*.py
+ - test_validation_heal_recovery.py
+ - test_validation_heal_authorization.py
+ - test_validation_heal_full_restart.py
+ - test_validation_heal_mrf.py
+ - test_validation_heal_unformatted.py
+ - test_validation_heal_performance.py
+ - test_validation_heal_multi_pool.py
+ - test_validation_heal_scopes.py
+ - test_validation_heal_bucket_metadata.py
+ - test_validation_heal_write_quorum.py
+ - test_validation_heal_coordinator_crash.py
+ - test_validation_heal_flapping.py
+ - test_validation_heal_capacity.py
+ - test_validation_heal_network.py
+ - test_validation_heal_encrypted.py
+ - test_validation_heal_schema.py
+ - test_validation_heal_native.py
+ - test_validation_heal_lock_successor.py
+ - test_validation_heal_race_native.py
+ - test_validation_heal_incarnation_native.py
+ - test_validation_heal_manager_native.py
+ - test_validation_heal_manager_fixture.py
+ - test_validation_heal_journal_fault.py
+ - test_validation_heal_checkpoint.py
+ - test_validation_heal_authority.py
+ - test_validation_heal_orphan.py
+ - test_validation_heal_options.py
+ - test_validation_heal_environment.py
+ - test_validation_heal_queue_pressure.py
+ - test_validation_heal_nolock.py
+ - test_validation_heal_race.py
+ - test_validation_heal_g06_source.py
+ - test_validation_heal_environment_gate.py
+ - test_validation_heal_outcome.py
+ - test_validation_heal_outcome_grace.py
+ - test_validation_heal_persistence.py
+ - test_validation_heal_persistence_mrf.py
+ - test_validation_heal_incarnation.py
+ - test_validation_heal_incarnation_replay.py
+ - scripts/validation_heal/**
+ - testdata/i05-*.ndjson
+ - testdata/validation/i05-*.zip
+ - docs/validation-heal-*.md
+ - rustfs_heal_test.sh
+ - test_validation_oidc.py
+ - scripts/validation_oidc.py
+ - scripts/validation_oidc/**
+ - test_validation_webhooks.py
+ - scripts/validation_webhooks.py
+ - scripts/run_validation_case.sh
+ - rustfs-replication-test.sh
+ - rustfs-storage-test.sh
+ - test_validation_scanner_usage.sh
+ - .github/workflows/validation-catalog-tests.yml
+ push:
+ branches: [main]
+ paths:
+ - validation-cases.json
+ - validation-migration-review.json
+ - scripts/validation_migration_review.py
+ - scripts/validation_catalog.py
+ - scripts/validation_report.py
+ - scripts/validation_result.py
+ - test_validation_migration.py
+ - test_validation_ilm.py
+ - scripts/validation_ilm/**
+ - test_validation_pool.py
+ - scripts/validation_pool/**
+ - rustfs_pool_expand.sh
+ - test_validation_replication.py
+ - scripts/validation_replication/**
+ - test_validation_heal.py
+ - test_validation_heal*.py
+ - test_validation_heal_recovery.py
+ - test_validation_heal_authorization.py
+ - test_validation_heal_full_restart.py
+ - test_validation_heal_mrf.py
+ - test_validation_heal_unformatted.py
+ - test_validation_heal_performance.py
+ - test_validation_heal_multi_pool.py
+ - test_validation_heal_scopes.py
+ - test_validation_heal_bucket_metadata.py
+ - test_validation_heal_write_quorum.py
+ - test_validation_heal_coordinator_crash.py
+ - test_validation_heal_flapping.py
+ - test_validation_heal_capacity.py
+ - test_validation_heal_network.py
+ - test_validation_heal_encrypted.py
+ - test_validation_heal_schema.py
+ - test_validation_heal_native.py
+ - test_validation_heal_lock_successor.py
+ - test_validation_heal_race_native.py
+ - test_validation_heal_incarnation_native.py
+ - test_validation_heal_manager_native.py
+ - test_validation_heal_manager_fixture.py
+ - test_validation_heal_journal_fault.py
+ - test_validation_heal_checkpoint.py
+ - test_validation_heal_authority.py
+ - test_validation_heal_orphan.py
+ - test_validation_heal_options.py
+ - test_validation_heal_environment.py
+ - test_validation_heal_queue_pressure.py
+ - test_validation_heal_nolock.py
+ - test_validation_heal_race.py
+ - test_validation_heal_g06_source.py
+ - test_validation_heal_environment_gate.py
+ - test_validation_heal_outcome.py
+ - test_validation_heal_outcome_grace.py
+ - test_validation_heal_persistence.py
+ - test_validation_heal_persistence_mrf.py
+ - test_validation_heal_incarnation.py
+ - test_validation_heal_incarnation_replay.py
+ - scripts/validation_heal/**
+ - testdata/i05-*.ndjson
+ - testdata/validation/i05-*.zip
+ - docs/validation-heal-*.md
+ - rustfs_heal_test.sh
+ - test_validation_oidc.py
+ - scripts/validation_oidc.py
+ - scripts/validation_oidc/**
+ - test_validation_webhooks.py
+ - scripts/validation_webhooks.py
+ - scripts/run_validation_case.sh
+ - rustfs-replication-test.sh
+ - rustfs-storage-test.sh
+ - test_validation_scanner_usage.sh
+ - .github/workflows/validation-catalog-tests.yml
+
+permissions:
+ contents: read
+
+jobs:
+ contract-tests:
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ steps:
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
+ with:
+ persist-credentials: false
+ - run: python3 scripts/validation_catalog.py check
+ - run: python3 -B scripts/validation_migration_review.py --require-complete
+ - run: python3 -B -m unittest -v test_validation_migration.py
+ - run: python3 -B -m unittest -v test_validation_ilm.py
+ - run: python3 -B -m unittest -v test_validation_pool.py
+ - run: python3 -B -m unittest -v test_validation_replication.py
+ - run: python3 -m pip install -r scripts/validation_ilm/requirements.txt
+ - name: Install network namespace test dependency
+ run: |
+ if ! command -v nft >/dev/null; then
+ sudo apt-get update
+ sudo apt-get install --no-install-recommends -y nftables
+ fi
+ - run: python3 -B -m unittest -v test_validation_heal.py test_validation_heal_recovery.py test_validation_heal_authorization.py test_validation_heal_full_restart.py test_validation_heal_mrf.py test_validation_heal_unformatted.py test_validation_heal_performance.py test_validation_heal_scopes.py test_validation_heal_multi_pool.py test_validation_heal_bucket_metadata.py test_validation_heal_native.py test_validation_heal_manager_native.py test_validation_heal_manager_fixture.py test_validation_heal_journal_fault.py test_validation_heal_checkpoint.py test_validation_heal_authority.py test_validation_heal_orphan.py test_validation_heal_options.py test_validation_heal_environment.py test_validation_heal_environment_gate.py test_validation_heal_queue_pressure.py test_validation_heal_nolock.py test_validation_heal_race.py test_validation_heal_write_quorum.py test_validation_heal_schema.py test_validation_heal_encrypted.py test_validation_heal_coordinator_crash.py test_validation_heal_flapping.py test_validation_heal_capacity.py test_validation_heal_network.py test_validation_oidc.py test_validation_webhooks.py
+ - run: python3 -B -m unittest -v test_validation_heal_timeout_retention.py test_validation_heal_race_retention.py test_validation_heal_retained_scene.py test_validation_heal_lock_timeout_runner.py test_validation_heal_lock_source.py test_validation_heal_lock_timeout.py test_validation_heal_lock_observer.py test_validation_heal_lock_successor.py test_validation_heal_caller_abort.py test_validation_heal_status_keepalive.py test_validation_heal_mpu_race_runner.py test_validation_heal_race_io.py test_validation_heal_race_physical.py test_validation_heal_g03_source.py test_validation_heal_version_race_fixture.py test_validation_heal_version_race_physical.py test_validation_heal_version_race_adapter.py test_validation_heal_version_race_runner.py
+ - run: python3 -B scripts/validation_heal/matrix.py check
+ - run: python3 -B -m unittest -v test_validation_heal_outcome.py test_validation_heal_outcome_grace.py
+ - run: python3 -B -m unittest -v test_validation_heal_persistence.py test_validation_heal_persistence_mrf.py
+ - run: python3 -B -m unittest -v test_validation_heal_incarnation.py test_validation_heal_incarnation_replay.py
+ - run: python3 -B -m unittest -v test_validation_heal_incarnation_native.py
+ - run: python3 -B -m unittest -v test_validation_heal_race_native.py test_validation_heal_g06_source.py
+ - run: python3 -B -m unittest -v test_validation_heal_g02_source.py test_validation_heal_endpoint_resolution.py
+ - run: bash -n scripts/run_validation_case.sh scripts/validation_heal/run_case.sh scripts/validation_oidc/run_case.sh
+ - run: bash -n rustfs-storage-test.sh rustfs_pool_expand.sh rustfs-replication-test.sh scripts/validation_pool/run_case.sh scripts/validation_replication/run_case.sh test_validation_scanner_usage.sh
+ - run: bash test_validation_scanner_usage.sh
diff --git a/README.md b/README.md
index 006a8da2..b7c0171a 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,84 @@
# auto-testing
Auto testing scripts
+## Release validation migration
+
+`validation-cases.json` snapshots all 158 canonical cases from
+`rustfs-release-validation/scenarios/catalog.yaml` (66 implemented, 84
+procedure-ready, 8 planned). The source status is historical context. A run
+result carries `migration_status: verified` only after its assertions, topology,
+and live evidence are checked in this repository. `reuse_candidates` names
+existing suites to inspect and never grants coverage by itself.
+
+```bash
+python3 scripts/validation_catalog.py check
+python3 scripts/validation_catalog.py summary
+python3 -B -m unittest -v test_validation_migration.py
+```
+
+To refresh after an intentional source catalog change:
+
+```bash
+python3 scripts/validation_catalog.py sync \
+ --source /absolute/path/to/rustfs-release-validation/scenarios/catalog.yaml
+```
+
+`scripts/validation_report.py` accepts one JSON result per canonical case ID.
+Results must separately report source status, migration status, verdict,
+cleanup, candidate/rc provenance, and evidence for each source assertion.
+Pass `--gate-status implemented` only after the 66 migrated executors are
+ready; missing or unverified cases fail that gate. Without a gate status, the
+report shows migration progress and still rejects malformed or false PASS
+results.
+
+The executable canonical cases currently include `s3.basic-object-round-trip`,
+`scanner.usage-convergence`, `kms.sse-kms-round-trip`,
+`tiering.transition-and-restore`, `events.delivery`,
+`audit.request-correlation`, `iam.policy-enforcement`, and
+`scanner.status-readiness`. The S3, KMS, and IAM
+cases use single-disk
+deployment. The scanner case uses the storage suite's single-node four-disk
+deployment, waits for a post-upload successful scanner cycle, and requires a
+complete usage snapshot with exactly 20 objects and 20,690 logical bytes.
+Each requires a candidate manifest with `source_sha`, `package_url`, and
+`package_sha256`. The installed binary
+SHA-256 is measured on the test node and included in result provenance:
+
+```bash
+./rustfs-s3-compat-test.sh \
+ --validation-case s3.basic-object-round-trip \
+ --candidate-manifest /absolute/path/candidate.json \
+ --testing-commit "$(git rev-parse HEAD)" \
+ --validation-artifacts-dir /absolute/path/new-artifacts-dir \
+ --yes
+
+./rustfs-storage-test.sh \
+ --validation-case scanner.usage-convergence \
+ --candidate-manifest /absolute/path/candidate.json \
+ --testing-commit "$(git rev-parse HEAD)" \
+ --validation-artifacts-dir /absolute/path/another-new-artifacts-dir \
+ --yes
+
+./rustfs-kms-test.sh \
+ --validation-case kms.sse-kms-round-trip \
+ --candidate-manifest /absolute/path/candidate.json \
+ --testing-commit "$(git rev-parse HEAD)" \
+ --validation-artifacts-dir /absolute/path/kms-new-artifacts-dir \
+ --yes
+
+python3 scripts/validation_report.py \
+ --results-dir /absolute/path/new-artifacts-dir/cases \
+ --output /absolute/path/new-artifacts-dir/report.md
+```
+
+The report's full implemented-case gate is intentionally not enabled by these
+runs. Run it with `--gate-status implemented` only after all 66
+implemented source cases have verified executors and live results.
+
+S3, scanner, KMS, tiering, and IAM have live PASS evidence against nightly
+build `37958901959` attempt `1` on the current auto-testing fleet. Events and
+audit have local executor tests but still need live verification.
+
## Fault-tolerance suite
`rustfs-fault-tolerance-test.sh` writes a machine-readable completion result
diff --git a/docs/validation-heal-cases.md b/docs/validation-heal-cases.md
new file mode 100644
index 00000000..72b29d74
--- /dev/null
+++ b/docs/validation-heal-cases.md
@@ -0,0 +1,1977 @@
+# Heal validation procedures
+
+These source procedures require live execution and branch evidence on the current auto-testing Heal fleet.
+Use [the operations reference](validation-heal-operations.md) before injecting a fault. A procedure card or complete archive is not a PASS.
+
+
+
+## A01: heal.admin-healthy-noop
+
+Topologies: H1,H2. Branches: normal, deep.
+
+1. 用 DB 夹具生成完整 manifest;先对每个对象保存 S3 基线和四盘分片摘要,关闭 scanner/MRF/auto-heal。
+2. 依次执行 START object(scanMode=1、2),WAIT 原 token 直至终态;再 STATUS 同 token 两次。
+3. 对每个对象的 outcome 区分 Normal 的 metadata_healthy 与 Deep 的 verified_healthy;两者均要求 healed=0、unchanged>0,Normal 不得冒充 payload 完整性证明。比较目标/控制对象物理摘要和 S3 清单,重复请求不得新建版本。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [BASELINE](validation-heal-operations.md#BASELINE) → [START](validation-heal-operations.md#START) → [WAIT](validation-heal-operations.md#WAIT) → [STATUS](validation-heal-operations.md#STATUS) → [VERIFY](validation-heal-operations.md#VERIFY) → [PHYSICAL](validation-heal-operations.md#PHYSICAL)
+
+Source assertions:
+
+- 对健康对象分别执行 normal/deep,再重复查询
+- 不修改用户数据或版本;Normal 只接受 `metadata_healthy`,Deep 才要求 `verified_healthy`,healthy 与 repaired 分开计数,不能把检查盘数当修复盘数。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## A02: heal.admin-scope-isolation
+
+Topologies: H1,H2. Branches: root, bucket, prefix, object, pool-set.
+
+1. 用 DS 在两个桶的 a/、ab/、b/ 写入不同内容;另在未选 set 放控制对象;逐对象预先记录实际 pool/set。
+2. 分别隔离选中对象、相邻前缀及另一桶/未选 set 的一个 part。每个 selector 使用新夹具和故障 ID。
+3. 对象请求路径到完整 key;prefix 路径到 a/ 且 recursive=true;bucket 路径只含桶;root 为 /;pool/set 写入 body。START 后 WAIT。
+4. 先 PHYSICAL:选择范围内全部重建、范围外控制故障仍缺失;然后 GET 验证,避免验证请求提前修好控制故障。root 必须包含整个专用服务清单。
+
+Operations: [SEED_DS](validation-heal-operations.md#SEED_DS) → [BASELINE](validation-heal-operations.md#BASELINE) → [FAULT](validation-heal-operations.md#FAULT) → [START](validation-heal-operations.md#START) → [WAIT](validation-heal-operations.md#WAIT) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 分别以 root、bucket、prefix、object、pool/set 选择器启动
+- 目标范围实际遍历;相邻前缀、其他桶及未选 set 的故障保持原状;按对象清单而非目录数量验收。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## A03: heal.admin-token-cross-node-status
+
+Topologies: H2. Branches: coordinator-to-peer, coordinator-to-stable-endpoint.
+
+1. 在 H2 同一跨节点 set 写入 DB;保存协调者、目标、稳定入口分别对应的端点,不把同一节点的两个别名当跨节点。
+2. 隔离目标节点 part;从 n0 START,记录原始 clientToken;通过 n1 和 stable 对同一路径、同 token 执行 STATUS。
+3. 按同一任务身份对齐三端状态,最多等候冻结的状态收敛预算;终态及核心计数必须一致,404/unknown 不能视为结束。
+4. WAIT 后先物理证明目标重建,再从所有 S3 入口 VERIFY;保存每个请求实际连接的节点日志。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [FAULT](validation-heal-operations.md#FAULT) → [START](validation-heal-operations.md#START) → [STATUS](validation-heal-operations.md#STATUS) → [WAIT](validation-heal-operations.md#WAIT) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 从节点甲启动,经乙及稳定入口按原 token 查询
+- 任务身份、scope、终态及核心计数有界收敛;状态缺失/未知不能解释为完成。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## A04: heal.admin-dry-run
+
+Topologies: H1,H2. Branches: missing-remove0-recreate0, missing-remove1-recreate0, bitflip-remove0-recreate1, bitflip-remove1-recreate1, versioned-missing, versioned-bitflip.
+
+1. 分别用 DB 与 DV 建夹具,冻结全部 versionId/marker;关闭后台来源,不在注入后 GET。
+2. 在原始 part 上做 missing/bitflip,每组独立新目录;保存故障后的 PHYSICAL 快照作为 dry-run 前基线。
+3. START body 同时传 dryRun=true、scanMode=2 和该实例的 remove/recreate;WAIT 仅接受 dry_run_observed,不能要求 dry-run 持有 incarnation。
+4. 前后所有分片/元数据/marker 字节与版本清单相同;没有 repaired ACK。完成非变更证明后恢复故障,再 VERIFY。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [SEED_DV](validation-heal-operations.md#SEED_DV) → [BASELINE](validation-heal-operations.md#BASELINE) → [FAULT](validation-heal-operations.md#FAULT) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [START](validation-heal-operations.md#START) → [WAIT](validation-heal-operations.md#WAIT) → [RECOVER](validation-heal-operations.md#RECOVER) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 对缺片与损坏对象执行 dryRun,参数化 remove/recreate
+- 有明确 dry_run_observed 诊断;用户对象分片、xl.meta、版本与删除标记不变;后台修复干扰须排除。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## A05: heal.admin-overlap-and-force-start
+
+Topologies: H1,H2. Branches: same-scope, parent-child, disjoint, force-start.
+
+1. 用 DX 超过 128 项的外部分片夹具注入多对象缺片;预先从固定候选记录 overlap policy(复用 token 或冲突错误),禁止接受任意结果。
+2. START 递归范围,WINDOW 证明 0
+
+## A06: heal.admin-stop-and-stale-token
+
+Topologies: H1,H2. Branches: stop-active, repeat-stop, old-status, old-stop-new-task.
+
+1. DX 批量缺片后 START;WINDOW 观察非零且未完成进度,再 STOP 原 token。
+2. 状态显示 aborted/cancelled 后仍观察物理写入静默窗口;停止受理成功不能替代在途退出。
+3. 新 START 得到不同 token,再对旧 token STATUS/STOP;新任务不应被旧请求终止,重复 stop 按冻结的 HTTP/错误码验收。
+4. 新任务完成后完整 VERIFY;记录取消项不能算 repaired,所有暂态错误保留原始 body。
+
+Operations: [SEED_DX](validation-heal-operations.md#SEED_DX) → [FAULT](validation-heal-operations.md#FAULT) → [START](validation-heal-operations.md#START) → [WINDOW](validation-heal-operations.md#WINDOW) → [STOP](validation-heal-operations.md#STOP) → [STATUS](validation-heal-operations.md#STATUS) → [WAIT](validation-heal-operations.md#WAIT) → [VERIFY](validation-heal-operations.md#VERIFY) → [PHYSICAL](validation-heal-operations.md#PHYSICAL)
+
+Source assertions:
+
+- 非零修复进度时 stop,稳定后新建任务,再发旧 token 的查询/停止
+- 停止需等在途写结束并稳定;新任务身份独立,旧请求不得终止新任务;重复 stop 按公开契约处理。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## A07: heal.admin-invalid-request
+
+Topologies: H1,H2. Branches: duplicate-query, invalid-bool, invalid-scan, conflicting-flags, invalid-selector, oversize-body.
+
+1. 建立健康 DB 和 PHYSICAL 基线;保存后台/任务状态及对象版本集合,用于检查负请求未意外启动任务。
+2. 逐个发送服务端负例:重复 clientToken;forceStart=wat;scanMode=99;forceStart=true&forceStop=true;pool=-1/set 越界;超过候选限制的 body。每个例固定预期 HTTP 及结构化错误码。
+3. 使用 ADMIN_RAW 的重复 --query 和原始 --body-file,不能只调用 rc 的参数校验;不使用超出专用 Heal 路由的请求。
+4. 全部响应无有效新任务受理,服务仍可用;PHYSICAL、版本、后台责任无改变;若返回 2xx 或错误无明确原因保留 FAIL。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [BASELINE](validation-heal-operations.md#BASELINE) → [ADMIN_RAW](validation-heal-operations.md#ADMIN_RAW) → [STATUS](validation-heal-operations.md#STATUS) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 发送非法 selector、重复 query、非法布尔/scanMode、冲突 flags、超长 body
+- 服务端返回已核实的结构化错误;无误启动、范围扩大或持久化副作用;CLI 参数错误不能替代服务端负例。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## A08: heal.admin-authorization
+
+Topologies: H1,H2. Branches: anonymous, bad-signature, expired-signature, denied-user, least-privilege-user.
+
+1. 通过 IAM 手册在专用服务创建两个临时用户,分别没有和仅有 HealAdminAction;用受控凭据环境运行,先证明限制用户不能读未授权对象。
+2. 管理员创建待查询/停止任务;分别对 start/status/stop 发匿名、坏签名、有效签名但时间超期、无授权用户请求;过期请求由隔离客户端签名时间设置产生,不改服务器时间。
+3. 检查冻结的 AccessDenied/签名/时间错误;响应不暴露对象结果。最小授权用户完成受理/查询/停止,证明接口本身可工作。
+4. 移除临时用户和策略并验证凭据失效;所有原始证据先脱敏再归档,不把管理员 token 暴露给无关主体。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [IAM](validation-heal-operations.md#IAM) → [START](validation-heal-operations.md#START) → [ADMIN_RAW](validation-heal-operations.md#ADMIN_RAW) → [STATUS](validation-heal-operations.md#STATUS) → [STOP](validation-heal-operations.md#STOP) → [VERIFY](validation-heal-operations.md#VERIFY) → [RECOVER](validation-heal-operations.md#RECOVER)
+
+Source assertions:
+
+- 匿名、坏签名、过期签名、无 HealAdminAction 用户与最小授权用户分别请求
+- 未授权 start/status/stop 均不能越权或泄漏对象信息;授权用户可完成规定操作,凭据不得写入证据。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## A09: heal.admin-nolock-safety
+
+Topologies: H1,H2. Branches: overwrite-nolock, delete-nolock, native-lock-fence.
+
+1. DB/DV 准备同一目标并缺片;两客户端使用同 key,冻结每个 PUT/DELETE 的唯一 nonce 和 ACK ledger。
+2. 一端 START body nolock=true,另一端按 WORKLOAD 提交覆盖或精确 versionId 删除;记录确实交叠的运行区间。
+3. 按 ACK 因果顺序重建期望清单并 VERIFY;最后成功覆盖不回退、已删除版本不复活。
+4. 另外运行 NATIVE 的锁/提交屏障回归并保存精确测试名、源码 SHA、命中日志;黑盒竞态与 native-lock-fence 单独结果,不以偶然重叠证明强制锁时点。
+
+Operations: [SEED_DV](validation-heal-operations.md#SEED_DV) → [FAULT](validation-heal-operations.md#FAULT) → [WORKLOAD](validation-heal-operations.md#WORKLOAD) → [START](validation-heal-operations.md#START) → [WINDOW](validation-heal-operations.md#WINDOW) → [VERIFY](validation-heal-operations.md#VERIFY) → [NATIVE](validation-heal-operations.md#NATIVE)
+
+Source assertions:
+
+- 通过签名 API 传 nolock=true,并与同对象提交制造确定性争用
+- 兼容字段不得跳过存储层对象锁;最后成功写入和已确认删除保持正确,原生屏障与真机竞争证据分开。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## A10: heal.admin-option-contract
+
+Topologies: H1,H2. Branches: recursive-false, recursive-true, recreate, remove-false, remove-true, update-parity, read-repair, selector-valid, selector-invalid.
+
+1. 将候选 HealOpts 支持表和危险参数副作用写入本批 contract.json;不根据 CLI help 推导 API 未暴露字段。
+2. 为每个 variant 使用新 DB/DV:固定范围、足够权威和控制对象;remove=true 只对经 C10 证明无活引用的本次孤儿执行。
+3. 逐一 START 并对比只改变一个字段的基线;recursive 检查子前缀,pool/set 检查实际目标身份,非法组合要求结构化拒绝且无副作用。
+4. WAIT、PHYSICAL、VERIFY 三者验收;updateParity/readRepair 若候选不支持必须显示明确拒绝,不可悄悄按默认修复算覆盖。
+
+Source implementation detail: frozen `heal_h1_options_batch.py` and
+`heal_h1_options_node.py` at `524eb5b32c3236497fdd8a9271df0494fa3d9974`
+run the remove=true orphan with a recursive bucket sweep after exact-version
+DELETE and one-member full-object replay (including metadata). Its false
+baseline is an option reference; remove=false is a separate repairable-object
+branch. The adapter keeps every original H1/H2 DB/DV branch, with 68 registered
+execution instances, and rejects the four formerly invented orphan baseline
+instances. Original failures remain archived. Physical closure requires the
+whole orphan to disappear, every post-DELETE control/format to remain exact,
+and complete version-addressed task receipts/counters before reads or rescue.
+
+Complete live acceptance: [run 39197088183/1](https://github.com/rustfs/rustfs/actions/runs/39197088183)
+on helper `b16e6072679d6a60a044b9d4520b30b4a79b5557` passed all 68 instances
+and independent ACK/task/physical/public/cleanup audit, including 170 node
+finalizations. Candidate `072b57b37559fbe626aeda8abf61617fdcbdd926`, package
+`38043283428/1` and inspector `38715612825/1` remain immutable. Earlier failures
+are retained; control rescue is not counted as product repair.
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [SEED_DV](validation-heal-operations.md#SEED_DV) → [FAULT](validation-heal-operations.md#FAULT) → [START](validation-heal-operations.md#START) → [WAIT](validation-heal-operations.md#WAIT) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 参数化 recursive、recreate、remove、updateParity、readRepair 与合法/非法 pool/set 组合
+- 仅测试已核实的候选语义;normal/deep 与危险 remove 的副作用明确;API 独有选项不得伪称 rc 已覆盖。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## B01: heal.disk-loss-and-repair
+
+Topologies: H1,H2. Branches: original-drive-return, offline-ack-repair.
+
+1. 由 layout.json 选一成员盘,记录同版本 N/R/W;使用 DRIVE_GATE 仅让本服务看不到该逻辑盘,不卸载共享 XFS。
+2. 故障窗口经测试进程 mount namespace 的读失败与 Admin offline 状态双重见证;在 quorum 允许范围执行 GET 和唯一 key PUT,逐次记 ACK。
+3. DRIVE_GATE 恢复原盘身份后,等待所选手工/自动来源处理离线期间责任;不得复制备份补盘。
+4. 验证所有故障前对象及成功 ACK 对象、目标盘实际新增分片;节点 ready/GET 成功不是补片证据。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [BASELINE](validation-heal-operations.md#BASELINE) → [DRIVE_GATE](validation-heal-operations.md#DRIVE_GATE) → [WORKLOAD](validation-heal-operations.md#WORKLOAD) → [BACKGROUND](validation-heal-operations.md#BACKGROUND) → [START](validation-heal-operations.md#START) → [WAIT](validation-heal-operations.md#WAIT) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 让一个已确认 set 成员盘对 heal 服务不可用,持续读取,恢复原盘
+- 记录真实故障窗口和源/目标盘身份;在可恢复条件下最终补齐目标分片及完整 manifest,不只验证降级 GET。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## B02: heal.disk-readonly-and-permission
+
+Topologies: H1,H2. Branches: read-only, permission-denied.
+
+1. 在专用测试服务的 mount namespace 设置目标根只读;permission 实例用专用非 root 服务 UID 和独立目录 ACL,不能对 root 仅 chmod 后假称生效。
+2. 在服务相同身份/namespace 执行有界写探针,要求 EROFS/EACCES;先保存并隔离目标 part,再触发 deep heal。
+3. 目标不可写期间必须保留失败/待修责任,不能 reported repaired;PHYSICAL 确認没有写到父目录/系统盘。
+4. 解除只读或恢复原 ACL,重新修复并 VERIFY;保留故障期间错误和恢复后物理补片证据。
+
+permission-denied 分支释放 ACL gate 后,保持同一候选进程运行,并用进程 birth、候选摘要及释放后的物理快照证明状态。释放后、提交新的唯一 Admin token 前,目标分片必须仍保持故障态;若自动恢复先改变分片,该分支记为 `NOT_OBSERVED`,不能将恢复归因给新的手工任务。新的修复 token 终态后再停止服务并验证原始分片摘要。read-only 分支按其 unit mount gate 的生命周期清理 gate,再执行独立修复任务。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [FAULT](validation-heal-operations.md#FAULT) → [DRIVE_GATE](validation-heal-operations.md#DRIVE_GATE) → [START](validation-heal-operations.md#START) → [WAIT](validation-heal-operations.md#WAIT) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [RECOVER](validation-heal-operations.md#RECOVER) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 仅对独占测试卷制造只读/拒绝访问,恢复后重试
+- 不可写目标不得 reported repaired;错误及待修责任可见;恢复后真实写入目标且不落到系统盘或其他目录。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## B03: heal.disk-replacement-formatted
+
+Topologies: H1,H2. Branches: new-disk-data, new-disk-versioned.
+
+1. 完整基线后停止本例服务,将目标逻辑盘 data 整体隔离到本 run 的 quarantine;使用 REPLACE 建空的新 data 根。
+2. 按候选初始化契约启动,读取 format.json 证明新 this UUID 属于原 pool/set/slot;其他盘格式不得变成新集群。
+3. 显式 START root/deep 或本实例指定重建来源,旧盘备份不可读给测试进程;WAIT 全部责任。
+4. 逐对象/版本验证目标盘分片与同角色基线摘要;确认不是从 quarantine 拷贝回去;恢复操作只在取证后做并标为 rescue。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [SEED_DV](validation-heal-operations.md#SEED_DV) → [BASELINE](validation-heal-operations.md#BASELINE) → [REPLACE](validation-heal-operations.md#REPLACE) → [FORMAT](validation-heal-operations.md#FORMAT) → [START](validation-heal-operations.md#START) → [WAIT](validation-heal-operations.md#WAIT) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 换入空的新逻辑盘,确认新 disk UUID/正确格式后触发重建
+- 新盘与旧盘身份可区分且属于正确 pool/set/slot;逐版本重建完成,不能从隔离备份复制回来冒充 heal。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## B04: heal.disk-replacement-unformatted
+
+Topologies: H1,H2. Branches: blank-unformatted, foreign-format.
+
+The Functional Chain runs the shared read-only replacement admission gate
+before package/service changes and the executor repeats it before reservation.
+If exact endpoint mounts or the reviewed mount-switch adapter are unavailable,
+the case is recorded as pre-deployment `BLOCKED` without creating either
+the blank or foreign-format fixture.
+
+1. REPLACE 建没有 format 的空逻辑盘;记录空目录证据,启动并观察候选支持的安全初始化或明确拒绝。
+2. 第二实例使用同 run 的第二个隔离小集群生成外来 format,将其按 FORMAT_BAD 手册装入替换根,保留原始副本和所有 UUID。
+3. 外来身份不能被静默覆盖或挂到错误 set;若拒绝,等待有界 deadline 并确认其他正常盘数据不变。
+4. 停止本服务、撤回替换根、恢复原身份后 VERIFY;空盘正常接纳实例还须逐对象重建证明。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [REPLACE](validation-heal-operations.md#REPLACE) → [FORMAT_BAD](validation-heal-operations.md#FORMAT_BAD) → [BACKGROUND](validation-heal-operations.md#BACKGROUND) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [RECOVER](validation-heal-operations.md#RECOVER) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 引入空未格式化测试盘,并与带异集群格式的盘分别比较
+- 按候选支持的初始化契约安全接纳或明确拒绝;不得覆盖外来身份或将未就绪盘标为可写健康。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## B05: heal.multiple-shard-loss
+
+Topologies: H1,H2. Branches: loss-1-data, loss-1-parity, loss-2-through-m-data-parity.
+
+Before seeding, before each fault and after each faulted restart, the executor
+observes signed read-only Admin info from every H1/H2 endpoint for at most
+90 seconds. All 4/16 slots must match the frozen format UUIDs and report online;
+read readiness alone does not prove this prerequisite. Every observation is
+retained. Incomplete or foreign identities fail immediately; persistent offline
+members expire as `NOT_OBSERVED` without retrying the Heal task. The original
+`38200772548` unknown-version receipts remain a retained failure. This startup
+check alone does not establish its underlying cause or a new live PASS.
+
+1. 从候选/实际元数据确定 M=N-R,枚举 1..M 的合法缺片数及 data/parity 组合,写入本批展开参数表;不得只运行 loss=1。
+2. 每实例用相同版本的新对象,将指定成员上的 part 分别 FAULT missing;多份元数据也要证明指向同 version/data_dir。
+3. 在满足修复提交条件时 START deep/WAIT;先检查全部目标片与角色基线相同,再 VERIFY。
+4. 每次恢复隔离备份后换新对象再做下一组合;低于 R 的组合另由 B07 计负例,不合并成绩。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [SEED_DV](validation-heal-operations.md#SEED_DV) → [FORMAT](validation-heal-operations.md#FORMAT) → [BASELINE](validation-heal-operations.md#BASELINE) → [FAULT](validation-heal-operations.md#FAULT) → [START](validation-heal-operations.md#START) → [WAIT](validation-heal-operations.md#WAIT) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 按实际同版本 EC 布局依次缺失 1..M 个 shard,覆盖 data/parity 角色组合
+- 每个参数实例单独满足读取与写入 quorum;满足修复条件时全部目标重建,不把跨版本或跨 set 的 shard 合并。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## B06: heal.read-quorum-boundary
+
+Topologies: H1,H2. Branches: exact-r-read, exact-r-heal-write-limited, restored-w.
+
+执行器仅接受经逐文件和锁策略复核的 `072b57b37559fbe626aeda8abf61617fdcbdd926` 与 GET quorum 修复候选 `1c7fc818dd858247c45b39542abd8291b1e6e31b`。记录所选候选的精确锁契约;inspector 必须同时匹配候选及当前 helper 源码摘要,未知候选在部署前拒绝。below-R GET 仍必须为候选冻结的 `503/SlowDownRead`,旧 `500/InternalError` 失败记录保留;支持新候选不等于新的 live 验收。
+
+1. 记录读取 R、修复写 W、锁 quorum 的来源;选择同一版本保留恰好 R 个权威片,其他片隔离。
+2. 第一次请求 GET 验证读取边界并记录 read-repair 干扰;用第二个相同形态新对象执行只手工 deep heal。
+3. 如果只有 R 但写/锁 quorum 不足,要求拒绝持久化成功;若拓扑中该组合不可实现,在运行前声明精确原因并选满足条件的拓扑,不伪造离线数。
+4. 恢复到 W 后重新启动修复并保存独立 token,比较精确目标片与全部内容;成功读不能充修复成功。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [FORMAT](validation-heal-operations.md#FORMAT) → [FAULT](validation-heal-operations.md#FAULT) → [GET](validation-heal-operations.md#GET) → [START](validation-heal-operations.md#START) → [WAIT](validation-heal-operations.md#WAIT) → [RECOVER](validation-heal-operations.md#RECOVER) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 保留恰好 R 个同版本权威 shard,测试读取及修复提交,再恢复写 quorum
+- 读取边界和修复写入边界分别判定;不能因还能 GET 就要求在写 quorum 不足时提交成功。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## B07: heal.below-read-quorum
+
+Topologies: H1,H2. Branches: r-minus-1-unversioned, r-minus-1-historical.
+
+1. 选定精确 versionId 保留 R-1 份同版本权威;隔离其余 part,确保没有其他 pool 或旧版本兜底被当作同版本来源。
+2. GET 必须返回冻结的不可恢复错误,不能返回 200 错内容或空对象;deep heal 的 execution/disposition 必须如实失败/未解决。
+3. 在恢复前快照所有好片和 xl.meta,证实负例没有扩大损坏或执行 remove;保留失败任务原 token。
+4. 仅在负例取证完成后 RECOVER 原始片,再显式 heal 和 VERIFY;恢复后的绿不能覆盖故障期间的错误。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [SEED_DV](validation-heal-operations.md#SEED_DV) → [FAULT](validation-heal-operations.md#FAULT) → [GET](validation-heal-operations.md#GET) → [START](validation-heal-operations.md#START) → [WAIT](validation-heal-operations.md#WAIT) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [RECOVER](validation-heal-operations.md#RECOVER) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 使权威来源小于 R,执行 GET 与 deep heal,再恢复被隔离源
+- 故障期间不得返回伪造的成功内容、空对象或误删记录;明确 unresolved/失败;恢复后原对象和版本完整。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## B08: heal.below-write-quorum
+
+Topologies: H1,H2. Branches: read-present-write-absent, lock-quorum-absent.
+
+1. 用 layout.json 分别计算对象写和锁 quorum;DRIVE_GATE/NETWORK 构造仍可读但不能安全提交的测试服务可见性。
+2. 执行唯一 key PUT 与已有缺片对象 heal;保存每个 ACK/HTTP/body,超时写必须查询唯一 key 解析结果,禁止直接重试换 key。
+3. 期间不能 reported repaired 或返回无法兑现的成功写 ACK;从各 peer 收集 quorum 与锁失败日志。
+4. 解除故障后按全部已确认/已解析写入重建 manifest,实际补齐目标并 VERIFY;普通低 quorum 错误不是基础设施错误。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [FORMAT](validation-heal-operations.md#FORMAT) → [DRIVE_GATE](validation-heal-operations.md#DRIVE_GATE) → [NETWORK](validation-heal-operations.md#NETWORK) → [WORKLOAD](validation-heal-operations.md#WORKLOAD) → [START](validation-heal-operations.md#START) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [RECOVER](validation-heal-operations.md#RECOVER) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 构造读 quorum 尚存而写/锁 quorum 不足,执行 heal 与新 PUT
+- 不能虚报持久化修复或成功写入;分别记录对象、元数据、分布式锁 quorum;恢复后核对所有已确认写入。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## B09: heal.node-outage-rejoin
+
+Topologies: H2. Branches: one-node-stop, rejoin-with-new-versions.
+
+1. 确认 H2 目标 set 跨节点,统计停一节点实际损失的盘数,不能使用每台独立 pool 的 H5 替代。
+2. PROCESS stop 精确测试 unit,见证 MainPID 消失及端点不可达;保持其他节点服务。
+3. 按计算允许的 quorum 执行降级 PUT/DELETE/GET,保存版本化 ACK;重入同一节点同一数据根。
+4. 观察 MRF/修复责任直到补齐离线期间成功对象;从所有节点 VERIFY 和目标 PHYSICAL,不能只看进程 ready。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [SEED_DV](validation-heal-operations.md#SEED_DV) → [FORMAT](validation-heal-operations.md#FORMAT) → [PROCESS](validation-heal-operations.md#PROCESS) → [WORKLOAD](validation-heal-operations.md#WORKLOAD) → [BACKGROUND](validation-heal-operations.md#BACKGROUND) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 停止同一跨节点 set 的一个节点,执行允许的降级 I/O,再重入
+- 按实际丢失盘数判定可用性;缺失期间成功提交的对象补齐;节点重新 ready 不等于所有对象已重建。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## B10: heal.disk-and-node-flapping
+
+Topologies: H2. Branches: drive-flap-3, node-flap-3, interrupt-rebuild-again.
+
+1. 预注册三个故障周期、每次最长窗口和总 deadline;准备带 nonce 的 ACK ledger,设置只属于本服务的 drive gate 或 unit 操作。
+2. 每轮离线/在线都先见证实际状态;第三轮在 WINDOW 证明部分重建时再次离线,不以固定 sleep 替代。
+3. 记录每轮责任/队列、任务身份、错误率及 CPU/FD;超过预注册重试/资源预算即失败,不继续循环等绿。
+4. 彻底解除故障后验证全部已确认对象、目标分片和持久队列收敛;活动窗口没命中记 NOT_OBSERVED。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [DRIVE_GATE](validation-heal-operations.md#DRIVE_GATE) → [PROCESS](validation-heal-operations.md#PROCESS) → [WORKLOAD](validation-heal-operations.md#WORKLOAD) → [WINDOW](validation-heal-operations.md#WINDOW) → [BACKGROUND](validation-heal-operations.md#BACKGROUND) → [METRICS](validation-heal-operations.md#METRICS) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 重复短暂离线/在线并在重建途中再次失联
+- 重试与队列有界、身份稳定,无重复任务风暴;最终故障解除后收敛,所有已确认对象保持正确。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## C01: heal.missing-part-file
+
+Topologies: H1,H2. Branches: data-single-part, parity-single-part, multipart-first, multipart-middle, multipart-last.
+
+1. DB/DM 写入后用 FORMAT/PHYSICAL 将每个 version/part 映射到真实盘角色;每个 variant 选不同目标,不按盘序号猜 data/parity。
+2. 停住注入目标写入后 FAULT missing 只移除已冻结摘要的 part,xl.meta 和 format 保持;恢复运行后禁止目标 GET。
+3. START deep 并 WAIT;首先验证该角色目标 part 恢复到故障前完整 SHA-256/长度,再 GET/Range 和版本清单。
+4. 任何仅 xl.meta 恢复、重建错误 part 或 control 变更都失败;清理时才允许使用原始备份。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [SEED_DM](validation-heal-operations.md#SEED_DM) → [FORMAT](validation-heal-operations.md#FORMAT) → [BASELINE](validation-heal-operations.md#BASELINE) → [FAULT](validation-heal-operations.md#FAULT) → [START](validation-heal-operations.md#START) → [WAIT](validation-heal-operations.md#WAIT) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 仅隔离指定对象某个 part/shard,保留 format 与其他元数据,执行 deep heal
+- 对应数据或校验分片重新生成,part/版本身份准确;不得只看到 xl.meta 重现就通过。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## C02: heal.data-shard-bitrot
+
+Topologies: H1,H2. Branches: data-first-byte, data-middle-byte, data-last-byte, multipart-data.
+
+1. 记录候选解析出的 data shard;用不重复 SHAKE 内容避免数据片/校验片摘要相同,验证健康基线。
+2. FAULT bitflip 在冻结的首/中/尾字节偏移各翻转一位;保存偏移、注入前后 hash 及目标物理角色。
+3. 保持足够正确来源,分别执行本批预注册 deep/read-repair 触发;手工实例不得预读目标。
+4. 目标恢复完整原始片摘要,解码后全文和 Range 正确;只返回正确 GET 但坏片仍在必须失败。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [SEED_DM](validation-heal-operations.md#SEED_DM) → [FORMAT](validation-heal-operations.md#FORMAT) → [FAULT](validation-heal-operations.md#FAULT) → [START](validation-heal-operations.md#START) → [WAIT](validation-heal-operations.md#WAIT) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 在指定 data shard 的已知偏移翻转字节,包含首/中/尾位置
+- 故障前后摘要证明注入生效;deep 或 read-repair 发现损坏并修复;独立 bitrot/EC 及 S3 全内容验证通过。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## C03: heal.parity-shard-bitrot
+
+Topologies: H1,H2. Branches: parity-first-byte, parity-middle-byte, parity-last-byte.
+
+1. 选择经 FORMAT 证明的 parity 片,保留足够 data;普通读取是否需要该片另作控制实例,不能触发后再声称纯 deep。
+2. FAULT bitflip 覆盖首/中/尾位置;注入后仅 PHYSICAL,不 GET/HEAD。
+3. START deep/WAIT,要求 repaired 责任对应正确对象/版本/pool/set;与修复前同角色 SHA-256 比较。
+4. 全文 GET 及版本/metadata 对照通过且错误 parity 已消失才通过;后台提前补片则该触发实例未观测。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [FORMAT](validation-heal-operations.md#FORMAT) → [FAULT](validation-heal-operations.md#FAULT) → [START](validation-heal-operations.md#START) → [WAIT](validation-heal-operations.md#WAIT) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 损坏 parity shard,使普通 GET 仍可能成功,再执行 deep heal
+- 必须识别并重建损坏 parity;只有 GET 正确、无目标分片修复不能通过。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## C04: heal.truncated-and-zeroed-shard
+
+Topologies: H1,H2. Branches: truncate-half, truncate-near-tail, zero-all, checksum-header.
+
+1. 保存外部 part 完整备份及正确 metadata quorum;每种损坏使用新的独立对象。
+2. FAULT truncate 默认保留一半;near-tail 按 FAULT_FORMAT 手册只截去最后一字节并记录原始长度;zero 清零全部;header 翻转前 32 字节。
+3. 见证实际长度/摘要差异后 START deep,捕获短读/校验诊断与责任结果。
+4. 目标原始长度和完整片摘要恢复,GET 无截断/补零,控制对象不变;拒绝把一份可解析 metadata 当内容健康。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [BASELINE](validation-heal-operations.md#BASELINE) → [FAULT](validation-heal-operations.md#FAULT) → [FAULT_FORMAT](validation-heal-operations.md#FAULT_FORMAT) → [START](validation-heal-operations.md#START) → [WAIT](validation-heal-operations.md#WAIT) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 分别截断 shard、置零和破坏校验头,保留正确元数据仲裁来源
+- 短读、全零和校验损坏被准确识别;修复不能接受截断内容或错误长度。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## C05: heal.missing-or-corrupt-xlmeta
+
+Topologies: H1,H2. Branches: missing-xlmeta, truncated-xlmeta, corrupt-xlmeta, inline-payload, versioned-xlmeta.
+
+1. DB 小对象与 DV 历史版本先全量读取,FORMAT 解析每盘 xl.meta 的版本/inline/data_dir 关联并保存解析器源码 SHA。
+2. 在少数成员上对精确对象 xl.meta 做 missing/truncate/header;inline 实例仅由匹配候选的解析器定位 payload 后翻位,不能随便偏移后称 inline。
+3. 保留同版本正确来源,START deep/WAIT;恢复后重新解析 xl.meta,核对所有版本、marker 与 data 引用。
+4. 每个 versionId 全 GET 和版本集合一致;可以有合法重编码,但不能新建空 metadata、遗漏历史版本或移动删除语义。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [SEED_DV](validation-heal-operations.md#SEED_DV) → [FORMAT](validation-heal-operations.md#FORMAT) → [FAULT](validation-heal-operations.md#FAULT) → [FAULT_FORMAT](validation-heal-operations.md#FAULT_FORMAT) → [START](validation-heal-operations.md#START) → [WAIT](validation-heal-operations.md#WAIT) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 在少数成员上移走或损坏 xl.meta/inline 内容,保留足够权威来源
+- 恢复同一对象的全部应存版本、数据引用与删除标记;不以新建空元数据替代真正修复。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## C06: heal.stale-version-rejoin
+
+Topologies: H1,H2. Branches: unversioned-overwrite, delete-historical-version, delete-marker.
+
+1. 保存 DV/旧代次,DRIVE_GATE 让一个成员离线且其旧数据原样保留。
+2. 在线 quorum 执行新内容覆盖、精确旧 versionId 删除或创建 marker;串行 ACK 确定因果顺序,并在在线端确认提交。
+3. 恢复携带旧数据的同一盘,核对原 disk UUID 后观察手工/自动修复,并保存旧/新代次权威选择日志。
+4. 最终最新值与全部历史/marker 严格匹配 ACK 清单;已删除版本不复活;目标旧盘同一 key 的 metadata 与内容纠正。
+
+Operations: [SEED_DV](validation-heal-operations.md#SEED_DV) → [DRIVE_GATE](validation-heal-operations.md#DRIVE_GATE) → [WORKLOAD](validation-heal-operations.md#WORKLOAD) → [BACKGROUND](validation-heal-operations.md#BACKGROUND) → [START](validation-heal-operations.md#START) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 盘离线后执行覆盖/版本删除,再让携带旧元数据的盘重入
+- 以已确认的新代次/版本为准;旧当前值与已删版本不复活,旧盘被纠正而非成为新的错误权威。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## C07: heal.conflicting-subquorum-authority
+
+Topologies: H1,H2. Branches: disjoint-version-quorums, disjoint-incarnations, native-authority-fence.
+
+1. 同一个本例 key 串行写两个不同版本/代次,保存各自成员副本;按 FORMAT_BAD 在静止服务下组合为两个各小于 R 的集合,原片全部保留。
+2. 解析每个成员版本身份,生成按 version/incarnation 分组的计数表;即使总数达到 R,也证明每一组都没有 quorum。
+3. GET/deep heal 必须安全拒绝,不以较新 mtime 或跨版本片并集提交;检查原本仍可恢复的证据没有被 remove。
+4. 运行匹配源代码的 NATIVE 权威仲裁回归;在真机解除混合现场后恢复原夹具并 VERIFY,两类证据分开计数。
+
+Operations: [SEED_DV](validation-heal-operations.md#SEED_DV) → [FORMAT](validation-heal-operations.md#FORMAT) → [FORMAT_BAD](validation-heal-operations.md#FORMAT_BAD) → [GET](validation-heal-operations.md#GET) → [START](validation-heal-operations.md#START) → [WAIT](validation-heal-operations.md#WAIT) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [RECOVER](validation-heal-operations.md#RECOVER) → [VERIFY](validation-heal-operations.md#VERIFY) → [NATIVE](validation-heal-operations.md#NATIVE)
+
+Source assertions:
+
+- 用受控原生夹具构造互不构成 quorum 的版本/代次集合,再做对应真机反例
+- 禁止把不同版本的 shard 数量并集当成权威;无法证明时安全拒绝且保留可恢复证据,不猜测较新时间戳。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## C08: heal.metadata-payload-mismatch
+
+Topologies: H1,H2. Branches: length-mismatch, payload-mismatch, insufficient-authority.
+
+1. 先保存版本化外部 part 的全套 metadata 与 hash;从本例另一种长度/内容的版本取得替换 payload。
+2. 停服务按 FAULT_FORMAT 只更换少数 part 的内容或截断,保留原 xl.meta 声称的长度/摘要,不改健康成员。
+3. deep heal 对比 metadata 与实际内容;充分权威实例应重建,不充分权威实例应拒绝且保留好的副本。
+4. 取证后恢复,验证精确版本无跨代内容;保留实际解析结果,不把任意无效 header 注入当作长度字段覆盖。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [SEED_DV](validation-heal-operations.md#SEED_DV) → [FORMAT](validation-heal-operations.md#FORMAT) → [FAULT_FORMAT](validation-heal-operations.md#FAULT_FORMAT) → [START](validation-heal-operations.md#START) → [WAIT](validation-heal-operations.md#WAIT) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [RECOVER](validation-heal-operations.md#RECOVER) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 构造元数据声称某长度/摘要但实际 part 不匹配的少数副本
+- 元数据与内容联合判断;不把可解析的 xl.meta 当健康;权威不足时拒绝覆盖正确副本。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## C09: heal.foreign-format-and-unknown-schema
+
+Topologies: H1,H2. Branches: foreign-disk, foreign-set, future-format, future-object-schema.
+
+1. 用第二个隔离测试集群生成真实外来 disk/set format;当前实例只有一个替换逻辑根可被修改。
+2. FORMAT_BAD 在服务停止时分别装入外来 format 或按候选解析器支持的版本头写入未来 schema;记录原始 bytes/digest,不碰共享盘 format。
+3. 启动、GET/deep heal,核对明确隔离/unsupported,不得把未知格式当 missing 初始化或写成新集群。
+4. 停止服务收集原记录仍存在的物理证据;撤回坏根、恢复原身份后完整 VERIFY;无法确认格式字段不能冒充已注入。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [FORMAT](validation-heal-operations.md#FORMAT) → [REPLACE](validation-heal-operations.md#REPLACE) → [FORMAT_BAD](validation-heal-operations.md#FORMAT_BAD) → [PROCESS](validation-heal-operations.md#PROCESS) → [START](validation-heal-operations.md#START) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [RECOVER](validation-heal-operations.md#RECOVER) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 提供外来 disk/set 身份、未来 schema 或不支持的记录格式
+- 明确隔离/拒绝且不破坏原记录,不把未知当 missing 后直接重建;解除故障后原数据可用。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## C10: heal.dangling-and-orphan-safety
+
+Topologies: H1,H2. Branches: remove-false, remove-true-before-grace, remove-true-after-grace, shared-version-reference, pending-mpu.
+
+1. 创建 DV/DM 并合法覆盖/删除形成候选孤儿;以匹配格式解析器列出所有 version→data_dir 引用和 pending UploadId。
+2. 只在没有活引用的本次测试目录形成 dangling;保存 orphan 成因与真实年龄,宽限期实例等待配置窗口,不改全局时间。
+3. 分别 START remove=false/true;宽限期前、共享历史引用、pending MPU 必须保留,过期且有充分删除证明的孤儿才允许清理。
+4. 比较物理引用集合,所有应存版本及未完成 upload list_parts 完整;remove 误删任何共享引用即失败,救援只在取证后。
+
+Operations: [SEED_DV](validation-heal-operations.md#SEED_DV) → [SEED_DM](validation-heal-operations.md#SEED_DM) → [FORMAT](validation-heal-operations.md#FORMAT) → [FAULT_FORMAT](validation-heal-operations.md#FAULT_FORMAT) → [START](validation-heal-operations.md#START) → [WAIT](validation-heal-operations.md#WAIT) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 构造 dangling part、旧 data_dir 与共享历史引用,分别 remove=false/true、宽限期前后检查
+- 默认不得破坏仍可恢复数据;显式 remove 只清除具有充分删除证明且过期的本次孤儿,不删共享版本或 MPU。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## C11: heal.bucket-incarnation-recreation
+
+Topologies: H1,H2. Branches: old-metadata-rejoin, old-checkpoint-replay, old-mrf-replay, native-incarnation-fence.
+
+Execution policy: `evidence-blocked`; disabled by default: `false`.
+
+Blocked reason (`EVIDENCE_INCARNATION_LIVE_ORCHESTRATION`): 已实现 H1/H2 旧元数据重返、真实 checkpoint/resume 回放、ACK 绑定的 committed MRF 回放和精确 native 证据门禁;尚缺全部现场分支的独立审计,诊断执行保持 FAIL/BLOCKED,原生前置通过不能替代 canonical 验收。
+
+1. 建旧 DV 桶并让一盘离线;按 S3 正常列举/删除全部版本和 marker,确认 delete_bucket 成功,禁止直接内部清空绕过删除。
+2. 同名 create_bucket 后写新内容,保存新 incarnation;恢复旧盘并让旧任务/持久 MRF 重放。
+3. 对比任务身份和代次守卫,旧责任不得删除/覆盖新对象,不得把旧 marker 或数据复活到新桶。
+4. 按 NATIVE 验证精确 incarnation 提交屏障;真机清单及新桶配置始终匹配成功 ACK,旧/新物理范围独立留证。
+
+Operations: [SEED_DV](validation-heal-operations.md#SEED_DV) → [DRIVE_GATE](validation-heal-operations.md#DRIVE_GATE) → [S3](validation-heal-operations.md#S3) → [BACKGROUND](validation-heal-operations.md#BACKGROUND) → [START](validation-heal-operations.md#START) → [FORMAT](validation-heal-operations.md#FORMAT) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY) → [NATIVE](validation-heal-operations.md#NATIVE)
+
+Source assertions:
+
+- 盘离线时完成旧桶合法清空/删除,再同名建桶写入新对象,恢复旧盘与旧任务
+- 旧 bucket incarnation 的 checkpoint/MRF/元数据不得污染新桶;旧任务不得删除或恢复新 incarnation 的对象。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## D01: heal.object-size-boundaries
+
+Topologies: H1,H2. Branches: zero, one, 4kib-minus-exact-plus, inline-minus-exact-plus, stripe-minus-exact-plus, large-external.
+
+1. SEED_DB 显式包含 0、1、4095、4096、4097;从固定候选配置/FORMAT 获得 inline 与 EC block 阈值,新增阈值±1 和一个多 block 外部对象。
+2. 每个大小先按物理记录确认实际形态;零字节/inline 没有外部 part 时改用合法 metadata/inline 故障,不能虚构 part。
+3. data/parity 上依次 missing 与 bitflip,START deep/WAIT;每个形态独立 variant 计数。
+4. 核对全体长度/摘要/ETag/HEAD/LIST;跨边界及末字节 Range 为正确 206,空对象范围按冻结契约拒绝。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [FORMAT](validation-heal-operations.md#FORMAT) → [FAULT](validation-heal-operations.md#FAULT) → [FAULT_FORMAT](validation-heal-operations.md#FAULT_FORMAT) → [START](validation-heal-operations.md#START) → [WAIT](validation-heal-operations.md#WAIT) → [RANGE](validation-heal-operations.md#RANGE) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 覆盖 0/1 字节、页/inline/EC block 边界±1以及大对象
+- 每个实际存储形态均确认后注入;长度、SHA-256、ETag、HEAD/LIST 与边界 Range 正确,无假设阈值。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## D02: heal.completed-multipart-object
+
+Topologies: H1,H2. Branches: first-part, middle-part, last-part, cross-part-range.
+
+1. SEED_DM 建至少三 part 且非末 part≥5 MiB,非等长内容和独立 nonce;保存 UploadId、每个 part ETag、Complete ACK/最终版本。
+2. 每实例损坏第一、中间、末尾 part 对应的一个物理片;按 FORMAT 映射实际 part,不依赖文件名猜逻辑位置。
+3. START deep/WAIT 后先验证对应目标片,再完整 GET;最终 ETag 应与基线一致,但不用 multipart ETag 计算整对象 MD5。
+4. RANGE 覆盖两个 part 边界及尾部,比较逐字节/摘要、metadata 和版本;其他未完成 MPU 仍可 list_parts。
+
+Operations: [SEED_DM](validation-heal-operations.md#SEED_DM) → [FORMAT](validation-heal-operations.md#FORMAT) → [FAULT](validation-heal-operations.md#FAULT) → [START](validation-heal-operations.md#START) → [WAIT](validation-heal-operations.md#WAIT) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [RANGE](validation-heal-operations.md#RANGE) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 修复已完成的不等长多 part 对象,分别损坏首/中/末 part
+- 完整内容、part 顺序、原版本/ETag/元数据和跨 part Range 不变;multipart ETag 不能当整对象 MD5。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## D03: heal.historical-versions
+
+Topologies: H1,H2. Branches: old-version-part, current-version-part, recursive-all-versions, multiple-version-targets.
+
+1. SEED_DV 每 key 三个不同 payload 版本,逐一保存 versionId/ETag;可额外写 marker,清单不按当前 GET 代替历史读取。
+2. 为不同历史/当前版本选不同盘和 part,metadata 故障实例保留同版本 quorum。
+3. 对 object 和 recursive bucket 分别 START/WAIT;物理解析必须显示所有应存 version/data_dir 而不是只最新对象。
+4. 对每个原 versionId 全 GET,LIST versions 无新增/遗漏,isLatest 身份与 marker 顺序保持。
+
+Operations: [SEED_DV](validation-heal-operations.md#SEED_DV) → [FORMAT](validation-heal-operations.md#FORMAT) → [FAULT](validation-heal-operations.md#FAULT) → [START](validation-heal-operations.md#START) → [WAIT](validation-heal-operations.md#WAIT) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 不同版本分布不同损坏位置,按对象和递归范围修复
+- 所有原 versionId 可寻址且内容正确,最新版本身份不漂移,无多余版本或遗漏历史版本。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## D04: heal.delete-marker-semantics
+
+Topologies: H1,H2. Branches: current-marker, historical-marker, missing-marker-replica, stale-disk-marker.
+
+1. 创建三个数据版本与当前/历史 marker,记录 delete_object 返回的 VersionId/DeleteMarker。
+2. 缺少 marker 的实例使用 xl.meta 少数副本缺失;陈旧实例通过成员离线后合法创建/删除 marker 形成,不能直接改任意 metadata。
+3. heal 后 current GET/HEAD 对当前 marker 应为 404 且正确 marker 身份头;指定 marker versionId 的 GET 按 S3 契约为 405 并保留版本头。
+4. LIST versions 精确集合与最新标记一致;显式删除当前 marker 后露出的应是预期历史对象,不能产生额外数据版本。
+
+Operations: [SEED_DV](validation-heal-operations.md#SEED_DV) → [S3](validation-heal-operations.md#S3) → [FAULT](validation-heal-operations.md#FAULT) → [DRIVE_GATE](validation-heal-operations.md#DRIVE_GATE) → [START](validation-heal-operations.md#START) → [WAIT](validation-heal-operations.md#WAIT) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 缺失/陈旧 marker 副本后 heal,并按当前值与精确 marker versionId 查询
+- 当前 GET/HEAD 与指定 marker 的状态码及身份头符合契约;marker 不复活数据,LIST versions 无重复。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## D05: heal.versioning-suspended-null
+
+Topologies: H1,H2. Branches: enabled-to-suspended, null-overwrite, null-delete-marker.
+
+1. 启用版本写三代,再 put_bucket_versioning Status=Suspended,连续写两次 null 版本并创建/删除 null marker。
+2. 基线同时保存非 null 历史和当前 null 身份;对目标外部 part 或 xl.meta 制造少数缺失。
+3. START deep/WAIT;记录 null 不能被转换成随机 versionId,也不能与启用期历史版本混片。
+4. VERIFY 全部原 versionId、null 内容和 marker;再次开启版本后写新对象,验证旧历史仍可寻址。
+
+Operations: [SEED_DV](validation-heal-operations.md#SEED_DV) → [S3](validation-heal-operations.md#S3) → [FORMAT](validation-heal-operations.md#FORMAT) → [FAULT](validation-heal-operations.md#FAULT) → [START](validation-heal-operations.md#START) → [WAIT](validation-heal-operations.md#WAIT) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 在版本开启、暂停、null 版本覆盖之间建夹具再修复
+- null 和非 null 版本不混淆,不新增假版本;暂停前历史对象仍准确可读。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## D06: heal.object-metadata-and-tags
+
+Topologies: H1,H2. Branches: user-metadata, content-headers, versioned-tags.
+
+1. 用 DS 设置中文值可编码的 metadata、Content-Type、Content-Encoding、Content-Disposition、Cache-Control 与两组 tags;内容本身使用已声明编码。
+2. 先 GET/HEAD/get_object_tagging 精确记录;对外部 part 和少数 xl.meta 分别故障。
+3. START deep/WAIT,比较所有对外属性和逐版本 tags,修复不新增版本、不丢用户 key 大小写规范化结果。
+4. 允许内部 metadata 合法重写,但必须通过 FORMAT 引用和物理内容证明;不把 Content-Encoding 当服务端压缩已启用。
+
+Operations: [SEED_DS](validation-heal-operations.md#SEED_DS) → [BASELINE](validation-heal-operations.md#BASELINE) → [FAULT](validation-heal-operations.md#FAULT) → [START](validation-heal-operations.md#START) → [WAIT](validation-heal-operations.md#WAIT) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 包含用户 metadata、Content-Type/Encoding/Disposition、tags 的对象修复
+- 对外 metadata、tags 和相关响应头逐项相同;不要求无关内部元数据字节完全一致。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## D07: heal.range-head-and-conditions
+
+Topologies: H1,H2. Branches: head, range-valid, range-outside, if-match, if-none-match, if-modified-since, if-unmodified-since.
+
+1. DB/DM 基线保存 ETag/LastModified 和跨 part 内容;先执行所有条件请求,冻结预期 200/206/304/412/416。
+2. 对精确 part 制造缺失/坏校验并 repair;同一组 HEAD、Range、IfMatch/IfNoneMatch、时间条件再次请求。
+3. 304/412/416 验证响应无错误成功内容;合法范围比较准确长度、ContentRange 和原片段摘要。
+4. 完整 VERIFY 版本和 metadata;条件请求必须用 API 参数传递,不由本地短路模拟状态码。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [SEED_DM](validation-heal-operations.md#SEED_DM) → [RANGE](validation-heal-operations.md#RANGE) → [FAULT](validation-heal-operations.md#FAULT) → [START](validation-heal-operations.md#START) → [WAIT](validation-heal-operations.md#WAIT) → [RANGE](validation-heal-operations.md#RANGE) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 修复前后验证 HEAD、合法/越界 Range、If-Match/If-None-Match 及时间条件
+- 200/206/304/412/416 等按逐请求契约判断,绝不返回旧内容、错误范围或错误 ETag。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## D08: heal.encrypted-objects
+
+Topologies: H1,H2. Branches: sse-s3, sse-kms, sse-c, encrypted-versioned.
+
+1. DEPENDENCY 先验证候选支持的每种 SSE 与专用 KMS/key 就绪;不支持的产品能力按明确错误契约另记,不能删除该参数实例。
+2. 用 S3 ServerSideEncryption/SSEKMSKeyId 或 SSECustomerAlgorithm/SSECustomerKey 创建对象;密钥从环境注入到内存,不放命令行/清单。
+3. 在密文物理层选择 part 做 missing/bitflip;deep heal 后比较同角色原密文片或匹配候选的重编码验证。
+4. 用正确授权逐版本解密 GET 校验原文,用缺少/错误密钥请求验证拒绝;不得写出明文替代或把密钥错误判成不存在。
+
+Operations: [DEPENDENCY](validation-heal-operations.md#DEPENDENCY) → [SEED_DE](validation-heal-operations.md#SEED_DE) → [FORMAT](validation-heal-operations.md#FORMAT) → [FAULT](validation-heal-operations.md#FAULT) → [START](validation-heal-operations.md#START) → [WAIT](validation-heal-operations.md#WAIT) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [S3](validation-heal-operations.md#S3) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 按候选支持的 SSE-S3/SSE-KMS/SSE-C 建加密对象并修复
+- 授权解密后原内容及版本一致,物理验证使用正确编码/密文层;密钥不可用时不误删、不降级明文,证据脱敏。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## D09: heal.object-lock-retention
+
+Topologies: H1,H2. Branches: governance-retention, compliance-retention, legal-hold, historical-protected-version.
+
+1. 创建启用 ObjectLock 的专用桶,SEED_DO 使用短且真实未到期的 retention 或 ON legal hold;记录 UTC 截止和独占资源到期安排。
+2. 在无 bypass 权限角色下对受保护 versionId DELETE 必须 AccessDenied,确认保护本来有效。
+3. 对受保护版本缺片/xl.meta 缺失后 repair,核对内容及 get_object_retention/legal_hold;保留期内再次删除仍拒绝。
+4. legal hold 仅正常 API 解除;Compliance 等到真实到期后清理,禁止改时钟或内部删文件;未到期现场标 RETAINED,不能交还空环境。
+
+Operations: [SEED_DO](validation-heal-operations.md#SEED_DO) → [S3](validation-heal-operations.md#S3) → [FAULT](validation-heal-operations.md#FAULT) → [START](validation-heal-operations.md#START) → [WAIT](validation-heal-operations.md#WAIT) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY) → [RECOVER](validation-heal-operations.md#RECOVER)
+
+Source assertions:
+
+- 对开启 retention/legal hold 的受保护版本修复,期间测试非法删除
+- heal 不绕过保留/hold,也不丢失其元数据;拒绝删除与合法修复均有明确证据。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## D10: heal.special-keys-and-pagination
+
+Topologies: H1,H2. Branches: unicode-space-percent, encoded-slash, adjacent-prefixes, multi-page-list.
+
+1. DS 创建中文、空格、%2F 字面量、真实 / 和多层前缀键;至少 257 个对象并设置 List MaxKeys=17,确保跨页。
+2. FORMAT 建逻辑 key→物理路径映射,确认 %2F 与 / 不混淆;文件故障只用受限相对路径工具,不拼 shell。
+3. 对选定 key 分别 missing part/xl.meta,使用 URL percent-encode 后的 Admin scope,deep heal。
+4. 分页 LIST/versions 与 manifest 无遗漏/重复;控制前缀不变;对每个特殊 key 精确 GET 和 metadata 校验。
+
+Operations: [SEED_DS](validation-heal-operations.md#SEED_DS) → [FORMAT](validation-heal-operations.md#FORMAT) → [FAULT](validation-heal-operations.md#FAULT) → [START](validation-heal-operations.md#START) → [WAIT](validation-heal-operations.md#WAIT) → [S3](validation-heal-operations.md#S3) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 覆盖中文、空格、百分号、编码斜线、多层前缀和多页对象列表
+- 逻辑 key 与物理路径映射准确,不跨桶/前缀;分页无遗漏重复,不能把用户 key 当可执行 shell 或裸文件路径。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## D11: heal.bucket-metadata-integrity
+
+Topologies: H1,H2. Branches: versioning-config, bucket-policy-tags, lock-config, unknown-record-negative.
+
+1. DS/DV 创建隔离桶配置:版本、只授权本桶的策略、bucket tags、可到期锁配置;通过 S3 GET 配置存基线。
+2. FORMAT_BAD 白名单仅包含该桶对应的系统 bucket metadata 文件;在匹配格式/可恢复契约已核实后少数副本缺失/截断。
+3. 不能使用通用 heal_fault 修改 .rustfs.sys;按专用系统记录手册在停止服务、精确备份和审批清单下操作,未知格式实例只要求安全拒绝。
+4. repair 后逐 API 读取配置比对,业务对象版本完整;确认 IAM/凭据配置及其他桶系统记录完全未变。
+
+Operations: [SEED_DS](validation-heal-operations.md#SEED_DS) → [SEED_DV](validation-heal-operations.md#SEED_DV) → [S3](validation-heal-operations.md#S3) → [FORMAT](validation-heal-operations.md#FORMAT) → [FAULT_FORMAT](validation-heal-operations.md#FAULT_FORMAT) → [START](validation-heal-operations.md#START) → [WAIT](validation-heal-operations.md#WAIT) → [VERIFY](validation-heal-operations.md#VERIFY) → [PHYSICAL](validation-heal-operations.md#PHYSICAL)
+
+Source assertions:
+
+- 在隔离桶白名单内测试可支持的桶元数据恢复,并核对外部 bucket 配置
+- 版本设置、策略/标签/保留配置按契约保持;不得遍历凭据配置或把未知内部记录当可重建数据。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## E01: heal.manual-normal-attribution
+
+Topologies: H1,H2. Branches: normal-missing-data, normal-missing-parity.
+
+1. 部署 manual 配置:关闭 scanner、auto-heal、MRF;保留 format,记录服务实际环境与 PID,不只保存拟用 env 文件。
+2. FAULT missing 后不能 GET/HEAD 目标;先确认缺失仍在,再 START scanMode=1。
+3. WAIT 精确 token/outcome,先 PHYSICAL 证明目标新生成,再 VERIFY;源归属需结合配置、时间线和任务对象。
+4. 正常扫描未发现静默 bitrot 不能因此判 E01 失败;那是 E02/C02/C03 的 deep 实例。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [CONFIG](validation-heal-operations.md#CONFIG) → [FAULT](validation-heal-operations.md#FAULT) → [START](validation-heal-operations.md#START) → [WAIT](validation-heal-operations.md#WAIT) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 禁止预读目标,保留 format,隔离背景干扰后显式 normal heal
+- 证明本次 Admin token 实际覆盖目标并修复可由 normal 检出的缺失;不要求 normal 发现所有静默 bitrot。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## E02: heal.manual-deep-attribution
+
+Topologies: H1,H2. Branches: deep-parity-bitrot, deep-data-bitrot.
+
+1. manual 配置排除其他后台来源;FORMAT 确认分片角色,注入 bitflip 并保存坏片仍存在证据。
+2. 不先读目标,START scanMode=2;保留请求 body、clientToken、incarnation/pool/set 与最终对象 disposition。
+3. 先验证坏片完整恢复,再读取原文;如果任务开始前已有恢复,记录 ATTRIBUTION_NOT_OBSERVED。
+4. 检查健康 control 没被意外修改,source 不能仅由最后一次发出的命令推测。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [CONFIG](validation-heal-operations.md#CONFIG) → [FORMAT](validation-heal-operations.md#FORMAT) → [FAULT](validation-heal-operations.md#FAULT) → [START](validation-heal-operations.md#START) → [WAIT](validation-heal-operations.md#WAIT) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 在普通读取仍可成功的 parity 损坏上显式 deep heal
+- 按 token/对象/版本及物理变更证明 deep 检查和修复,不把后台提前修复算给手工任务。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## E03: heal.read-repair-attribution
+
+Topologies: H1,H2. Branches: get-missing, range-bitrot, historical-version-read.
+
+1. 关闭 scanner/auto-heal,按候选 read-repair 实现配置其所需 MRF 队列,记录为何开启;不发送 Admin heal。
+2. FAULT 后物理证明损坏尚存;只对一个精确 key/version 发一次带请求 ID 的 GET 或 Range。
+3. GET 内容先验正确还不够:只观察 PHYSICAL/后台状态/日志,直到该片重建;异步路径需关联 read-repair 入队和责任完成。
+4. 完成补片后才全量 VERIFY;如果只能看到读取成功但找不到 source 证据,该路径仍 NOT_OBSERVED,不能计 PASS。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [SEED_DV](validation-heal-operations.md#SEED_DV) → [CONFIG](validation-heal-operations.md#CONFIG) → [FAULT](validation-heal-operations.md#FAULT) → [GET](validation-heal-operations.md#GET) → [RANGE](validation-heal-operations.md#RANGE) → [BACKGROUND](validation-heal-operations.md#BACKGROUND) → [LOGS](validation-heal-operations.md#LOGS) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 不发 Admin heal,对已确认的特定损坏对象发一次可追踪 GET/Range
+- 读取正确且后续真实补片;若走异步队列需证实 source=read-repair,不能只有 GET 成功。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## E04: heal.scanner-cold-object-repair
+
+Topologies: H1,H2. Branches: cold-missing, cold-bitrot, scanner-cycle-boundary.
+
+1. 启用 scanner 和所需自动修复,预注册 cycle/start delay/bitrot cycle 与预算;先用健康冷对象证明扫描器实际巡检。
+2. 注入时用 PROCESS 短停以保持静止窗口;重启后不发目标 GET/HEAD、不发 Admin heal。
+3. 只用磁盘快照、scanner 遍历日志和后台责任观察,证明当前周期检查目标,记录发现→入队→补片。
+4. PHYSICAL 完成后再全量 VERIFY;单纯周期计数增长、全局 idle 或其他自动 replacement 先修不能记 scanner 通过。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [CONFIG](validation-heal-operations.md#CONFIG) → [PROCESS](validation-heal-operations.md#PROCESS) → [FAULT](validation-heal-operations.md#FAULT) → [BACKGROUND](validation-heal-operations.md#BACKGROUND) → [LOGS](validation-heal-operations.md#LOGS) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 不 GET/HEAD 损坏目标、不发手工 heal,仅等待已启用的 scanner 路径
+- 冷对象被发现并修复,来源和遍历窗口明确;scanner 周期结束但目标没检查不能通过。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## E05: heal.mrf-offline-write-replay
+
+Topologies: H2. Branches: offline-put, offline-version-delete, offline-marker.
+
+1. 在 H2 开启 durable MRF,先验证目标节点离线时剩余同 set 满足提交条件;DRIVE_GATE 或 PROCESS 只影响目标成员。
+2. 在线节点执行唯一 key PUT、精确版本 DELETE、marker 操作;逐个保存 ACK 和责任 identity。
+3. 在目标仍离线时采集 journal/状态,证明责任已持久化;恢复同一盘身份,不发手工 heal。
+4. 观察 MRF 重放和退休理由;只有实际 repaired/verified_healthy/authoritatively_absent 后退休;VERIFY 全部 ACK 和目标片,不能以队列空独自证明。
+
+Operations: [SEED_DV](validation-heal-operations.md#SEED_DV) → [CONFIG](validation-heal-operations.md#CONFIG) → [DRIVE_GATE](validation-heal-operations.md#DRIVE_GATE) → [WORKLOAD](validation-heal-operations.md#WORKLOAD) → [CHECKPOINT](validation-heal-operations.md#CHECKPOINT) → [BACKGROUND](validation-heal-operations.md#BACKGROUND) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 成员离线时完成有 quorum 的 PUT/DELETE,确认待修责任后重入
+- durable MRF 责任持久且身份精确;只在 repaired/verified healthy/authoritatively absent 等已证明结果后释放,成功提交不丢失。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## E06: heal.replacement-auto-rebuild
+
+Topologies: H1,H2. Branches: replacement-unversioned, replacement-versioned-marker.
+
+1. 启用 replacement auto-heal;REPLACE 注入空逻辑盘,旧盘备份对服务不可达;保存新 UUID/slot。
+2. 重启/接纳后不发 Admin heal、不读目标;仅查询 BACKGROUND/REPLACEMENT、日志及 PHYSICAL。
+3. 从 replacement generation、ready 与责任完成状态关联所有 manifest 对象/版本的补片时间线。
+4. 全部目标分片实际重建后 VERIFY;服务 ready、计数重置或队列空都不能提前结束。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [SEED_DV](validation-heal-operations.md#SEED_DV) → [CONFIG](validation-heal-operations.md#CONFIG) → [REPLACE](validation-heal-operations.md#REPLACE) → [FORMAT](validation-heal-operations.md#FORMAT) → [REPLACEMENT](validation-heal-operations.md#REPLACEMENT) → [BACKGROUND](validation-heal-operations.md#BACKGROUND) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 替换盘接纳后不发 Admin/目标 GET,观察自动 replacement heal
+- 替换代次、ready/completion 与盘上全部目标版本一致;不能用服务 ready、队列空或计数重置代替完成。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## F01: heal.root-graceful-coordinator-restart
+
+Topologies: H1,H2. Branches: root-partial-graceful, same-record-disk-resume.
+
+1. DX root heal 启动前保存原协调者、拓扑、记录盘和预算;所有 root checkpoint 通过 CHECKPOINT 只读白名单采集。
+2. WINDOW 证明 0<已修目标数<总数 且任务 running,再 PROCESS graceful-stop 原协调者;记录退出信号和持久记录。
+3. 用原 unit/env/数据根/候选重启原协调者,不换记录盘;观察原任务 ID/剩余责任恢复,不能将新扫描当原任务续跑。
+4. 最终全部对象/片正确,root 活动记录退休,重复重启无重复任务;未观察部分进度保留 NOT_OBSERVED。
+
+Operations: [SEED_DX](validation-heal-operations.md#SEED_DX) → [FAULT](validation-heal-operations.md#FAULT) → [START](validation-heal-operations.md#START) → [WINDOW](validation-heal-operations.md#WINDOW) → [CHECKPOINT](validation-heal-operations.md#CHECKPOINT) → [PROCESS](validation-heal-operations.md#PROCESS) → [STATUS](validation-heal-operations.md#STATUS) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- root heal 非零进度时优雅重启原协调者,保留原拓扑与记录盘
+- 在明确支持的边界内恢复原 task ID 和剩余责任/预算,完成后无遗留活动记录;不扩张为协调盘丢失后的故障转移。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## F02: heal.target-graceful-restart
+
+Topologies: H2. Branches: target-partial-graceful, versioned-target-partial.
+
+1. REPLACE 目标逻辑盘并启用自动重建,使用 DX/DV 使目标总量可观测;协调者不是重启目标。
+2. PHYSICAL 与 REPLACEMENT 双证实目标只完成部分,再 PROCESS 对目标 unit 优雅停止;保存 MainPID、退出码、前后 boot_id。
+3. 保留同一新 disk UUID/代次,正常启动原目标服务;不能换新目录重测。
+4. 观察余下目标继续构建且任务责任一致,全部版本/marker/片完整;备份不得参与重建。
+
+Operations: [SEED_DX](validation-heal-operations.md#SEED_DX) → [SEED_DV](validation-heal-operations.md#SEED_DV) → [REPLACE](validation-heal-operations.md#REPLACE) → [REPLACEMENT](validation-heal-operations.md#REPLACEMENT) → [WINDOW](validation-heal-operations.md#WINDOW) → [PROCESS](validation-heal-operations.md#PROCESS) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 替换目标仅完成部分重建时优雅重启目标节点
+- 故障确实发生于部分完成窗口;重启后续建同一目标盘/代次,最终 manifest 与全部目标分片正确。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## F03: heal.target-process-crash
+
+Topologies: H2. Branches: target-partial-sigkill, target-partial-sigkill-versioned.
+
+1. 与 F02 同样准备替换盘和可观察部分进度,先冻结将被杀掉的 unit MainPID/进程启动时间。
+2. WINDOW 内 PROCESS kill,精确 systemctl kill --signal=SIGKILL --kill-whom=main;记录 signal=9 与非优雅退出,明确不是物理断电。
+3. 正常启动同 unit/根/新 disk UUID;记录恢复 responsibility/代次,不允许丢弃尚未完成对象。
+4. 全量 VERIFY/PHYSICAL,检查临时文件和持久责任无泄漏;最终通过不抹掉这次真实中断的证据。
+
+Operations: [SEED_DX](validation-heal-operations.md#SEED_DX) → [SEED_DV](validation-heal-operations.md#SEED_DV) → [REPLACE](validation-heal-operations.md#REPLACE) → [WINDOW](validation-heal-operations.md#WINDOW) → [PROCESS](validation-heal-operations.md#PROCESS) → [CHECKPOINT](validation-heal-operations.md#CHECKPOINT) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 部分重建时 SIGKILL 测试进程,再正常启动
+- unclean shutdown 与修复进度有真实证据;已提交对象不丢失,未完成目标不会被当完整;明确不是断电测试。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## F04: heal.coordinator-process-crash
+
+Topologies: H2. Branches: root-coordinator-kill, mrf-coordinator-kill, replacement-coordinator-kill.
+
+1. 三个机制使用独立场景和冻结的持久化支持表;在协调者非零且未完成进度时采集 CHECKPOINT。
+2. PROCESS SIGKILL 协调者,目标节点保持;恢复原根/原拓扑。
+3. 分别核对机制承诺:持久任务必须续责;易失 token 丢失不能算已完成,应显示未完成并允许明确的新扫描补偿。
+4. 原任务与补偿任务分别保存;最终 VERIFY 所有 ACK、版本和片,不能把安全重扫成功写成原 token 自动恢复。
+
+Operations: [SEED_DX](validation-heal-operations.md#SEED_DX) → [SEED_DB](validation-heal-operations.md#SEED_DB) → [START](validation-heal-operations.md#START) → [WINDOW](validation-heal-operations.md#WINDOW) → [CHECKPOINT](validation-heal-operations.md#CHECKPOINT) → [PROCESS](validation-heal-operations.md#PROCESS) → [BACKGROUND](validation-heal-operations.md#BACKGROUND) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 协调者非优雅退出并恢复,分别观察 root、MRF、replacement
+- 按各机制已核实的持久化边界判断;不得把丢失的易失 token 当成功,需显式记录剩余责任并可安全重扫恢复。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## F05: heal.full-cluster-restart
+
+Topologies: H2. Branches: all-graceful-restart, all-restart-with-mpu, repeat-full-restart.
+
+1. 准备 DB/DV/DM、pending UploadId 与未完成持久修复责任;冻结全部节点/盘/slot。
+2. WINDOW 命中后按固定顺序只停止本例全部 units;保存每节点退出码、checkpoint/MRF 与完整时序。
+3. 按原拓扑同时或预注册顺序重启,等待全部 peer,不能用自动重新格式化或新数据根恢复。
+4. 完成后全版本/marker/MPU 和物理片校验,再重复一次重启验证幂等,任何丢失的已承诺责任计失败。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [SEED_DV](validation-heal-operations.md#SEED_DV) → [SEED_DM](validation-heal-operations.md#SEED_DM) → [FAULT](validation-heal-operations.md#FAULT) → [START](validation-heal-operations.md#START) → [WINDOW](validation-heal-operations.md#WINDOW) → [PROCESS](validation-heal-operations.md#PROCESS) → [CHECKPOINT](validation-heal-operations.md#CHECKPOINT) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 有未完成修复责任时重启全部测试节点,固定原拓扑/磁盘身份
+- 有持久化保证的责任不遗失;全部端点恢复一致,旧版本/marker/MPU 与目标分片完整,重复重启不扩散错误。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## F06: heal.cancelled-task-restart
+
+Topologies: H1,H2. Branches: cancel-restart, old-token-after-restart, force-new-after-cancel.
+
+1. DX 缺片任务在 WINDOW 中 STOP;STATUS 与磁盘静默窗口证明取消和在途退出完成。
+2. 保存旧 token/checkpoint,PROCESS 优雅重启;确认已取消任务不会自行复活或报 repaired。
+3. 新 START/forceStart 获取独立 token,向旧 token 发 stop/status;不得影响新任务。
+4. 新任务完成后 VERIFY/PHYSICAL;取消记录/新任务责任不串代。
+
+Operations: [SEED_DX](validation-heal-operations.md#SEED_DX) → [FAULT](validation-heal-operations.md#FAULT) → [START](validation-heal-operations.md#START) → [WINDOW](validation-heal-operations.md#WINDOW) → [STOP](validation-heal-operations.md#STOP) → [CHECKPOINT](validation-heal-operations.md#CHECKPOINT) → [PROCESS](validation-heal-operations.md#PROCESS) → [STATUS](validation-heal-operations.md#STATUS) → [VERIFY](validation-heal-operations.md#VERIFY) → [PHYSICAL](validation-heal-operations.md#PHYSICAL)
+
+Source assertions:
+
+- 完成取消及在途退出后重启,随后 forceStart/新任务正常修复
+- 已取消的任务不自动复活;旧 checkpoint/token 不能取消或覆盖新任务,旧责任不会误标为修复完成。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## F07: heal.checkpoint-and-mrf-corruption
+
+Topologies: H1,H2. Branches: checkpoint-truncate, mrf-checksum, future-schema, stale-generation, journal-write-failure, native-persist-fence.
+
+实时读取遇到原子替换后链接数为零的旧文件句柄时,先核对文件类型、大小与挂载边界,再拒绝旧句柄字节并返回读取变化。只有原有有界观察循环可以重试;停止后的读取和故障注入仍直接失败,硬链接、非普通文件、超限和挂载逃逸仍被拒绝。原 pidfd、60 秒 advance 预算和完整 checkpoint/resume/root 校验不变。Linux 原子替换组件复现仅证明读取竞争,不证明旧 live 失败的唯一原因,也不替代 RustFS 持久化屏障证据。
+
+Execution policy: `evidence-blocked`; disabled by default: `false`.
+
+Blocked reason (`EVIDENCE_DURABLE_JOURNAL_ORCHESTRATION`): 已实现 H1/H2 checkpoint 截断、未来 schema、真实较早代次回放、committed MRF CRC 故障、私有限额卷的真实 journal ENOSPC 和独立安全重扫;10 项原生回归仅为前置证据。仍缺全部所需 fsync/commit 临界点审核和完整现场分支的独立 live 审计,诊断执行不能提升 canonical 验收。
+
+Journal ENOSPC recovery preserves the first failed checkpoint/terminal publication
+attempt before restarting the original durable owner after filler release. A new
+rescan is permitted only after that same responsibility either completes with
+full physical and durable terminal proof or retains an exact failed terminal
+without a runnable owner. A still-pending scope remains a failure. Successful
+original replay must repair the complete remaining manifest before any target
+GET; the later distinct rescan must report zero repairs and preserve original
+completion bytes. No forceStart, owner deletion or old-record restoration is
+used to bypass overlap admission. This flow still requires fresh live evidence.
+
+1. 先制造并证明真实 durable root/MRF 责任,CHECKPOINT 定位精确本 run task/record,其他系统记录禁止触碰。
+2. 停止测试服务,按 FAULT_FORMAT 对这些已审核记录分别截断、坏校验、未来 schema、旧代次;写失败实例用独占限额故障层,不塞满真实共享盘。
+3. 重启后核对解析/错误状态:未知或损坏记录保留、可诊断,不能静默删除并 ACK;责任仍能安全重扫处理。
+4. 精确 fsync/commit 崩溃窗口执行 NATIVE 回归,保存屏障命中证据;真机记录损坏与原生临界点不要混算。
+
+Operations: [SEED_DV](validation-heal-operations.md#SEED_DV) → [START](validation-heal-operations.md#START) → [CHECKPOINT](validation-heal-operations.md#CHECKPOINT) → [PROCESS](validation-heal-operations.md#PROCESS) → [FAULT_FORMAT](validation-heal-operations.md#FAULT_FORMAT) → [IO_FAULT](validation-heal-operations.md#IO_FAULT) → [BACKGROUND](validation-heal-operations.md#BACKGROUND) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY) → [NATIVE](validation-heal-operations.md#NATIVE)
+
+Source assertions:
+
+- 受控构造 checkpoint/MRF 截断、坏校验、未来 schema、陈旧代次及写入失败
+- 已确认持久化责任不得静默删除或伪 ACK;未知记录保留且可诊断;精确崩溃窗口由原生屏障证明,不伪称真机命中。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## F08: heal.mixed-version-upgrade-rollback
+
+Topologies: H2. Branches: rolling-old-new, mixed-peer-status, rollback-new-old, mixed-restart.
+
+Execution policy: `environment-gated`; disabled by default: `true`.
+
+Blocked reason (`ENV_IMMUTABLE_RELEASE_PAIR`): 需要两个已固定且相互兼容的 RustFS release、混合版本 peer 状态和经过审核的回滚兼容矩阵;当前 auto-testing Heal 节点没有这组不可变双版本资源。
+
+1. 固定两个 release optimized 包的 commit/tree/binary/格式兼容说明;UPGRADE 先验证支持回滚的记录版本,不能使用任意旧包。
+2. 有持久修复责任时逐节点升级,保留数据根和 unit;混合阶段通过各 endpoint STATUS/BACKGROUND 并持续唯一版本写入。
+3. 缺少能力的 peer 必须显示 partial/unsupported,不能 aggregate 成 complete;新记录不被老节点误退休。
+4. 按兼容方案回滚并重启,验证所有 ACK 和责任;不兼容格式只测明确拒绝/保护,禁止冒险降级覆盖。
+
+Operations: [SEED_DV](validation-heal-operations.md#SEED_DV) → [START](validation-heal-operations.md#START) → [CHECKPOINT](validation-heal-operations.md#CHECKPOINT) → [UPGRADE](validation-heal-operations.md#UPGRADE) → [WORKLOAD](validation-heal-operations.md#WORKLOAD) → [STATUS](validation-heal-operations.md#STATUS) → [BACKGROUND](validation-heal-operations.md#BACKGROUND) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 固定前后两个明确兼容候选,覆盖滚动升级、混合 peer、回滚与重启
+- 能力不足必须显示 partial/unsupported 而非完整成功;按格式兼容契约保留责任,旧版本不能误退休新版本记录。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## F09: heal.dedicated-host-forced-restart
+
+Topologies: H6. Branches: dedicated-vm-forced-stop, dedicated-host-power-cycle.
+
+Execution policy: `environment-gated`; disabled by default: `true`.
+
+Blocked reason (`ENV_DEDICATED_H6_HOST`): 需要 H6 专用主机、平台强制停启权限、带外恢复和 boot_id/平台事件证据;共享 Heal 节点不能证明真实电源循环。
+
+1. 先取得 H6 专项资源 ID、独占证明和带外恢复批准;普通 fixed fleet 禁止执行本例。
+2. 持续限量写入并在 replacement 部分重建窗口,记录已成功 ACK/结果未知写集合与最近持久化证据。
+3. HOST_POWER 使用批准的平台命令强制停启,保留平台活动 ID、时间线和 boot_id;VM stop 与真实电源断电分别命名实例。
+4. 恢复后按原盘/根重启并解析未知写;全版本/片和持久责任验收,不把 SIGKILL 或软件 reboot 当作此项证据。
+
+Operations: [SEED_DX](validation-heal-operations.md#SEED_DX) → [SEED_DV](validation-heal-operations.md#SEED_DV) → [WORKLOAD](validation-heal-operations.md#WORKLOAD) → [REPLACE](validation-heal-operations.md#REPLACE) → [WINDOW](validation-heal-operations.md#WINDOW) → [HOST_POWER](validation-heal-operations.md#HOST_POWER) → [CHECKPOINT](validation-heal-operations.md#CHECKPOINT) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 经专项授权在部分修复/持续写入时强制重启独占测试 VM/主机
+- 保留平台事件与 ACK ledger;验证落盘和恢复,不把 SIGKILL 当断电;VM 强制停止也不等同于物理控制器断电。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## G01: heal.race-unversioned-overwrite
+
+Topologies: H1,H2. Branches: overwrite-vs-deep, overwrite-vs-read-repair, native-commit-cleanup.
+
+1. 同 key 串行 nonce 的覆盖客户端与 heal 客户端独立;WORKLOAD 每次记录 request-start/end、ACK、payload hash,不凭跨机墙钟判最后版本。
+2. 旧对象缺片/bitflip 后两端同时运行,WINDOW 证明实际交叠;每个 worker 的同 key PUT 必须串行,避免期望值本身无序。
+3. 停止写入后解析全部未知 ACK,按最后可证明提交核对最新 GET 与 data_dir;不能回滚旧内容。
+4. 运行 NATIVE commit/cleanup 屏障实例;真实竞争没命中精确窗口只算黑盒实例,记录 no-lock/锁重验日志。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [FAULT](validation-heal-operations.md#FAULT) → [WORKLOAD](validation-heal-operations.md#WORKLOAD) → [START](validation-heal-operations.md#START) → [WINDOW](validation-heal-operations.md#WINDOW) → [VERIFY](validation-heal-operations.md#VERIFY) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [NATIVE](validation-heal-operations.md#NATIVE)
+
+Source assertions:
+
+- 同 key 连续覆盖与 heal 同时推进,包含提交尾部清理窗口
+- 按可证明的提交顺序验证最终最后一次成功写入;不能回滚到旧 data_dir,未确认超时写入单独解析。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## G02: heal.race-versioned-put
+
+Topologies: H1,H2. Branches: new-version-vs-historical-heal, read-all-versions-during-heal, native-version-fence.
+
+1. DV 历史版本缺片时,前台串行写入独立 payload 的新版本,保存每个成功 versionId。
+2. START deep 与 WORKLOAD 交叠;读取端始终指定 versionId 验证新旧内容,禁止用 current GET 代替。
+3. 静默后按完整 ACK ledger 重建期望版本集合,最新值根据串行提交顺序判断;全量 PHYSICAL 检查不同 data_dir 无混片。
+4. 精确版本提交屏障运行 NATIVE;任何未解析超时写/缺失 ACK 阻止最终通过。
+
+Operations: [SEED_DV](validation-heal-operations.md#SEED_DV) → [FAULT](validation-heal-operations.md#FAULT) → [WORKLOAD](validation-heal-operations.md#WORKLOAD) → [START](validation-heal-operations.md#START) → [WINDOW](validation-heal-operations.md#WINDOW) → [VERIFY](validation-heal-operations.md#VERIFY) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [NATIVE](validation-heal-operations.md#NATIVE)
+
+Source assertions:
+
+- 修复历史版本时创建新版本并重复读取各 versionId
+- 新旧版本不混片、不串内容;已返回成功的每个版本都保留,最新值按确定的先后关系验收。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## G03: heal.race-version-delete
+
+Topologies: H1,H2. Branches: delete-old-version-vs-deep, delete-current-version-vs-deep, native-delete-fence.
+
+1. 选 DV 的精确 versionId 制造可恢复缺片;前台删除请求明确带该 ID,不是只创建 marker。
+2. WINDOW 内并发 delete_object 与 deep heal,保存 DELETE 成功回执和 heal scope/identity。
+3. 终态按精确 version GET 应不存在,LIST versions 不得复活;其他所有版本内容保持。
+4. 运行 NATIVE 删除/修复提交交错点;清理旧片仅作为最终物理证据,不能先用备份复活已删版本。
+
+Operations: [SEED_DV](validation-heal-operations.md#SEED_DV) → [FAULT](validation-heal-operations.md#FAULT) → [START](validation-heal-operations.md#START) → [WORKLOAD](validation-heal-operations.md#WORKLOAD) → [WINDOW](validation-heal-operations.md#WINDOW) → [S3](validation-heal-operations.md#S3) → [VERIFY](validation-heal-operations.md#VERIFY) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [NATIVE](validation-heal-operations.md#NATIVE)
+
+Source assertions:
+
+- 对损坏的精确历史 versionId 发 DELETE,与 deep heal 竞争
+- 已确认删除的版本不能被复活,其他历史版本不受影响;原生临界点测试与真机竞争分别留证。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## G04: heal.race-markers-and-batch-delete
+
+Topologies: H1,H2. Branches: create-marker, delete-marker, batch-version-delete, unversioned-delete, native-marker-fence.
+
+1. 分别建立版本化和非版本化桶;marker创建/删除、批量带versionId删除和无版本DELETE分开运行,不混淆接口语义。
+2. 在 xl.meta 少数缺失下启动 heal,同时提交冻结的删除列表;delete_objects 必须逐项检查 Deleted/Errors,HTTP200不代表全成功。
+3. 按逐项 ACK 计算最终数据版本、marker 和最新值集合;精确 GET验证不存在项和存留项。
+4. native-marker-fence 独立取证;恢复故障不得把已确认删除的旧 metadata 覆盖回来。
+
+Operations: [SEED_DV](validation-heal-operations.md#SEED_DV) → [SEED_DB](validation-heal-operations.md#SEED_DB) → [FAULT](validation-heal-operations.md#FAULT) → [START](validation-heal-operations.md#START) → [WORKLOAD](validation-heal-operations.md#WORKLOAD) → [S3](validation-heal-operations.md#S3) → [VERIFY](validation-heal-operations.md#VERIFY) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [NATIVE](validation-heal-operations.md#NATIVE)
+
+Source assertions:
+
+- 与创建/删除 marker、批量版本删除和无版本删除分别交错
+- 按逐项 DELETE 回执核对版本/marker 集合,无复活、误删或遗漏批量错误。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## G05: heal.race-multipart-complete-abort
+
+Topologies: H1,H2. Branches: upload-continue, complete, abort, native-mpu-fence.
+
+1. DM 同时准备已提交对象与两个可识别的 pending UploadId,各自 part/ETag 独立。
+2. 已提交对象缺片并 heal;并发给 upload-A 续传并 Complete,upload-B Abort,WINDOW 保存实际交叠。
+3. Complete 返回版本只出现一次,完整内容为按PartNumber排序的ACK part;Abort 的 UploadId 不再可list_parts,不能误伤另一个upload或已提交对象。
+4. 核对所有 MPU 列表和物理目录;原生提交/取消屏障另用 NATIVE,不允许仅发出并发请求即声称命中。
+
+Operations: [SEED_DM](validation-heal-operations.md#SEED_DM) → [FAULT](validation-heal-operations.md#FAULT) → [START](validation-heal-operations.md#START) → [WORKLOAD](validation-heal-operations.md#WORKLOAD) → [WINDOW](validation-heal-operations.md#WINDOW) → [S3](validation-heal-operations.md#S3) → [VERIFY](validation-heal-operations.md#VERIFY) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [NATIVE](validation-heal-operations.md#NATIVE)
+
+Source assertions:
+
+- heal 期间对不同已知 UploadId 分别续传、Complete、Abort
+- 成功 Complete 内容只出现一次;Abort 不破坏已提交对象或别的 upload;未完成 part 不被伪造为完整对象。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## G06: heal.race-copy-and-metadata-update
+
+Topologies: H1,H2. Branches: copy-source, copy-destination, replace-metadata, update-tags, native-copy-fence.
+
+1. DS 源/目标使用不同内容和 metadata,copy 请求 CopySource 使用结构化 Bucket/Key/VersionId,不拼路径字符串。
+2. 源或目标缺片并 heal,同窗口 COPY、MetadataDirective=REPLACE 的自拷贝、put_object_tagging 分别运行。
+3. 保存对应提交ETag/versionId及更新顺序,验证最终内容与该提交 metadata/tags 组合一致;旧 read-repair 必须重验代次。
+4. 检查源目标和control均无交叉覆盖;NATIVE 记录锁升级/陈旧提交拒绝的精确窗口。
+
+Operations: [SEED_DS](validation-heal-operations.md#SEED_DS) → [FAULT](validation-heal-operations.md#FAULT) → [START](validation-heal-operations.md#START) → [WORKLOAD](validation-heal-operations.md#WORKLOAD) → [WINDOW](validation-heal-operations.md#WINDOW) → [S3](validation-heal-operations.md#S3) → [VERIFY](validation-heal-operations.md#VERIFY) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [NATIVE](validation-heal-operations.md#NATIVE)
+
+Source assertions:
+
+- COPY 源/目标和标签/元数据更新与修复交错
+- 最终对象内容与对应提交的 metadata 一致,读修复发现新代次应重验/放弃陈旧提交而非覆盖。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## G07: heal.race-ilm-expiry-and-transition
+
+Topologies: H2. Branches: current-expiry, noncurrent-expiry, transition, restore, native-ilm-fence.
+
+Execution policy: `environment-gated`; disabled by default: `true`.
+
+Blocked reason (`ENV_DEDICATED_ILM_TIER_WINDOW`): 需要隔离 ILM/tier 租户、真实 Expire/Transition/Restore 窗口和远端版本映射;共享 ILM 环境不能安全制造交叠窗口。
+
+1. 用现有 ILM 专用远端 tier 能力建立本例桶和规则;记录真实到期窗口/远端version映射,不在共享桶上改规则。
+2. 在实际出现 Expire/Transition/Restore 进度时启动缺片 heal,WINDOW 必须从日志/状态证明交叠;提前完成只能记 NOT_OBSERVED。
+3. 过期 ACK/ILM 删除证明决定哪些版本应不存在,禁止 heal 复活;迁移对象通过真实远端 GET/restore 验证原版本内容。
+4. 本地物理与远端引用双重校验;源端 GET 成功不足,清理只删除本例规则/远端数据。
+
+Operations: [SEED_DV](validation-heal-operations.md#SEED_DV) → [SEED_DE](validation-heal-operations.md#SEED_DE) → [DEPENDENCY](validation-heal-operations.md#DEPENDENCY) → [ILM](validation-heal-operations.md#ILM) → [FAULT](validation-heal-operations.md#FAULT) → [START](validation-heal-operations.md#START) → [WINDOW](validation-heal-operations.md#WINDOW) → [S3](validation-heal-operations.md#S3) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY) → [NATIVE](validation-heal-operations.md#NATIVE)
+
+Source assertions:
+
+- 分别与当前/历史版本过期、远端 transition/restore 同窗口执行
+- 已授权过期不被恢复;迁移状态与远端版本引用正确;只有实际观察到交叠才计并发覆盖,tier 未配置需明确阻断对应实例。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## G08: heal.race-replication-resync
+
+Topologies: H2. Branches: replicate-put, replicate-delete, resync, native-replication-fence.
+
+Execution policy: `environment-gated`; disabled by default: `true`.
+
+Blocked reason (`ENV_ISOLATED_REPLICATION_PAIR`): 需要隔离的复制两端、resync 控制、跨端版本映射和 Heal/resync 实际交叠窗口;当前固定 fleet 没有独占复制对。
+
+1. 准备两个隔离复制端点/本例桶、明确源目标版本映射和同步规则;先跑一份健康复制控制。
+2. 源端缺片 heal 同时提交 PUT/DELETE 并启动指定 resync;保存两侧日志与跨端版本映射,证明操作时间交叠。
+3. 分别从两端全量列举版本、marker、内容并比对复制契约;源端正确但远端缺失/无限重试必须失败。
+4. 恢复临时规则/凭据,确认无无限循环/重复传播;精确交错仅由匹配 NATIVE 证据补充。
+
+Operations: [SEED_DV](validation-heal-operations.md#SEED_DV) → [DEPENDENCY](validation-heal-operations.md#DEPENDENCY) → [REPLICATION](validation-heal-operations.md#REPLICATION) → [FAULT](validation-heal-operations.md#FAULT) → [START](validation-heal-operations.md#START) → [WORKLOAD](validation-heal-operations.md#WORKLOAD) → [WINDOW](validation-heal-operations.md#WINDOW) → [VERIFY](validation-heal-operations.md#VERIFY) → [LOGS](validation-heal-operations.md#LOGS) → [NATIVE](validation-heal-operations.md#NATIVE)
+
+Source assertions:
+
+- 在隔离复制两端与 heal 同时运行新增/删除/重同步
+- 对端版本、删除语义及源数据最终一致,无无限回环或重复传播;不得依赖仅源端 GET 宣称复制正确。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## H01: heal.with-expansion-rebalance
+
+Topologies: H5. Branches: expand-while-data-heal, expand-while-parity-heal, versioned-rebalance.
+
+Execution policy: `environment-gated`; disabled by default: `true`.
+
+Blocked reason (`ENV_H5_EXPANSION_REBALANCE`): 需要 H5 pool 扩容、rebalance 和 Heal 同时活动的完整拓扑,并逐对象保存 pool/set 迁移证据;当前 auto-testing 未绑定扩容前后布局与专用操作窗口。
+
+1. 使用已批准 H5,先在源 pool 写 DB/DV 并以 FORMAT/PHYSICAL 证明位置;新 pool 为空且未纳入旧选择器。
+2. 目标 data/parity 缺失或bitflip后按 POOL 手册扩容重启,确认新拓扑;启动 rebalance 与 heal。
+3. WINDOW 保存两任务活动证据,逐对象跟踪源/目标 pool/set/版本身份,不能只看全局数量。
+4. 两任务均正常完成后物理证明目标权威副本,VERIFY全版本,保留scanner backlog的持久提交/退休证据。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [SEED_DV](validation-heal-operations.md#SEED_DV) → [FORMAT](validation-heal-operations.md#FORMAT) → [FAULT](validation-heal-operations.md#FAULT) → [POOL](validation-heal-operations.md#POOL) → [START](validation-heal-operations.md#START) → [WINDOW](validation-heal-operations.md#WINDOW) → [BACKGROUND](validation-heal-operations.md#BACKGROUND) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 先证明源 pool 持有夹具,再扩容并与修复同时 rebalance
+- 修复目标不得指向错误 pool/set;新旧数据完整、移动终态正确,保留 scanner backlog 提交权威检查。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## H02: heal.with-rebalance-stop-resume
+
+Topologies: H5. Branches: rebalance-stop, rebalance-resume, rebalance-service-restart.
+
+Execution policy: `environment-gated`; disabled by default: `true`.
+
+Blocked reason (`ENV_H5_REBALANCE_LIFECYCLE`): 需要可观测的 H5 rebalance、停止/恢复/服务重启控制及独立 task 身份;当前 auto-testing 未绑定这两类任务的独立身份和操作窗口。
+
+1. H5 中使用足够对象使移动可观测,缺片 heal 与 rebalance 同时活动;记录两类独立 task ID。
+2. 只停止 rebalance,观察 heal 不被误取消;然后恢复 rebalance,重启实例仅重启本例 unit。
+3. 持续唯一 key 写入并保存 ACK;停止/恢复前后物理和任务状态要连续,不用新数据根替代恢复。
+4. 最终两类任务各自满足完成契约、对象/版本及移动目标正确,重复停止不影响新代次。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [POOL](validation-heal-operations.md#POOL) → [FAULT](validation-heal-operations.md#FAULT) → [START](validation-heal-operations.md#START) → [WINDOW](validation-heal-operations.md#WINDOW) → [PROCESS](validation-heal-operations.md#PROCESS) → [WORKLOAD](validation-heal-operations.md#WORKLOAD) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- heal 活动期间 stop/restart/resume rebalance
+- 两类任务状态和身份独立;停止/恢复移动不误终止修复或丢失对象,恢复后两者均满足各自契约。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## H03: heal.with-decommission
+
+Topologies: H5. Branches: heal-decommission-source, heal-survivor-target, versioned-multipart.
+
+Execution policy: `environment-gated`; disabled by default: `true`.
+
+Blocked reason (`ENV_H5_DECOMMISSION`): 需要 H5 pool decommission、survivor 目标、写入限制和移动 ACK/物理权威证据;当前 auto-testing 未绑定本例退役前后拓扑和恢复方案。
+
+1. 先证实夹具位于待退役源,另写 survivor control;分别在源和目标制造在quorum内可恢复故障。
+2. 按既有 Pool 行为启动退役与 heal;退役期间按当前与 MinIO 一致的写入限制检查拒绝,不能把拒绝写当数据丢失。
+3. 源数据只能在目标提交权威后删除;记录移动 ACK 与 target physical,而不是临时副本数量。
+4. 退役完成后停止/移除源,只访问 survivor 全版本/MPU/内容;若还依赖源兜底必失败。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [SEED_DV](validation-heal-operations.md#SEED_DV) → [SEED_DM](validation-heal-operations.md#SEED_DM) → [POOL](validation-heal-operations.md#POOL) → [FAULT](validation-heal-operations.md#FAULT) → [START](validation-heal-operations.md#START) → [WINDOW](validation-heal-operations.md#WINDOW) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 分别在待退役源和存活目标制造可恢复缺片,再执行退役
+- 不得提前删源或以临时副本冒充迁移成功;按 Pool 既定写入限制验收;只剩 survivor 后仍完整可读且有物理证明。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## H04: heal.with-decommission-cancel-clear
+
+Topologies: H5. Branches: cancel-during-heal, clear-after-inflight-exit, resume-new-generation, capacity-lock-retry-window.
+
+Execution policy: `environment-gated`; disabled by default: `true`.
+
+Blocked reason (`ENV_H5_DECOMMISSION_CANCEL`): 需要退役取消/清理/新代次恢复以及 #2423 所需容量锁重试窗口;普通取消结果不能替代这个临界点。
+
+1. 在移动和 heal 都有部分进度时取消退役;保存取消持久化记录、通知和在途退出顺序。
+2. 确认已退出后 Clear,再恢复/新建退役;旧 token/代次不得误取消新任务,持续 ACK ledger 不能丢。
+3. 容量锁超时实例需要 LOCK_WINDOW 或匹配 NATIVE 屏障,实际见证 #2423 所涉重试路径;普通取消成功不能代替。
+4. 两流程收敛后 VERIFY/PHYSICAL;没有命中锁窗口单列 NOT_OBSERVED,不声称覆盖 #2423 的临界点。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [POOL](validation-heal-operations.md#POOL) → [FAULT](validation-heal-operations.md#FAULT) → [START](validation-heal-operations.md#START) → [WINDOW](validation-heal-operations.md#WINDOW) → [WORKLOAD](validation-heal-operations.md#WORKLOAD) → [LOCK_WINDOW](validation-heal-operations.md#LOCK_WINDOW) → [CHECKPOINT](validation-heal-operations.md#CHECKPOINT) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY) → [NATIVE](validation-heal-operations.md#NATIVE)
+
+Source assertions:
+
+- heal/移动争用中取消退役、Clear、恢复,并覆盖容量锁超时恢复
+- 保持持久化后通知及在途退出顺序;不误取消新代次;对 #2423 的重试窗口单独注明真机是否实际触发,不只看最终绿色。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## H05: heal.after-retired-pool-removal
+
+Topologies: H5. Branches: survivor-missing-part, survivor-missing-metadata, retired-source-offline.
+
+Execution policy: `environment-gated`; disabled by default: `true`.
+
+Blocked reason (`ENV_H5_RETIRED_POOL_REMOVAL`): 需要完整迁移后移除 retired pool、源保持离线和 survivor-only 访问;当前 auto-testing 未提供本例源持续离线和 survivor-only 的绑定证据。
+
+1. 依既有 Pool 全量迁移并完成退休配置移除,停止源服务,证明入口仅绑定 survivor;保留源不可达证据。
+2. 只在 survivor 的选定对象/版本制造缺片或metadata缺失;START root或正确pool/set deep。
+3. 记录 source authority 选择,不能向已退休 pool 写入,也不能无权威覆盖/错误回退(#2422)。
+4. 检查survivor实际补片、全部版本/marker和源根未变化;在源持续停机情况下全量 VERIFY。
+
+Operations: [SEED_DV](validation-heal-operations.md#SEED_DV) → [POOL](validation-heal-operations.md#POOL) → [PROCESS](validation-heal-operations.md#PROCESS) → [FAULT](validation-heal-operations.md#FAULT) → [START](validation-heal-operations.md#START) → [WAIT](validation-heal-operations.md#WAIT) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY) → [LOGS](validation-heal-operations.md#LOGS)
+
+Source assertions:
+
+- 源 pool 完全退役移除并停机后,只在 survivor 上触发修复
+- 不再向已移除源写入或依赖其兜底;来源权威和版本正确;#2422 路径不能出现无权威覆盖或错误回退。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## H06: heal.multi-pool-target-isolation
+
+Topologies: H5. Branches: selected-pool, selected-set, unselected-control-fault.
+
+1. 在两个以上 pool/set分别放不同对象,用实际layout确认归属;健康control与控制故障分开登记。
+2. 只选一个pool/set START;其他pool保留缺片控制,不先GET它们。
+3. PHYSICAL先确认选中目标补齐,未选故障原样保留;任务、容量、配额与drive身份都指向正确目标。
+4. 取证后才恢复控制故障和VERIFY;对象在其他pool可读不能证明所选盘完成修复。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [SEED_DV](validation-heal-operations.md#SEED_DV) → [FORMAT](validation-heal-operations.md#FORMAT) → [FAULT](validation-heal-operations.md#FAULT) → [START](validation-heal-operations.md#START) → [WAIT](validation-heal-operations.md#WAIT) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [RECOVER](validation-heal-operations.md#RECOVER) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 仅破坏选定 pool/set,并在另一 pool 保留不同对象与控制故障
+- 选择器、任务、容量/配额和目标 drive 一致;未选 pool 不受错误修复,跨 pool 存在不等于该目标已修复。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## H07: heal.ec84-multi-set-restart
+
+Topologies: H3,H4. Branches: ec84-one-set-restart, ec84-one-set-crash, ec84-two-sets-restart, ec84-two-sets-crash.
+
+Execution policy: `environment-gated`; disabled by default: `true`.
+
+Blocked reason (`ENV_EC84_MULTI_SET_TOPOLOGY`): 需要 H3 的 12 个独立盘和 H4 的至少 24 个独立 endpoint,并按 set 分别记录重启/崩溃窗口;当前 H4 资源不可用。
+
+1. 分别绑定 H3 12盘和 H4 至少24盘,FORMAT必须证实每set为8data+4parity;同一物理设备多个目录不能算独立故障盘。
+2. 每个set都准备DB/DV/DX并证明对象归属,REPLACE选定目标,在目标仅部分重建时优雅重启或SIGKILL。
+3. 保存实际中断窗口、盘代次、每set已修/未修对象,不能跨set合并片数。
+4. 每个set单独完成物理与全版本校验;四盘pilot不进入该项分子,native EC8+4回归作为独立补证。
+
+Operations: [SEED_DX](validation-heal-operations.md#SEED_DX) → [SEED_DV](validation-heal-operations.md#SEED_DV) → [FORMAT](validation-heal-operations.md#FORMAT) → [REPLACE](validation-heal-operations.md#REPLACE) → [WINDOW](validation-heal-operations.md#WINDOW) → [PROCESS](validation-heal-operations.md#PROCESS) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY) → [NATIVE](validation-heal-operations.md#NATIVE)
+
+Source assertions:
+
+- 固定 EC8+4 单 set 与至少两 set,部分重建时重启/崩溃目标
+- 每个 set 的故障覆盖和重建分别证明,不跨 set 拼 shard;必须满足真实盘数及映射,不能用 4 盘用例替代 EC8+4。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## I01: heal.slow-io-and-eio
+
+Topologies: H1,H2. Branches: bounded-latency, short-read, eio.
+
+Execution policy: `environment-gated`; disabled by default: `true`.
+
+Blocked reason (`ENV_DEDICATED_FAULT_IO`): 需要专用 faultfs 或故障设备、可恢复的 slow/short-read/EIO 注入和 namespace 隔离;共享盘模拟会污染其他运行。
+
+1. IO_FAULT 只在预分配专用故障设备或受限faultfs,冻结延迟/错误率、IO范围、最长窗口和原状恢复命令。
+2. 在服务相同namespace/UID先证明迟延/短读/EIO真实生效,再启动目标缺片heal;不得让dd填满共享盘。
+3. 错误窗口记录重试、CPU、队列和物理片,不能提前reported healthy;暂态与永久错误分开记录。
+4. 撤销故障后重新观察责任完成并VERIFY;故障层证据缺失算INFRA_ERROR,不把模拟EIO说成真实介质损坏。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [SEED_DX](validation-heal-operations.md#SEED_DX) → [IO_FAULT](validation-heal-operations.md#IO_FAULT) → [FAULT](validation-heal-operations.md#FAULT) → [START](validation-heal-operations.md#START) → [BACKGROUND](validation-heal-operations.md#BACKGROUND) → [METRICS](validation-heal-operations.md#METRICS) → [RECOVER](validation-heal-operations.md#RECOVER) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 在独占故障层注入有界延迟、短读、EIO,解除后观察恢复
+- 暂态与永久错误分开,重试有界且无忙循环;解除故障后实际修复,期间不得提前标健康。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## I02: heal.enospc-inodes-and-quota
+
+Topologies: H1,H2. Branches: bounded-volume-enospc, bounded-inode-exhaustion, bucket-quota.
+
+1. IO_FAULT使用有容量上限的独立loop/fault卷,预注册字节/inode限制及本例桶配额,不改变共享XFS全局配额。
+2. 先证明服务实际写返回ENOSPC或quota错误;准备缺片目标和DM写入,再触发heal/PUT,逐次保留ACK和错误。
+3. 不可写期间不得损坏健康来源、虚报补片或释放仍需容量;元数据/part双重快照。
+4. 释放本例占用或提高已批准限额后续修,VERIFY全内容和目标片;共享盘可用空间保持安全余量。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [SEED_DM](validation-heal-operations.md#SEED_DM) → [IO_FAULT](validation-heal-operations.md#IO_FAULT) → [QUOTA](validation-heal-operations.md#QUOTA) → [FAULT](validation-heal-operations.md#FAULT) → [START](validation-heal-operations.md#START) → [WORKLOAD](validation-heal-operations.md#WORKLOAD) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [RECOVER](validation-heal-operations.md#RECOVER) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 限制专用测试卷空间/inode,并参数化本次桶配额
+- 仅填专用受限卷、不耗尽共享真实盘;失败不破坏好副本或释放尚需容量,释放空间后正确续修。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## I03: heal.network-partition-and-lock-quorum
+
+Topologies: H2. Branches: minority-partition, read-boundary, write-boundary, lock-boundary, rejoin.
+
+候选支持与 B06 相同,但 storageclass 与锁策略分别按精确候选绑定。`1c7fc81` 的锁客户端范围、quorum 阈值及 EC:8 配置已复核;其 RPC 控制通道工厂有变化,必须重新取得真实双向网络探针、admission、partial coverage 和 rejoin 证据,不能由源码等价或 B07 通过推定本例通过。B08 的候选范围保持独立。
+
+The adapter additionally binds exact source `62ff96774fa4139d45f14d70b70519aae8070ac0` and its typed RPC `Unavailable` response mapping to `503/ServiceUnavailable`. Every referenced lock, EC, multipart and delete-recovery source file remains independently checked against that committed source; neighboring or abbreviated revisions are rejected. This source requires its matching immutable package and source-bound inspector before live execution. All five original branches, complete DB/DV/DL fixtures, refused-mutation accounting, physical-before-GET proofs, task responsibility and cleanup requirements remain mandatory; the source admission alone does not establish live acceptance.
+
+1. H2 先冻结实际节点/slot/对象R/W和锁quorum,NETWORK规则只匹配本例服务端口并带run ID、超时恢复。
+2. 构造少数派及边界分区,双端探针证实隔离;SSH、跳板机和其他服务端口保持连通。
+3. 双方执行带nonce的PUT/GET/heal,少数派不能承诺不安全提交;BACKGROUND必须暴露missing peer/partial coverage。
+4. 解除规则并检查规则计数归零,按ACK因果对账全版本/内容及目标片,无分叉、旧代覆盖或丢失责任。
+
+Operations: [SEED_DV](validation-heal-operations.md#SEED_DV) → [FORMAT](validation-heal-operations.md#FORMAT) → [NETWORK](validation-heal-operations.md#NETWORK) → [WORKLOAD](validation-heal-operations.md#WORKLOAD) → [START](validation-heal-operations.md#START) → [BACKGROUND](validation-heal-operations.md#BACKGROUND) → [METRICS](validation-heal-operations.md#METRICS) → [RECOVER](validation-heal-operations.md#RECOVER) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 仅隔离测试服务节点间流量,覆盖少数派、读/写/锁 quorum 边界及恢复
+- 少数派不得承诺无安全性的修复或写入;cluster coverage 不完整不能显示全局健康;恢复无分叉、跨代覆盖。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## I04: heal.queue-pressure-and-admission
+
+Topologies: H1,H2. Branches: full-queue, dropped-admission, merged-duplicates, cross-bucket-fairness.
+
+1. 专用服务冻结队列容量、并发、最大请求数和时限;DX与多桶夹具通过WORKLOAD施加有上限的Admin/read-repair请求。
+2. 分别记录Full/Dropped/Merged的真实返回及任务ID,重复token/合并不按独立修复数计分。
+3. 持久责任不能因入队失败被退休;记录journal、队列深度、等待时延和资源曲线,超过预算立即停注入。
+4. 停止负载后在deadline内收敛,所有可修目标真实补齐;不能只以CLI exit0或全局idle通过。
+
+Operations: [SEED_DX](validation-heal-operations.md#SEED_DX) → [CONFIG](validation-heal-operations.md#CONFIG) → [WORKLOAD](validation-heal-operations.md#WORKLOAD) → [START](validation-heal-operations.md#START) → [BACKGROUND](validation-heal-operations.md#BACKGROUND) → [CHECKPOINT](validation-heal-operations.md#CHECKPOINT) → [METRICS](validation-heal-operations.md#METRICS) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 多来源请求冲击有限队列,混合重复任务、不同优先级和多个桶
+- Full/Dropped/Merged 语义与实际任务一致;丢弃不能丢 durable 责任;在预先声明负载窗口内无无限堆积或健康误报。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## I05: heal.lock-timeout-and-caller-abort
+
+Topologies: H1,H2. Branches: object-lock-timeout, caller-disconnect, stale-read-repair, native-lock-owner-fence.
+
+Execution policy: `evidence-blocked`; disabled by default: `false`.
+
+Blocked reason (`NATIVE_LOCK_OWNER_ABORT_BARRIER`): 已有同候选源码的 lock owner、取消物理 unlink 与 stale read-repair 原生子集;H1/H2 timeout 与 queued-successor 诊断均已通过独立 live 审计(39767336741/1、39817606025/1),包括原始 ACK、历史代次、真实锁事件、各 peer inventory、停机后新分片完整性和有界 FD 收尾。仍缺实际断连边界的精确所有权、完整 native timeout/stale 屏障、stale-generation controller,以及 H1/H2 DL/DV 完整对象/ACK/物理/锁/FD 闭环。精确源码 cfg(test) 原生证据独立保留;诊断子集和原生子集不能充作完整 canonical PASS。
+
+1. LOCK_WINDOW用本例慢IO/受控原生锁屏障持有目标对象锁,先证明真实争用超过预注册预算。
+2. 同时heal/覆盖或删除;调用方断开实例仅断本次TCP连接,不杀服务器或关闭跳板机;记录请求开始/断开/服务在途结束。
+3. 锁释放后观察取消/超时不提前释放所有权、不重复提交,陈旧read-repair必须重验版本。
+4. 核对所有ACK和最终代次,检查锁/FD无泄漏;黑盒不能证明锁所有权时以NATIVE窗口独立补证,不能猜命中。
+
+Operations: [SEED_DV](validation-heal-operations.md#SEED_DV) → [IO_FAULT](validation-heal-operations.md#IO_FAULT) → [LOCK_WINDOW](validation-heal-operations.md#LOCK_WINDOW) → [WORKLOAD](validation-heal-operations.md#WORKLOAD) → [START](validation-heal-operations.md#START) → [WINDOW](validation-heal-operations.md#WINDOW) → [METRICS](validation-heal-operations.md#METRICS) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY) → [NATIVE](validation-heal-operations.md#NATIVE)
+
+Source assertions:
+
+- 真实对象锁争用超过预算及调用方断开,再释放锁恢复
+- 取消/超时不得提前释放在途所有权或重复提交;陈旧 read-repair 必须重验代次;恢复后无泄漏和错误覆盖。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## I06: heal.kms-and-tier-outage
+
+Topologies: H2. Branches: kms-unavailable, kms-denied, tier-unavailable, tier-version-unavailable.
+
+Execution policy: `environment-gated`; disabled by default: `true`.
+
+Blocked reason (`ENV_DEDICATED_KMS_TIER_TENANT`): 需要专用 KMS/tier 租户、依赖网络隔离和安全的 key/tier 版本证据;不能在共享依赖上全局断网。
+
+1. 先在专用KMS/tier租户验证DE/DV夹具可正常解密/restore,保存非敏感key/tier版本标识。
+2. DEPENDENCY仅断本测试客户端/服务到该依赖的专用namespace路由,或撤销本例租户权限;不全局停共享KMS。
+3. 发GET/restore/deep heal并观察责任,依赖错误不能判对象不存在、可remove、已修好;密钥不进入错误日志。
+4. 恢复依赖和权限后原版本可解密/恢复,物理/责任完整;远端对象不存在与远端暂不可达分别判定。
+
+Operations: [SEED_DE](validation-heal-operations.md#SEED_DE) → [SEED_DV](validation-heal-operations.md#SEED_DV) → [DEPENDENCY](validation-heal-operations.md#DEPENDENCY) → [NETWORK](validation-heal-operations.md#NETWORK) → [S3](validation-heal-operations.md#S3) → [START](validation-heal-operations.md#START) → [BACKGROUND](validation-heal-operations.md#BACKGROUND) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 仅让已分配测试 KMS/远端 tier 暂不可用,持续查询与修复
+- 依赖故障不得解释为对象不存在/可删/已修复,不暴露密钥;恢复后原版本可解密或恢复,责任未丢失。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## J01: heal.status-partial-disabled-unknown
+
+Topologies: H1,H2. Branches: enabled-idle, disabled, uninitialized, missing-peer, recovered-complete.
+
+1. 在独立服务配置分别观察 enabled无任务、HEAL禁用、初始化未完成,保留实际启动配置及时间线。
+2. H2 PROCESS停一个peer并跨节点查询BACKGROUND/REPLACEMENT;完整coverage必须含expected/responded/unknown明细。
+3. disabled/uninitialized/partial/degraded不能被rc或聚合器解释成全局完成;收集rc原输出与raw Admin逐字段对照。
+4. 恢复所有peer后才允许完整idle,依然用PHYSICAL/manifest核对无遗留责任;未观测初始化窗口单独记NOT_OBSERVED。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [CONFIG](validation-heal-operations.md#CONFIG) → [PROCESS](validation-heal-operations.md#PROCESS) → [BACKGROUND](validation-heal-operations.md#BACKGROUND) → [REPLACEMENT](validation-heal-operations.md#REPLACEMENT) → [RC_STATUS](validation-heal-operations.md#RC_STATUS) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 分别检查 enabled/disabled/uninitialized、全体可达及缺失 peer 的状态
+- idle 只在覆盖完整且真实无活动时成立;partial/unknown/degraded/disabled 不被客户端转换为全部完成。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## J02: heal.outcome-counters-and-error-truth
+
+Topologies: H1,H2. Branches: mixed-healthy-repaired-failed, dry-run, deferred, readonly-terminal-failure, unknown-negative.
+
+Execution policy: `evidence-blocked`; disabled by default: `false`.
+
+Blocked reason (`EVIDENCE_MIXED_OUTCOME_ORCHESTRATION`): 已有 mixed healthy/repaired/failed、独立 dry-run、依赖 deferral、真实 SDK legacy Unknown、原始全副本损坏元数据 Unknown、只读永久失败/新 token 补偿及版本级 grace Deferred/新 token 清理执行流程,均按逐对象 manifest/attemptFailures 对账;仍需完整串行 live 证据及独立验收,故保持禁止默认 dispatch 和 canonical admission;本地检查不能作为完整 PASS。
+
+1. 同批独立key建立健康、可恢复缺片、低于R的不可恢复对象;deferred由冻结的锁/依赖故障产生,dry-run另建任务。
+2. 保存逐对象物理基线和期望disposition,对结果的execution/coverage/counters/object identities做逐项对账。
+3. unknown/skipped/completed_with_errors不算修复成功,legacy objectsHealed与canonical healed分开解释;attemptFailures不得被隐藏。
+4. 使用独立完整manifest弥补有界outcome窗口,保持不可修对象的正确失败记录;恢复后另任务修复不覆盖原结果。
+5. grace 分支先用 SDK PUT 建立两个真实版本并验证完整分片;保存一个成员的原对象树后,SDK DELETE 精确历史版本,逐盘验证仅当前版本存在且其逻辑元数据、数据片及控制对象未变。只向原成员回放原对象树;实际候选进程固定 grace=120 秒,不修改元数据年龄或时钟。原任务须在窗口内完成,精确报告旧版 deferred、当前版 verified_healthy 及 retryNotBefore;自然等待过期后原 token 仍不变,新 token 才能清理旧版。清理后先核对全部物理字节等于 DELETE 后权威现场,再跨节点 GET/404 和复查旧 token,最后才做 fixture finalization。错过窗口保留 NOT_OBSERVED;异常保留现场并标 DIRTY;救援仅恢复 DELETE 后权威树,不复活已删除版本。
+6. Manual diagnostic entry: explicitly select heal.outcome-counters-and-error-truth through Functional Chain with the frozen candidate and candidate-bound manager/inspector tools. The default catalog policy stays evidence-blocked; even all passing diagnostic branches retain complete=false, canonicalCasePassClaimed=false and a blocking gate verdict until independent acceptance.
+7. For a pre-replay public-manifest refusal, recover only after the complete SDK request/ACK ledger proves all writes acknowledged and no DELETE or replay occurred. Bind all four original physical members and the reserved owner, stop the exact candidate, then preserve all roots and state using durable same-filesystem non-overwriting rename. Partial archive retries require the same durable inventory; missing roots or drift retain DIRTY. Isolation is not product repair or restoration and never rewrites the failed case.
+8. Manual J02 candidate admission is limited to the original 072b57b37559fbe626aeda8abf61617fdcbdd926 and corrected 1c7fc818dd858247c45b39542abd8291b1e6e31b sources. Each execution requires the matching immutable package and independently audited inspector/manager bundle. A corrected-candidate run is new evidence; it cannot replace an original failure. The full exact-version, physical, timing, retry and responsibility oracles remain unchanged, and all passing diagnostics still require independent canonical acceptance.
+9. The deferred source branch follows Batch.deferred in frozen validation script 524eb5b32c3236497fdd8a9271df0494fa3d9974: complete the exact-version grace, natural-expiry and new-token cleanup procedure, then finalize this branch. An additional R-1 metadata existence-check diagnostic is not part of that source procedure and does not gate its verdict. Its historical failures and strict replay assertions remain retained; original source failures, missed-window NOT_OBSERVED and uncertain-mutation DIRTY are unchanged.
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [SEED_DV](validation-heal-operations.md#SEED_DV) → [FAULT](validation-heal-operations.md#FAULT) → [LOCK_WINDOW](validation-heal-operations.md#LOCK_WINDOW) → [START](validation-heal-operations.md#START) → [WAIT](validation-heal-operations.md#WAIT) → [BACKGROUND](validation-heal-operations.md#BACKGROUND) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [RECOVER](validation-heal-operations.md#RECOVER) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 同批次混合健康、可修、不可修、deferred、dry-run 对象
+- execution/coverage/disposition/计数和逐对象物理结果相符;unknown、skipped 或 completed_with_errors 不能算修复成功。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## J03: heal.incremental-cursor-and-truncation
+
+Topologies: H1,H2. Branches: within-window-cursor, past-retention-gap, cross-node-cursor, terminal-truncation.
+
+1. DX至少257个目标,超过128诊断窗口;边启动recursive heal边以sinceSeq查询,每次请求/nextSeq/minSeq/items全部保存。
+2. 单端连续读取应无重复倒退;故意使用过旧cursor应明示truncated/gap;跨节点查询按任务/epoch而非全局数字比较。
+3. 任务终态也读取旧cursor,验证有界列表不被当完整清单;所有错误与缺口原样进入报告。
+4. 最终用全量独立S3/physical manifest与canonical counters验收覆盖;若任务太快仍可做终态截断,但不能声称观察了运行中游标。
+
+Operations: [SEED_DX](validation-heal-operations.md#SEED_DX) → [FAULT](validation-heal-operations.md#FAULT) → [START](validation-heal-operations.md#START) → [CURSOR](validation-heal-operations.md#CURSOR) → [STATUS](validation-heal-operations.md#STATUS) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 生成超过结果保留上限的任务,分页轮询 sinceSeq,并跨节点/终态读取
+- nextSeq/minSeq 单调且 gap/truncation 明示;有界 items 不是完整责任清单,最终须用独立 manifest/计数补证,不静默丢错误。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## J04: heal.repeat-idempotency
+
+Topologies: H1,H2. Branches: normal-repeat, deep-repeat, repeat-after-restart, versioned-repeat.
+
+1. DB/DV缺片先修复;versioned 的整份 xl.meta 缺失允许所有 identity repaired,或至少一个 repaired 加其余正健康态;marker 只能为 repaired/metadata_healthy,counters.healed 与 repaired 数量一致、counters.unchanged 与其余数量一致,unknown/skipped/failed=0。完整保存 post-heal S3/physical 作为第二轮基线,不使用最初故障前 xl.meta 要求内部字节完全相同。
+2. 再次 normal/deep:normal 只接受 metadata_healthy,deep 才要求 verified_healthy;两者均 healed=0、unknown/skipped/failed=0,不得新增 repaired,所有原版本/marker 身份与数量一致。
+3. PROCESS原unit重启后再重复;检查新任务有独立token但不产生新对象版本、无限责任或重复修复计数。
+4. 允许经格式证据证明的内部维护记录变化,用户内容/引用不变,目标片摘要稳定。
+
+Operations: [SEED_DB](validation-heal-operations.md#SEED_DB) → [SEED_DV](validation-heal-operations.md#SEED_DV) → [FAULT](validation-heal-operations.md#FAULT) → [START](validation-heal-operations.md#START) → [WAIT](validation-heal-operations.md#WAIT) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY) → [PROCESS](validation-heal-operations.md#PROCESS)
+
+Source assertions:
+
+- 第一次修复后重复 normal/deep、重启再检查
+- versioned 首次允许所有 identity repaired 或至少一个 repaired 加其余正健康态,marker 仅 repaired/metadata_healthy,计数必须与 receipt 集合一致;normal repeat 接受 `metadata_healthy`,deep repeat 要求 `verified_healthy`,repeat marker 为 `metadata_healthy` 且 healed=0;unknown/skipped/failed 不得成为 PASS。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## J05: heal.long-soak
+
+Topologies: H2,H5. Branches: 24h, 72h.
+
+Execution policy: `environment-gated`; disabled by default: `true`.
+
+Blocked reason (`ENV_DEDICATED_LONG_SOAK_LANE`): 必须在独占 H2/H5 lane 真实运行 24 小时和 72 小时,包含限速、监控、到期回收和完整资源预算;短跑不能替代 soak 证据。
+
+1. 只在专门分配的H2/H5独占lane,冻结完整24h或72h墙钟窗口、循环故障日程、并发/限速/存储上限和到期回收安排。
+2. WORKLOAD持续记录逐请求ACK,周期性故障仅使用本run DRIVE_GATE/PROCESS/依赖规则;每轮见证与恢复,不无限加数据。
+3. 持续METRICS、日志、队列/MRF/checkpoint取样;资源增长、panic、未解释责任达到预注册阈值立即保留失败证据。
+4. 到期停止写入/注入,解除全部故障,解析未知ACK后全量VERIFY和PHYSICAL;短跑不能充24h/72h结果。
+
+Operations: [SEED_DX](validation-heal-operations.md#SEED_DX) → [SEED_DV](validation-heal-operations.md#SEED_DV) → [WORKLOAD](validation-heal-operations.md#WORKLOAD) → [DRIVE_GATE](validation-heal-operations.md#DRIVE_GATE) → [PROCESS](validation-heal-operations.md#PROCESS) → [BACKGROUND](validation-heal-operations.md#BACKGROUND) → [CHECKPOINT](validation-heal-operations.md#CHECKPOINT) → [METRICS](validation-heal-operations.md#METRICS) → [LOGS](validation-heal-operations.md#LOGS) → [RECOVER](validation-heal-operations.md#RECOVER) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 独占环境运行预注册 24h/72h 窗口,混合修复、掉线、写入和版本删除
+- 每次 ACK 可追踪;最终全量 manifest 正确,队列/内存/FD/临时空间无无界增长,无后台 panic 或未解释持久责任。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
+
+
+
+## J06: heal.performance-abba
+
+Topologies: H2. Branches: abba-repeat-1, abba-repeat-2, abba-repeat-3.
+
+1. 同硬件/候选/rc预注册预热、每轮时长、并发、读写比例、数据量/缓存策略、P95/P99与错误率阈值;禁止结果出来再定门槛。
+2. 按A无heal、B有heal、B有heal、A无heal顺序,每轮从相同逻辑内容和队列基线开始;完成至少三组独立ABBA。
+3. WORKLOAD记录每请求延迟/成功/错误/超时,METRICS记录CPU/IO/FD及实际修复字节;错误不从总请求分母删除。
+4. 报告每轮及配对差异、修复吞吐和GET/PUT各自P95/P99;功能VERIFY必须通过,性能阈值也必须独立满足。
+
+Operations: [SEED_DX](validation-heal-operations.md#SEED_DX) → [PERFORMANCE](validation-heal-operations.md#PERFORMANCE) → [WORKLOAD](validation-heal-operations.md#WORKLOAD) → [FAULT](validation-heal-operations.md#FAULT) → [START](validation-heal-operations.md#START) → [METRICS](validation-heal-operations.md#METRICS) → [LOGS](validation-heal-operations.md#LOGS) → [PHYSICAL](validation-heal-operations.md#PHYSICAL) → [VERIFY](validation-heal-operations.md#VERIFY)
+
+Source assertions:
+
+- 同硬件同候选按 ABBA 运行无 heal 基线和有 heal 负载,重复并固定限速
+- 分别报告修复吞吐、GET/PUT P95/P99、错误率和 CPU/IO;阈值事先冻结,不能事后挑轮次或把功能 PASS 充性能验收。
+
+先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。
+
+逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。
diff --git a/docs/validation-heal-contract.md b/docs/validation-heal-contract.md
new file mode 100644
index 00000000..63b216c7
--- /dev/null
+++ b/docs/validation-heal-contract.md
@@ -0,0 +1,309 @@
+# Heal 真机验证设计与验收契约
+
+## 状态与范围
+
+本规范保留源仓库的 84 个 Heal canonical 用例、302 个操作分支及 534 个适用拓扑分支模板。资源要求、公共通过判据和逐项附加断言必须一起满足。
+
+入口为 `scripts/validation_heal/matrix.py`、[逐例操作卡](validation-heal-cases.md) 和 [执行手册](validation-heal-operations.md)。运行环境是当前 auto-testing 的 Heal 节点;候选及 rc 使用当前 validation lane 的不可变包和实际二进制摘要。源仓库历史 pilot 结果不能计入本次通过数量。
+
+全部 84 个 Heal ID 已有调度路由,包括 67 个常规入口、13 个默认关闭的环境场景入口,以及 C11/F07/I05/J02 的四个诊断入口。测试实现完整性与产品 live 结果独立统计:C11/F07/I05/J02 的源分支已有执行流程;F07 组合相同候选的 manager 中断与目录 fsync 证据,I05 总控串行执行 H1/H2 超时和断连控制器,并独立绑定完整内容/物理代次及 owner 原生证明,见[实现审查](validation-migration-implementation-review.md)。四个诊断入口的 canonical 验收门禁继续关闭;注册入口或 native 子集通过均不能充作完整 PASS。A09/G01–G06 的精确 native 与候选准入按各自执行器核对,G06 已有完整独立 live 验收。产品 FAIL 不自动等于测试未迁移。分片执行器核对实际盘角色、停止服务后的文件故障、原始同角色分片摘要与逐块 HighwayHash。A03 通过原路径/token 比较 H2 协调者、独立 peer 和固定节点入口的任务身份、终态及完整 outcome;候选每响应生成的 startTime 不作为任务 epoch。A04 保留四个 DB 参数组合及两个 DV 分支,要求完整 dry_run_observed 集合且对象没有变化。A05 使用独立部署执行全部八个 H1/H2 分支,冻结实际 merge 配置、真实非零 running 窗口、精确冲突与 token 策略及旧/新任务责任,完成后在服务运行状态下证明所有 peer 退出和物理静默。A06/F06 证明取消后的物理静默、旧 token 隔离;F06 还保留取消任务原始持久化记录,重启后不得复活或覆盖新任务。A08 保留五种主体各自 start/status/stop 操作、真实过期签名、仅授权 admin:Heal 的临时用户及清理后凭据失效。B09 使用明确启用 MRF 的专用服务,记录节点离线时的降级 ACK,并先验逐盘补片再执行目标 S3 请求。J03 必须实际超过 1024 项保留窗口,截断的诊断清单仍需独立全量物理/S3 补证。13 个环境场景保留源操作分支、未满足条件和显式启用入口;其完整 live 验证仍需资源适配器与源断言审核。每项实际状态见 [迁移映射](validation-migration-map.md);本地检查、操作卡和归档不能代替 live PASS。
+
+B03/B04 replacement runner 先执行只读 mount admission preflight,再允许停止服务、下载/安装候选包或创建远端 run namespace;阻断结果明确记录 `execution_stage=pre-deployment`、候选/rc 来源和 `binary_sha256=null`,表示尚未观察到安装候选二进制。通过预检后,部署阶段仍会在 reservation 前再次读取同一组 mount/设备身份,避免预检与实际变更之间的环境漂移。
+
+H07 的单 set 两个分支只属于 H3,双 set 两个分支只属于 H4;四个不匹配组合记为 `nonApplicableCombinations`。缺少机器属于资源阻塞。
+
+目标不是只证明“坏盘后仍能 GET”,而是分别证明:
+
+1. 故障真实发生在指定对象、版本、set 和目标盘。
+2. 客户可见控制接口及期望触发源处理了这份修复责任。
+3. 可恢复时确实修好了目标分片;不可恢复时明确失败且不扩大损坏。
+4. 对象、历史版本、删除语义、并发成功写入和持久化责任正确。
+5. 原始证据完整,临时故障被解除,环境可安全交还。
+
+## 版本与已核实的产品边界
+
+接口参考来自源设计记录的 RustFS 与 rc 提交,保留下面的源码链接用于追溯。每批执行仍以当前 candidate manifest 的精确源码、包、已安装二进制摘要和 auto-testing/rc 摘要为准,不由历史版本的验证结果推断本次候选已通过。
+
+| 已核实的接口/行为 | 对设计的影响 |
+| --- | --- |
+| 签名 Admin POST `/rustfs/admin/v3/heal/`、`/{bucket}`、`/{bucket}/{prefix}`,token 查询与 forceStop/forceStart | 启动回执仅表示受理;必须查询任务并结合内容与物理证据验收。 |
+| POST `/rustfs/admin/v3/background-heal/status`,GET `/rustfs/admin/v4/heal/replacement-recovery` | 后台状态、完整 peer coverage、replacement readiness 与具体任务终态分开判断。 |
+| outcome 含 execution、coverage、counters、逐对象 disposition;objects/items 是有界诊断窗口 | finished、队列为 0、HTTP 200 都不是“所有对象已修好”的充分条件。 |
+| query 支持 sinceSeq;结果含 nextSeq/minSeq/truncated | 需要游标、截断和缺口负例;被截断的 items 不能用作完整清单。 |
+| Admin 保留 nolock wire 字段,但实际强制走正常对象锁路径 | A09 验证其不能绕过锁;不沿用历史架构文档中允许 Admin 绕锁的旧描述。 |
+| root heal 优雅重启恢复限于原协调者、原拓扑且记录盘仍可读 | 不预设跨协调者故障转移、记录盘丢失恢复或任意降级兼容;MRF/replacement/root 各按各自责任契约验收。 |
+| 参考 rc 提供 heal start/status/stop 及 bucket/prefix/token、normal/deep、dry-run、remove/recreate | pool/set、forceStart、sinceSeq 等未在该 CLI 版本暴露的功能通过签名 Admin 测,不虚构 rc 参数;新批次先检查实际 help/schema。 |
+
+源码:[Admin heal](https://github.com/rustfs/rustfs/blob/940e221988694da33fe005054bb86086bae88cd6/rustfs/src/admin/handlers/heal.rs)、[HealOpts](https://github.com/rustfs/rustfs/blob/940e221988694da33fe005054bb86086bae88cd6/crates/heal-contracts/src/heal_channel.rs)、[outcome/责任回执](https://github.com/rustfs/rustfs/blob/940e221988694da33fe005054bb86086bae88cd6/crates/heal/src/heal/outcome.rs)、[rc 命令](https://github.com/rustfs/cli/blob/204eb5ad85b46a007afd4da538cbeb1cadd867b6/crates/cli/src/commands/admin/heal.rs)。
+
+## 拓扑与故障域
+
+下表是**需求模型,不是已经分配的可用机器清单**。所有真机动作必须经跳板机和固定 fleet lease,不能私自创建/删除 Azure VM、磁盘或网络。已有 example profile 不是运行许可;实现时必须绑定节点、端口、服务、mount UUID、盘数、pool/set/slot 与独占范围,读回实际 EC 元数据确认布局。
+
+| 编号 | 最低形状/用途 | 当前边界 |
+| --- | --- | --- |
+| H1 | 单节点四独立盘、一个 EC set;基础盘修复 | 可参考 single-4disk;使用独立 heal 服务和每盘 heal-validation 根。需要先绑定空闲资源,不复用现有 Pool 数据目录。 |
+| H2 | 四节点各四盘、同一分布式 pool,明确固定 set 划分;跨节点故障/锁/状态 | 可参考 cluster-1pool;必须证明目标 set 跨节点,不能只看全局节点总数。 |
+| H3 | 精确 EC8+4 单 set,参考主仓库的 3 节点×4 盘测试形状 | 是测试参数,不是生产部署推荐;旧两节点 Pool 环境不满足此拓扑。 |
+| H4 | 精确 EC8+4 至少两个 set,参考 3 节点×8 盘、同 pool | 至少 24 个独立盘端点及正确 set 映射;不得用 4 盘或一个 set 替代。固定资源未分配时为阻塞,不是通过。 |
+| H5 | 两个以上 pool;分开登记两节点各自四盘的生命周期形状和跨节点 pool 形状 | 现 lifecycle-a 每台一个独立 pool,无节点级冗余;可验证盘级修复及移动交互,不能证明节点故障容错。对齐主仓库多 pool EC8+4 基准时,另登记其三单机×12 盘/三个 pool 布局,不混用计数。 |
+| H6 | 独占 VM/主机与专用故障盘,可做强制重启 | 需要额外运维授权、带外恢复和事件证据;普通故障 lane 不允许自动进入。 |
+
+生产拓扑取舍可参考 [RustFS Linux 部署说明](https://docs.rustfs.com/en/installation/linux):单机多盘的盘冗余不等于整机故障冗余。本设计的结论只对实际验证的拓扑成立。
+
+每个对象/版本预先记录 N、data/parity、分片索引与 set/盘映射,再从候选实现与元数据确定读取 quorum R、修复写 quorum W 和锁 quorum。测试 R-1/R/R+1 与 W-1/W/W+1 的**合法组合**,不以全局在线盘数或固定“坏一半仍能写”代替计算。跨版本、跨 set 的片不能相加;R 与 W 不一定相同。N=1 无 EC 的能力/不可恢复负例属于额外参数实例,不得宣传可自动找回完全丢失的数据。
+
+## 故障后端与安全门禁
+
+所有注入必须有 machine-readable fault-plan 和 fault-receipt;不存在“命令成功即注入成功”。默认每次只隔离本用例一个已解析目标,再按预注册组合扩展。使用以下分类,报告必须记录实际 faultBackend,不能互相冒充。
+
+| 编号 | 故障类别 | 注入有效性与限制 |
+| --- | --- | --- |
+| F0 | 无故障、纯接口请求 | 记录稳定基线;仅用于无副作用/负请求验证。 |
+| F1 | 指定 part/shard 缺失 | 先校验精确路径和摘要,以同盘隔离备份/原子移位保存,再证明目标缺失;不是物理掉盘。 |
+| F2 | 指定字节翻转、截断、校验头损坏 | 在隔离测试对象副本上操作,保存偏移、长度、前后摘要及原始备份;必须有足够未损坏权威。 |
+| F3 | 指定 xl.meta/inline/身份/桶元数据缺失或损坏 | 需要与候选格式匹配的只读解析器和允许列表;不得操作 IAM/凭据配置、共享 format 或无关对象。 |
+| F4 | 原逻辑盘/服务卷短暂不可达、只读 | 采用仅影响测试服务的受限挂载命名空间或故障层,并实际验证访问失败。不得卸载含其他服务数据的共享 XFS mount;chmod 不能未经验证当作有效故障。 |
+| F5 | 慢 I/O、EIO、短读、ENOSPC/inode 故障 | 只在预分配独占测试设备或有容量上限的故障层;不得塞满共享数据盘。明确模拟故障与真实设备故障的区别。 |
+| F6 | 测试进程优雅停止、SIGKILL、重启 | 限定已验证服务和 MainPID,证明在部分进度窗口发生;记录 boot/process 身份。SIGKILL 不是断电。 |
+| F7 | 节点间测试端口网络故障 | 仅对本服务地址/端口、受控 namespace 或独占环境,保护 SSH/跳板机/管理平面;规则带唯一 ID、超时和幂等恢复。 |
+| F8 | 空替换逻辑盘、新 disk UUID | 验证原/新盘和 pool/set/slot;不把移动回备份当重建。真实设备 detach/attach 另需独占资源授权,不修改共享盘。 |
+| F9 | checkpoint/commit/锁窗口的确定性故障 | 优先原生 runtime-fence/E2E 屏障;Release 黑盒无法证明精确窗口时,单独列原生证据与真机覆盖缺口,禁止给优化包塞入隐藏测试 hook 冒充同一候选。 |
+| F10 | 独占 VM/主机强制重启 | 单独审批的破坏性 lane;有带外恢复和平台时间线。VM forced stop 不证明物理控制器缓存断电语义。 |
+| F11 | 队列/请求压力、测试 KMS/tier/配额依赖故障 | 只作用于已分配测试租户/进程,限量限时,禁止全局断开共享 KMS 或远端服务。 |
+
+fault-plan 至少包含 run/case/variant ID、拓扑指纹、对象/版本/incarnation、pool/set/slot、盘 UUID、精确文件允许列表、设备/根目录身份、注入前条件、变更预算、备份摘要、恢复动作和自动解除故障截止时间。路径按目录描述符解析,拒绝链接逃逸、设备漂移、未知文件和自由 shell 插值。
+
+故障解除与数据清理是两件事:网络/只读/进程等临时环境损伤要安全解除;失败数据、分片、checkpoint 和证据应保留,环境标记 DIRTY/QUARANTINED,不能继续下一例。捕获证据后可按已批准流程恢复业务可达性,但不覆盖失败记录或把恢复重跑算首次通过。备份只可用于失败后的救援/重置,不参与修复成功判定。
+
+## 数据夹具与参数维度
+
+| 编号 | 内容 |
+| --- | --- |
+| DB | 确定性基础 manifest:默认 128 个对象,包含 0/1、4095/4096/4097 字节以及实际 inline、EC stripe/block、multipart 边界±1;按磁盘证据确认形态,不硬编码内部阈值。 |
+| DM | 已完成的多 part 对象、非等长 part、跨 part Range;另外创建可追踪 UploadId 的 pending/Complete/Abort 夹具,遵守 S3 最小非末 part 大小限制。 |
+| DV | 每 key 至少三个不同内容版本、当前/历史 delete marker、versioning suspended/null、明确删除回执;记录 versionId、Last-Modified、ETag 和版本列表。 |
+| DS | 多桶、相邻前缀、多页 key、中文/空格/百分号等编码,metadata/tags/content headers 与隔离桶配置。 |
+| DE | 候选明确支持且依赖就绪的 SSE-S3/KMS/C、远端 tier 版本;密钥只由受控凭据渠道提供,禁止进入日志/manifest。 |
+| DO | object-lock、retention 和 legal hold 的受保护版本;使用可到期测试策略,防止成功用例因无法清理永久占用环境。 |
+| DL | 并发 PUT/GET/HEAD/LIST/COPY/DELETE、版本及 MPU 操作的逐请求 ACK ledger;每 key 用递增 nonce 与确定性内容,保存请求/响应和不确定结果。 |
+| DX | 保证可观察部分进度、大于状态窗口(>128 项)的夹具;默认上限在运行计划中冻结,长稳/性能单独配额,不靠无限加数据等到偶然命中。 |
+
+按 case/variant 参数化:normal/deep、data/parity shard、inline/外置 part、单片/多片、原盘/新盘、对象/前缀/root/set、版本/加密、协调者/目标节点、单节点/多节点/多 set/pool。使用预先冻结的 pairwise 组合控制规模;损坏+quorum、旧代次+并发删除、目标重建+进程中断等高风险组合必须显式三维覆盖,不由 pairwise 替代。
+
+候选声明支持的非默认 parity/storage class、服务端压缩 on/off 也应加入 DB/DM/DV 参数;Content-Encoding 响应头不是服务端压缩已被执行的证据。每个实例都从实际元数据确认编码形态。Object Lock 的拒绝删除必须发生于真实保留期内;清理不能绕过 Compliance、修改主机时间或以内部删文件规避限制。Legal hold 仅由本次授权测试角色正常解除;保留期超出 lane 窗口的实例必须预先分配独占 stateful 环境及到期处理方案,未清理环境不能交还 READY。
+
+重试不能隐藏 ACK:PUT/Complete 超时属于结果未知,必须按唯一 key/version/UploadId 查询解析;不能把未收到成功的一次写当丢失,也不能把重试新版本当同一次成功。并发时只根据可证明的 happens-before/提交回执判断“最后一次”,不凭跨节点墙钟排序。
+
+## 公共通过判据(每例均适用)
+
+每行用例的完整规范 = 本节 + 对应拓扑/夹具/故障契约 + 行内操作与附加断言。参数实例独立留证、计分。
+
+1. **预检和基线。** 全部预期 peer/磁盘、candidate/rc/harness 身份一致;预先 GET/HEAD/LIST 完整基线并保存必要的盘上记录。记录扫描/自动 heal 配置与任务归属。
+2. **注入和见证。** 以只读物理证据确认故障确实发生,未影响其他对象/服务;注入后不要先 GET 目标而意外触发 read-repair。受理过快或已被其他来源修好,不能冒充本例触发成功。
+3. **控制面真相。** 收集每次 rc exitCode、签名 Admin HTTP/body、token、source、scope、outcome 和 peer coverage。区分 repaired、verified_healthy、metadata_healthy、authoritatively_absent、deferred、failed、unknown、cancelled、dry_run_observed;deferred、failed、unknown、cancelled、dry_run_observed 不能作为“修复完成”。`metadata_healthy` 只证明 metadata/identity 健康,不证明 payload integrity;Normal scan 不得冒充 `verified_healthy`,deep payload 证明继续要求 `verified_healthy`。实际 wire 枚举以固定候选为准。
+4. **对象正确。** 所有必须存在的对象/版本全量 GET 验证长度/SHA-256/ETag/metadata/tags;HEAD、分页 LIST、历史版本与 marker 语义、已确认删除和并发 ACK 全部核对。允许修复产生合法内部元数据变化,不要求整个 xl.meta 字节不变。
+5. **实际补片。** 在静默校验窗口解析目标物理数据与元数据,核对版本/代次/data_dir、part 长度、bitrot 校验与 EC 一致性。对 data/parity 各有独立重建或重新编码对照,或用包含目标片的固定合法解码集合佐证;不能只统计文件、看 mtime 或依赖 GET 自动纠删码兜底。密文/压缩存储层不能直接拿明文摘要比较。格式不支持/HighwayHash 未验证时必须标明缺口,不能声称已完成深度物理验收。
+6. **持久化与幂等。** 需重启的用例验证重启后责任和数据;重复 heal 不引入新版本、复活删除或无限增加任务。状态计数按进程/任务/epoch 区分,不能把重置后的全局计数简单判为回退。
+7. **完成条件。** 正向修复要有相关责任完成、范围 coverage 完整、对象与物理证据一致。无关的全局 idle、列表项被截断、缺失 peer、unknown/残留待修工作都不能代替证明。预期不可恢复/权限/取消/dry-run 用例按预注册负向契约通过,明确它不是修复成功。
+8. **恢复和证据。** 解除临时故障,保存 before/fault/after/restart 四阶段证据,校验全局结果/JUnit/计划/对象数量/证据引用及清理。任何取证缺失、目标误命中、恢复失败或结果结构损坏均阻断该 lane。
+
+针对精确时间窗口,必须观察活动/部分进度或受控屏障,再注入故障;普通重建允许快速完成但必须有实际补片证据。禁止用固定 sleep 或“重跑直到绿”代替见证。建议起始测试预算:接口就绪 90 秒、普通修复 15 分钟、重启恢复 30 分钟、同 epoch 静默窗口 5 秒;这些是待校准的**测试预算,不是产品 SLO**,所有最终预算须在运行前冻结。预期暂态错误只按明确 code 有界重试,每次都留证。
+
+判定类型:产品安全/内容/终态错误为 fail;环境注入/取证工具失败为 infra_error;缺少尚未分配拓扑或辅助能力在部署前阻断。不得把产品 SlowDown 或低 quorum 下的预期错误泛化成 infra_error。必选能力缺失、未知选项、缺失参数实例不能用 unsupported/skipped 充 PASS;不适用项只能在运行前的 applicability manifest 中说明,环境尚未准备好不是“不适用”。
+
+## 84 个基础用例
+
+编号用于设计评审;`heal.*` 为稳定 catalog ID。topology、fixture、fault 中逗号表示必须展开记录的适用实例/后端集合,不表示同时向全部对象注入所有故障。对应的组合、先后和最小 quorum 必须在编译后的 variant plan 中明确,未证明的一项不得计为通过。
+
+| 分类 | 设计条目数 |
+| --- | ---: |
+| A 控制面、任务与权限契约 | 10 |
+| B 磁盘故障、替换与 quorum 边界 | 10 |
+| C 分片损坏、元数据与权威选择 | 11 |
+| D 对象类型与 S3 语义 | 11 |
+| E 触发路径与修复责任 | 6 |
+| F 中断、持久化、恢复与升级 | 9 |
+| G 与前台和后台操作并发 | 8 |
+| H 多 Pool、移动与多 set 隔离 | 7 |
+| I 资源、依赖故障与调度 | 6 |
+| J 可观测性、幂等、长稳与性能 | 6 |
+| 合计 | 84 |
+
+
+
+### A. 控制面、任务与权限契约
+
+| 编号 / 稳定 ID | P | 拓扑 / 夹具 / 故障 | 操作与触发 | 附加通过判据 |
+| --- | --- | --- | --- | --- |
+| A01 · `heal.admin-healthy-noop` | P0 | H1,H2 / DB / F0 | 对健康对象分别执行 normal/deep,再重复查询 | 不修改用户数据或版本;Normal 只接受 `metadata_healthy`,Deep 才要求 `verified_healthy`,healthy 与 repaired 分开计数,不能把检查盘数当修复盘数。 |
+| A02 · `heal.admin-scope-isolation` | P0 | H1,H2 / DB,DS / F1 | 分别以 root、bucket、prefix、object、pool/set 选择器启动 | 目标范围实际遍历;相邻前缀、其他桶及未选 set 的故障保持原状;按对象清单而非目录数量验收。 |
+| A03 · `heal.admin-token-cross-node-status` | P0 | H2 / DB / F1 | 从节点甲启动,经乙及稳定入口按原 token 查询 | 任务身份、scope、终态及核心计数有界收敛;状态缺失/未知不能解释为完成。 |
+| A04 · `heal.admin-dry-run` | P0 | H1,H2 / DB,DV / F1,F2 | 对缺片与损坏对象执行 dryRun,参数化 remove/recreate | 有明确 dry_run_observed 诊断;用户对象分片、xl.meta、版本与删除标记不变;后台修复干扰须排除。 |
+| A05 · `heal.admin-overlap-and-force-start` | P1 | H1,H2 / DB,DS / F1 | 活动中重发同范围、父子前缀和不相交请求,再 forceStart | 按已固定 overlap policy 验证合并返回原 token 或明确冲突;forceStart 不能泄漏旧任务责任或并发破坏对象。 |
+| A06 · `heal.admin-stop-and-stale-token` | P0 | H1,H2 / DB / F1 | 非零修复进度时 stop,稳定后新建任务,再发旧 token 的查询/停止 | 停止需等在途写结束并稳定;新任务身份独立,旧请求不得终止新任务;重复 stop 按公开契约处理。 |
+| A07 · `heal.admin-invalid-request` | P0 | H1,H2 / DB / F0 | 发送非法 selector、重复 query、非法布尔/scanMode、冲突 flags、超长 body | 服务端返回已核实的结构化错误;无误启动、范围扩大或持久化副作用;CLI 参数错误不能替代服务端负例。 |
+| A08 · `heal.admin-authorization` | P0 | H1,H2 / DB / F0 | 匿名、坏签名、过期签名、无 HealAdminAction 用户与最小授权用户分别请求 | 未授权 start/status/stop 均不能越权或泄漏对象信息;授权用户可完成规定操作,凭据不得写入证据。 |
+| A09 · `heal.admin-nolock-safety` | P0 | H1,H2 / DL,DV / F1,F9 | 通过签名 API 传 nolock=true,并与同对象提交制造确定性争用 | 兼容字段不得跳过存储层对象锁;最后成功写入和已确认删除保持正确,原生屏障与真机竞争证据分开。 |
+| A10 · `heal.admin-option-contract` | P1 | H1,H2 / DB,DV / F1,F2 | 参数化 recursive、recreate、remove、updateParity、readRepair 与合法/非法 pool/set 组合 | 仅测试已核实的候选语义;normal/deep 与危险 remove 的副作用明确;API 独有选项不得伪称 rc 已覆盖。 |
+
+
+
+### B. 磁盘故障、替换与 quorum 边界
+
+| 编号 / 稳定 ID | P | 拓扑 / 夹具 / 故障 | 操作与触发 | 附加通过判据 |
+| --- | --- | --- | --- | --- |
+| B01 · `heal.disk-loss-and-repair` | P0 | H1,H2 / DB / F4 | 让一个已确认 set 成员盘对 heal 服务不可用,持续读取,恢复原盘 | 记录真实故障窗口和源/目标盘身份;在可恢复条件下最终补齐目标分片及完整 manifest,不只验证降级 GET。 |
+| B02 · `heal.disk-readonly-and-permission` | P1 | H1,H2 / DB / F4,F5 | 仅对独占测试卷制造只读/拒绝访问,恢复后重试 | 不可写目标不得 reported repaired;错误及待修责任可见;恢复后真实写入目标且不落到系统盘或其他目录。 |
+| B03 · `heal.disk-replacement-formatted` | P0 | H1,H2 / DB,DV / F8 | 换入空的新逻辑盘,确认新 disk UUID/正确格式后触发重建 | 新盘与旧盘身份可区分且属于正确 pool/set/slot;逐版本重建完成,不能从隔离备份复制回来冒充 heal。 |
+| B04 · `heal.disk-replacement-unformatted` | P1 | H1,H2 / DB / F8 | 引入空未格式化测试盘,并与带异集群格式的盘分别比较 | 按候选支持的初始化契约安全接纳或明确拒绝;不得覆盖外来身份或将未就绪盘标为可写健康。 |
+| B05 · `heal.multiple-shard-loss` | P0 | H1,H2 / DB,DV / F1 | 按实际同版本 EC 布局依次缺失 1..M 个 shard,覆盖 data/parity 角色组合 | 每个参数实例单独满足读取与写入 quorum;满足修复条件时全部目标重建,不把跨版本或跨 set 的 shard 合并。 |
+| B06 · `heal.read-quorum-boundary` | P0 | H1,H2 / DB / F1 | 保留恰好 R 个同版本权威 shard,测试读取及修复提交,再恢复写 quorum | 读取边界和修复写入边界分别判定;不能因还能 GET 就要求在写 quorum 不足时提交成功。 |
+| B07 · `heal.below-read-quorum` | P0 | H1,H2 / DB,DV / F1 | 使权威来源小于 R,执行 GET 与 deep heal,再恢复被隔离源 | 故障期间不得返回伪造的成功内容、空对象或误删记录;明确 unresolved/失败;恢复后原对象和版本完整。 |
+| B08 · `heal.below-write-quorum` | P0 | H1,H2 / DB,DL / F4 | 构造读 quorum 尚存而写/锁 quorum 不足,执行 heal 与新 PUT | 不能虚报持久化修复或成功写入;分别记录对象、元数据、分布式锁 quorum;恢复后核对所有已确认写入。 |
+| B09 · `heal.node-outage-rejoin` | P0 | H2 / DB,DV,DL / F6 | 停止同一跨节点 set 的一个节点,执行允许的降级 I/O,再重入 | 按实际丢失盘数判定可用性;缺失期间成功提交的对象补齐;节点重新 ready 不等于所有对象已重建。 |
+| B10 · `heal.disk-and-node-flapping` | P1 | H2 / DB,DL / F4,F6 | 重复短暂离线/在线并在重建途中再次失联 | 重试与队列有界、身份稳定,无重复任务风暴;最终故障解除后收敛,所有已确认对象保持正确。 |
+
+
+
+### C. 分片损坏、元数据与权威选择
+
+| 编号 / 稳定 ID | P | 拓扑 / 夹具 / 故障 | 操作与触发 | 附加通过判据 |
+| --- | --- | --- | --- | --- |
+| C01 · `heal.missing-part-file` | P0 | H1,H2 / DB,DM / F1 | 仅隔离指定对象某个 part/shard,保留 format 与其他元数据,执行 deep heal | 对应数据或校验分片重新生成,part/版本身份准确;不得只看到 xl.meta 重现就通过。 |
+| C02 · `heal.data-shard-bitrot` | P0 | H1,H2 / DB,DM / F2 | 在指定 data shard 的已知偏移翻转字节,包含首/中/尾位置 | 故障前后摘要证明注入生效;deep 或 read-repair 发现损坏并修复;独立 bitrot/EC 及 S3 全内容验证通过。 |
+| C03 · `heal.parity-shard-bitrot` | P0 | H1,H2 / DB,DM / F2 | 损坏 parity shard,使普通 GET 仍可能成功,再执行 deep heal | 必须识别并重建损坏 parity;只有 GET 正确、无目标分片修复不能通过。 |
+| C04 · `heal.truncated-and-zeroed-shard` | P0 | H1,H2 / DB,DM / F2 | 分别截断 shard、置零和破坏校验头,保留正确元数据仲裁来源 | 短读、全零和校验损坏被准确识别;修复不能接受截断内容或错误长度。 |
+| C05 · `heal.missing-or-corrupt-xlmeta` | P0 | H1,H2 / DB,DV / F3 | 在少数成员上移走或损坏 xl.meta/inline 内容,保留足够权威来源 | 恢复同一对象的全部应存版本、数据引用与删除标记;不以新建空元数据替代真正修复。 |
+| C06 · `heal.stale-version-rejoin` | P0 | H1,H2 / DV,DL / F4 | 盘离线后执行覆盖/版本删除,再让携带旧元数据的盘重入 | 以已确认的新代次/版本为准;旧当前值与已删版本不复活,旧盘被纠正而非成为新的错误权威。 |
+| C07 · `heal.conflicting-subquorum-authority` | P0 | H1,H2 / DV / F3,F9 | 用受控原生夹具构造互不构成 quorum 的版本/代次集合,再做对应真机反例 | 禁止把不同版本的 shard 数量并集当成权威;无法证明时安全拒绝且保留可恢复证据,不猜测较新时间戳。 |
+| C08 · `heal.metadata-payload-mismatch` | P1 | H1,H2 / DB,DV / F2,F3 | 构造元数据声称某长度/摘要但实际 part 不匹配的少数副本 | 元数据与内容联合判断;不把可解析的 xl.meta 当健康;权威不足时拒绝覆盖正确副本。 |
+| C09 · `heal.foreign-format-and-unknown-schema` | P0 | H1,H2 / DB / F3,F8 | 提供外来 disk/set 身份、未来 schema 或不支持的记录格式 | 明确隔离/拒绝且不破坏原记录,不把未知当 missing 后直接重建;解除故障后原数据可用。 |
+| C10 · `heal.dangling-and-orphan-safety` | P1 | H1,H2 / DV,DM / F1,F3 | 构造 dangling part、旧 data_dir 与共享历史引用,分别 remove=false/true、宽限期前后检查 | 默认不得破坏仍可恢复数据;显式 remove 只清除具有充分删除证明且过期的本次孤儿,不删共享版本或 MPU。 |
+| C11 · `heal.bucket-incarnation-recreation` | P0 | H1,H2 / DV / F4,F9 | 盘离线时完成旧桶合法清空/删除,再同名建桶写入新对象,恢复旧盘与旧任务 | 旧 bucket incarnation 的 checkpoint/MRF/元数据不得污染新桶;旧任务不得删除或恢复新 incarnation 的对象。 |
+
+
+
+### D. 对象类型与 S3 语义
+
+| 编号 / 稳定 ID | P | 拓扑 / 夹具 / 故障 | 操作与触发 | 附加通过判据 |
+| --- | --- | --- | --- | --- |
+| D01 · `heal.object-size-boundaries` | P0 | H1,H2 / DB / F1,F2 | 覆盖 0/1 字节、页/inline/EC block 边界±1以及大对象 | 每个实际存储形态均确认后注入;长度、SHA-256、ETag、HEAD/LIST 与边界 Range 正确,无假设阈值。 |
+| D02 · `heal.completed-multipart-object` | P0 | H1,H2 / DM / F1,F2 | 修复已完成的不等长多 part 对象,分别损坏首/中/末 part | 完整内容、part 顺序、原版本/ETag/元数据和跨 part Range 不变;multipart ETag 不能当整对象 MD5。 |
+| D03 · `heal.historical-versions` | P0 | H1,H2 / DV / F1,F3 | 不同版本分布不同损坏位置,按对象和递归范围修复 | 所有原 versionId 可寻址且内容正确,最新版本身份不漂移,无多余版本或遗漏历史版本。 |
+| D04 · `heal.delete-marker-semantics` | P0 | H1,H2 / DV / F3,F4 | 缺失/陈旧 marker 副本后 heal,并按当前值与精确 marker versionId 查询 | 当前 GET/HEAD 与指定 marker 的状态码及身份头符合契约;marker 不复活数据,LIST versions 无重复。 |
+| D05 · `heal.versioning-suspended-null` | P1 | H1,H2 / DV / F1,F3 | 在版本开启、暂停、null 版本覆盖之间建夹具再修复 | null 和非 null 版本不混淆,不新增假版本;暂停前历史对象仍准确可读。 |
+| D06 · `heal.object-metadata-and-tags` | P1 | H1,H2 / DS / F1,F3 | 包含用户 metadata、Content-Type/Encoding/Disposition、tags 的对象修复 | 对外 metadata、tags 和相关响应头逐项相同;不要求无关内部元数据字节完全一致。 |
+| D07 · `heal.range-head-and-conditions` | P1 | H1,H2 / DB,DM / F1,F2 | 修复前后验证 HEAD、合法/越界 Range、If-Match/If-None-Match 及时间条件 | 200/206/304/412/416 等按逐请求契约判断,绝不返回旧内容、错误范围或错误 ETag。 |
+| D08 · `heal.encrypted-objects` | P0 | H1,H2 / DE / F1,F2 | 按候选支持的 SSE-S3/SSE-KMS/SSE-C 建加密对象并修复 | 授权解密后原内容及版本一致,物理验证使用正确编码/密文层;密钥不可用时不误删、不降级明文,证据脱敏。 |
+| D09 · `heal.object-lock-retention` | P0 | H1,H2 / DO,DV / F1,F3 | 对开启 retention/legal hold 的受保护版本修复,期间测试非法删除 | heal 不绕过保留/hold,也不丢失其元数据;拒绝删除与合法修复均有明确证据。 |
+| D10 · `heal.special-keys-and-pagination` | P1 | H1,H2 / DS / F1,F3 | 覆盖中文、空格、百分号、编码斜线、多层前缀和多页对象列表 | 逻辑 key 与物理路径映射准确,不跨桶/前缀;分页无遗漏重复,不能把用户 key 当可执行 shell 或裸文件路径。 |
+| D11 · `heal.bucket-metadata-integrity` | P0 | H1,H2 / DS,DV / F3 | 在隔离桶白名单内测试可支持的桶元数据恢复,并核对外部 bucket 配置 | 版本设置、策略/标签/保留配置按契约保持;不得遍历凭据配置或把未知内部记录当可重建数据。 |
+
+
+
+### E. 触发路径与修复责任
+
+| 编号 / 稳定 ID | P | 拓扑 / 夹具 / 故障 | 操作与触发 | 附加通过判据 |
+| --- | --- | --- | --- | --- |
+| E01 · `heal.manual-normal-attribution` | P1 | H1,H2 / DB / F1 | 禁止预读目标,保留 format,隔离背景干扰后显式 normal heal | 证明本次 Admin token 实际覆盖目标并修复可由 normal 检出的缺失;不要求 normal 发现所有静默 bitrot。 |
+| E02 · `heal.manual-deep-attribution` | P0 | H1,H2 / DB / F2 | 在普通读取仍可成功的 parity 损坏上显式 deep heal | 按 token/对象/版本及物理变更证明 deep 检查和修复,不把后台提前修复算给手工任务。 |
+| E03 · `heal.read-repair-attribution` | P0 | H1,H2 / DB,DV / F1,F2 | 不发 Admin heal,对已确认的特定损坏对象发一次可追踪 GET/Range | 读取正确且后续真实补片;若走异步队列需证实 source=read-repair,不能只有 GET 成功。 |
+| E04 · `heal.scanner-cold-object-repair` | P0 | H1,H2 / DB / F1,F2 | 不 GET/HEAD 损坏目标、不发手工 heal,仅等待已启用的 scanner 路径 | 冷对象被发现并修复,来源和遍历窗口明确;scanner 周期结束但目标没检查不能通过。 |
+| E05 · `heal.mrf-offline-write-replay` | P0 | H2 / DL,DV / F4 | 成员离线时完成有 quorum 的 PUT/DELETE,确认待修责任后重入 | durable MRF 责任持久且身份精确;只在 repaired/verified healthy/authoritatively absent 等已证明结果后释放,成功提交不丢失。 |
+| E06 · `heal.replacement-auto-rebuild` | P0 | H1,H2 / DB,DV / F8 | 替换盘接纳后不发 Admin/目标 GET,观察自动 replacement heal | 替换代次、ready/completion 与盘上全部目标版本一致;不能用服务 ready、队列空或计数重置代替完成。 |
+
+
+
+### F. 中断、持久化、恢复与升级
+
+| 编号 / 稳定 ID | P | 拓扑 / 夹具 / 故障 | 操作与触发 | 附加通过判据 |
+| --- | --- | --- | --- | --- |
+| F01 · `heal.root-graceful-coordinator-restart` | P0 | H1,H2 / DX / F6 | root heal 非零进度时优雅重启原协调者,保留原拓扑与记录盘 | 在明确支持的边界内恢复原 task ID 和剩余责任/预算,完成后无遗留活动记录;不扩张为协调盘丢失后的故障转移。 |
+| F02 · `heal.target-graceful-restart` | P0 | H2 / DB,DV,DX / F6,F8 | 替换目标仅完成部分重建时优雅重启目标节点 | 故障确实发生于部分完成窗口;重启后续建同一目标盘/代次,最终 manifest 与全部目标分片正确。 |
+| F03 · `heal.target-process-crash` | P0 | H2 / DB,DV,DX / F6,F8 | 部分重建时 SIGKILL 测试进程,再正常启动 | unclean shutdown 与修复进度有真实证据;已提交对象不丢失,未完成目标不会被当完整;明确不是断电测试。 |
+| F04 · `heal.coordinator-process-crash` | P0 | H2 / DB,DX / F6 | 协调者非优雅退出并恢复,分别观察 root、MRF、replacement | 按各机制已核实的持久化边界判断;不得把丢失的易失 token 当成功,需显式记录剩余责任并可安全重扫恢复。 |
+| F05 · `heal.full-cluster-restart` | P0 | H2 / DB,DV,DM / F6 | 有未完成修复责任时重启全部测试节点,固定原拓扑/磁盘身份 | 有持久化保证的责任不遗失;全部端点恢复一致,旧版本/marker/MPU 与目标分片完整,重复重启不扩散错误。 |
+| F06 · `heal.cancelled-task-restart` | P0 | H1,H2 / DB,DX / F6 | 完成取消及在途退出后重启,随后 forceStart/新任务正常修复 | 已取消的任务不自动复活;旧 checkpoint/token 不能取消或覆盖新任务,旧责任不会误标为修复完成。 |
+| F07 · `heal.checkpoint-and-mrf-corruption` | P0 | H1,H2 / DB,DV / F9 | 受控构造 checkpoint/MRF 截断、坏校验、未来 schema、陈旧代次及写入失败 | 已确认持久化责任不得静默删除或伪 ACK;未知记录保留且可诊断;精确崩溃窗口由原生屏障证明,不伪称真机命中。 |
+| F08 · `heal.mixed-version-upgrade-rollback` | P1 | H2 / DB,DV / F6,F9 | 固定前后两个明确兼容候选,覆盖滚动升级、混合 peer、回滚与重启 | 能力不足必须显示 partial/unsupported 而非完整成功;按格式兼容契约保留责任,旧版本不能误退休新版本记录。 |
+| F09 · `heal.dedicated-host-forced-restart` | P1 | H6 / DB,DV,DX / F10 | 经专项授权在部分修复/持续写入时强制重启独占测试 VM/主机 | 保留平台事件与 ACK ledger;验证落盘和恢复,不把 SIGKILL 当断电;VM 强制停止也不等同于物理控制器断电。 |
+
+
+
+### G. 与前台和后台操作并发
+
+| 编号 / 稳定 ID | P | 拓扑 / 夹具 / 故障 | 操作与触发 | 附加通过判据 |
+| --- | --- | --- | --- | --- |
+| G01 · `heal.race-unversioned-overwrite` | P0 | H1,H2 / DL / F1,F2,F9 | 同 key 连续覆盖与 heal 同时推进,包含提交尾部清理窗口 | 按可证明的提交顺序验证最终最后一次成功写入;不能回滚到旧 data_dir,未确认超时写入单独解析。 |
+| G02 · `heal.race-versioned-put` | P0 | H1,H2 / DV,DL / F1,F9 | 修复历史版本时创建新版本并重复读取各 versionId | 新旧版本不混片、不串内容;已返回成功的每个版本都保留,最新值按确定的先后关系验收。 |
+| G03 · `heal.race-version-delete` | P0 | H1,H2 / DV / F1,F2,F9 | 对损坏的精确历史 versionId 发 DELETE,与 deep heal 竞争 | 已确认删除的版本不能被复活,其他历史版本不受影响;原生临界点测试与真机竞争分别留证。 |
+| G04 · `heal.race-markers-and-batch-delete` | P0 | H1,H2 / DV / F3,F9 | 与创建/删除 marker、批量版本删除和无版本删除分别交错 | 按逐项 DELETE 回执核对版本/marker 集合,无复活、误删或遗漏批量错误。 |
+| G05 · `heal.race-multipart-complete-abort` | P0 | H1,H2 / DM,DL / F1,F9 | heal 期间对不同已知 UploadId 分别续传、Complete、Abort | 成功 Complete 内容只出现一次;Abort 不破坏已提交对象或别的 upload;未完成 part 不被伪造为完整对象。 |
+| G06 · `heal.race-copy-and-metadata-update` | P1 | H1,H2 / DS,DL / F1,F9 | COPY 源/目标和标签/元数据更新与修复交错 | 最终对象内容与对应提交的 metadata 一致,读修复发现新代次应重验/放弃陈旧提交而非覆盖。 |
+| G07 · `heal.race-ilm-expiry-and-transition` | P0 | H2 / DV,DE / F1,F9,F11 | 分别与当前/历史版本过期、远端 transition/restore 同窗口执行 | 已授权过期不被恢复;迁移状态与远端版本引用正确;只有实际观察到交叠才计并发覆盖,tier 未配置需明确阻断对应实例。 |
+| G08 · `heal.race-replication-resync` | P1 | H2 / DV,DL / F1,F9,F11 | 在隔离复制两端与 heal 同时运行新增/删除/重同步 | 对端版本、删除语义及源数据最终一致,无无限回环或重复传播;不得依赖仅源端 GET 宣称复制正确。 |
+
+
+
+### H. 多 Pool、移动与多 set 隔离
+
+| 编号 / 稳定 ID | P | 拓扑 / 夹具 / 故障 | 操作与触发 | 附加通过判据 |
+| --- | --- | --- | --- | --- |
+| H01 · `heal.with-expansion-rebalance` | P0 | H5 / DB,DV / F1,F2 | 先证明源 pool 持有夹具,再扩容并与修复同时 rebalance | 修复目标不得指向错误 pool/set;新旧数据完整、移动终态正确,保留 scanner backlog 提交权威检查。 |
+| H02 · `heal.with-rebalance-stop-resume` | P1 | H5 / DB,DL / F1,F6 | heal 活动期间 stop/restart/resume rebalance | 两类任务状态和身份独立;停止/恢复移动不误终止修复或丢失对象,恢复后两者均满足各自契约。 |
+| H03 · `heal.with-decommission` | P0 | H5 / DB,DV,DM / F1,F2 | 分别在待退役源和存活目标制造可恢复缺片,再执行退役 | 不得提前删源或以临时副本冒充迁移成功;按 Pool 既定写入限制验收;只剩 survivor 后仍完整可读且有物理证明。 |
+| H04 · `heal.with-decommission-cancel-clear` | P0 | H5 / DB,DL / F1,F9 | heal/移动争用中取消退役、Clear、恢复,并覆盖容量锁超时恢复 | 保持持久化后通知及在途退出顺序;不误取消新代次;对 #2423 的重试窗口单独注明真机是否实际触发,不只看最终绿色。 |
+| H05 · `heal.after-retired-pool-removal` | P0 | H5 / DB,DV / F1,F3 | 源 pool 完全退役移除并停机后,只在 survivor 上触发修复 | 不再向已移除源写入或依赖其兜底;来源权威和版本正确;#2422 路径不能出现无权威覆盖或错误回退。 |
+| H06 · `heal.multi-pool-target-isolation` | P0 | H5 / DB,DV / F1,F4 | 仅破坏选定 pool/set,并在另一 pool 保留不同对象与控制故障 | 选择器、任务、容量/配额和目标 drive 一致;未选 pool 不受错误修复,跨 pool 存在不等于该目标已修复。 |
+| H07 · `heal.ec84-multi-set-restart` | P0 | H3,H4 / DB,DV,DX / F6,F8 | 固定 EC8+4 单 set 与至少两 set,部分重建时重启/崩溃目标 | 每个 set 的故障覆盖和重建分别证明,不跨 set 拼 shard;必须满足真实盘数及映射,不能用 4 盘用例替代 EC8+4。 |
+
+
+
+### I. 资源、依赖故障与调度
+
+| 编号 / 稳定 ID | P | 拓扑 / 夹具 / 故障 | 操作与触发 | 附加通过判据 |
+| --- | --- | --- | --- | --- |
+| I01 · `heal.slow-io-and-eio` | P1 | H1,H2 / DB,DX / F5 | 在独占故障层注入有界延迟、短读、EIO,解除后观察恢复 | 暂态与永久错误分开,重试有界且无忙循环;解除故障后实际修复,期间不得提前标健康。 |
+| I02 · `heal.enospc-inodes-and-quota` | P0 | H1,H2 / DB,DM / F5,F11 | 限制专用测试卷空间/inode,并参数化本次桶配额 | 仅填专用受限卷、不耗尽共享真实盘;失败不破坏好副本或释放尚需容量,释放空间后正确续修。 |
+| I03 · `heal.network-partition-and-lock-quorum` | P0 | H2 / DB,DV,DL / F7 | 仅隔离测试服务节点间流量,覆盖少数派、读/写/锁 quorum 边界及恢复 | 少数派不得承诺无安全性的修复或写入;cluster coverage 不完整不能显示全局健康;恢复无分叉、跨代覆盖。 |
+| I04 · `heal.queue-pressure-and-admission` | P1 | H1,H2 / DX / F11 | 多来源请求冲击有限队列,混合重复任务、不同优先级和多个桶 | Full/Dropped/Merged 语义与实际任务一致;丢弃不能丢 durable 责任;在预先声明负载窗口内无无限堆积或健康误报。 |
+| I05 · `heal.lock-timeout-and-caller-abort` | P0 | H1,H2 / DL,DV / F5,F9 | 真实对象锁争用超过预算及调用方断开,再释放锁恢复 | 取消/超时不得提前释放在途所有权或重复提交;陈旧 read-repair 必须重验代次;恢复后无泄漏和错误覆盖。 |
+| I06 · `heal.kms-and-tier-outage` | P1 | H2 / DE,DV / F11 | 仅让已分配测试 KMS/远端 tier 暂不可用,持续查询与修复 | 依赖故障不得解释为对象不存在/可删/已修复,不暴露密钥;恢复后原版本可解密或恢复,责任未丢失。 |
+
+
+
+### J. 可观测性、幂等、长稳与性能
+
+| 编号 / 稳定 ID | P | 拓扑 / 夹具 / 故障 | 操作与触发 | 附加通过判据 |
+| --- | --- | --- | --- | --- |
+| J01 · `heal.status-partial-disabled-unknown` | P0 | H1,H2 / DB / F0,F6 | 分别检查 enabled/disabled/uninitialized、全体可达及缺失 peer 的状态 | idle 只在覆盖完整且真实无活动时成立;partial/unknown/degraded/disabled 不被客户端转换为全部完成。 |
+| J02 · `heal.outcome-counters-and-error-truth` | P0 | H1,H2 / DB,DV / F1,F2 | 同批次混合健康、可修、不可修、deferred、dry-run 对象 | execution/coverage/disposition/计数和逐对象物理结果相符;unknown、skipped 或 completed_with_errors 不能算修复成功。 |
+| J03 · `heal.incremental-cursor-and-truncation` | P0 | H1,H2 / DS,DX / F1 | 生成超过结果保留上限的任务,分页轮询 sinceSeq,并跨节点/终态读取 | nextSeq/minSeq 单调且 gap/truncation 明示;有界 items 不是完整责任清单,最终须用独立 manifest/计数补证,不静默丢错误。 |
+| J04 · `heal.repeat-idempotency` | P1 | H1,H2 / DB,DV / F1 | 第一次修复后重复 normal/deep、重启再检查 | versioned 首次允许所有 identity repaired 或至少一个 repaired 加其余正健康态,marker 仅 repaired/metadata_healthy,计数必须与 receipt 集合一致;normal repeat 接受 `metadata_healthy`,deep repeat 要求 `verified_healthy`,repeat marker 为 `metadata_healthy` 且 healed=0;unknown/skipped/failed 不得成为 PASS。 |
+| J05 · `heal.long-soak` | P2 | H2,H5 / DX,DL,DV / F4,F6,F11 | 独占环境运行预注册 24h/72h 窗口,混合修复、掉线、写入和版本删除 | 每次 ACK 可追踪;最终全量 manifest 正确,队列/内存/FD/临时空间无无界增长,无后台 panic 或未解释持久责任。 |
+| J06 · `heal.performance-abba` | P2 | H2 / DX,DL / F1,F11 | 同硬件同候选按 ABBA 运行无 heal 基线和有 heal 负载,重复并固定限速 | 分别报告修复吞吐、GET/PUT P95/P99、错误率和 CPU/IO;阈值事先冻结,不能事后挑轮次或把功能 PASS 充性能验收。 |
+
+## 与主仓库既有验证的复用关系
+
+以相同源 SHA 的实现为准,先读脚本的 scope、环境要求和故障 oracle,再适配到固定 fleet;**不直接在共享机器运行原 E2E 的清盘/建进程脚本**。已有覆盖只是复用线索,不能直接标记本目录 planned 用例为 implemented。
+
+| 已有资产 | 可复用内容 / 本矩阵对应 | 不能混淆的边界 |
+| --- | --- | --- |
+| `crates/e2e_test/src/heal_erasure_disk_rebuild_test.rs` | runtime wiped disk、Admin deep、remote replacement,B/C/E/F | 进程 E2E 与固定真机、同节点目录盘与独立 XFS 物理盘需分别标记;检查其 skip 分支,必选故障能力缺失不能通过。 |
+| registry 的 background-target-restart / crash | F02/F03、目标部分进度与 replacement shard 证据 | SIGKILL 不是断电;原夹具无版本覆盖不能自动扩张为 DV 已覆盖。 |
+| registry 的 EC8+4 single-set / multi-set / multi-pool 项 | H07/H06 与相应 F02/F03 参数 | 精确盘数、set/pool 分布与同一故障窗口要匹配,4 盘 PASS 不能覆盖 G14。 |
+| W13 durable MRF 与 W16 recovery-intent/quota 证据 | E05、F07、I02、J02 | 责任 ACK、恢复意图、配额与终态内容都需要原始实测输入,不只运行 JSON assembler。 |
+| G05/G06 status/outcome、G02 checkpoint、G09 upgrade 证据生产脚本 | A03、J01-J03、F07/F08 | schema 自检、descriptor 组装和真实故障执行是三种不同证据,不可互相代替。 |
+| ECStore/heal 原生 runtime-fence、版本删除/覆盖竞争回归 | A09、C07、G01-G06、I05 | 原生可控屏障覆盖精确窗口;没有在 fleet 触发该窗口时只记录功能覆盖,不把两类日志混为真机因果证明。 |
+
+来源:[必选 E2E registry](https://github.com/rustfs/rustfs/blob/940e221988694da33fe005054bb86086bae88cd6/.config/scanner-heal-required-tests.json)、[磁盘重建 E2E](https://github.com/rustfs/rustfs/blob/940e221988694da33fe005054bb86086bae88cd6/crates/e2e_test/src/heal_erasure_disk_rebuild_test.rs)、[Linux evidence plan](https://github.com/rustfs/rustfs/blob/940e221988694da33fe005054bb86086bae88cd6/scripts/run_scanner_heal_linux_evidence_plan.py)、[脚本分类](https://github.com/rustfs/rustfs/blob/940e221988694da33fe005054bb86086bae88cd6/scripts/README.md)。其中 plan/preflight 不产生 measured evidence,assembler 不替代采集。
+
+## 当前框架的执行方式
+
+Functional Chain 在共享节点并发锁内调用 auto-testing 的 canonical dispatcher。普通 live 执行消费候选 deb 和工具产物,无需 RustFS checkout;只读 metadata inspector 和原生屏障测试须在构建阶段绑定精确候选源码,单独留证。
+
+尚未自动化的分支由 `matrix.py plan` 固定全部适用拓扑、源断言、操作顺序、参数绑定与恢复步骤。`matrix.py audit` 校验九类证据的摘要和分支完整性,结果仍须逐项审阅,绝不自行生成 canonical PASS。
+
+
+正向 H1/H2 夹具显式启用 `RUSTFS_SHARD_INTEGRITY_WRITE=true` 和 `RUSTFS_SHARD_INTEGRITY_FLEET_CONFIRMED=true`。该确认只针对本例私有 endpoint 集合:所有参与节点先安装并校验同一候选二进制,再启动专用服务;配置从真实进程环境读回。候选默认值为 false,默认 legacy 对象的 unknown 回执已单独保留为失败证据,不能改写为 repaired,也不从保护模式推断默认部署已满足这些正向证明契约。
diff --git a/docs/validation-heal-environment-gates.md b/docs/validation-heal-environment-gates.md
new file mode 100644
index 00000000..6486284e
--- /dev/null
+++ b/docs/validation-heal-environment-gates.md
@@ -0,0 +1,56 @@
+# Heal 环境门控与原生阻断
+
+## Read-only fleet inspection
+
+The Functional Chain `environment.inspect` entry records the current four Heal hosts' mount capabilities, fixed `rustfs.service` status, loaded validation service names and listening PIDs on port 9000. Service and listener observations must agree across two samples. Process arguments, service descriptions and credential files are excluded. Inspection never stops services or changes retained scenes.
+
+These observations do not certify a clean environment. Independently audit retained validation scenes and the complete shared-workflow census before dispatching another case. A completed workflow's success or failure alone does not prove recovery. The regular case installer remains responsible for stopping the standard service, and its existing namespace/port reservation guards remain authoritative.
+
+When the shared runner cannot start `environment.inspect`, the separate `validation_runner_inspect_only=true` entry inspects GitHub registration and queue metadata on a hosted runner without fleet access. Inspection [40256930895/1](https://github.com/rustfs/rustfs/actions/runs/40256930895) observed `rustfs-smoke-testing` (ID 139653) offline at `2026-10-26T02:00:24Z`. This is not a diagnosis of host or disk health. API failures remain incomplete; restoring registration does not certify retained-scene cleanup or release the existing live admission gates.
+
+第一批 13 个用例保留全部 recipe、分支和 source assertions,并新增 `environment_cases.py` 的控制流程与断言,接入 validation 调度。它们默认关闭,只产生 `BLOCKED` 结果与未满足原因;默认路径不会连接或修改 fleet。真实执行沿用 auto-testing 的 S3/Admin collector、ACK ledger、候选包绑定、物理故障 oracle 和恢复模式,专用资源操作由部署环境的适配器提供。当前没有把适配器协议或未运行代码计为 canonical PASS。
+
+设置 `AUTO_TESTING_ENABLE_ENV_GATED_HEAL=1` 并通过 `VALIDATION_HEAL_ENVIRONMENT_CAPABILITIES` 指向 JSON 能力清单后,门控会检查全部能力。缺项仍返回 `BLOCKED`。能力齐全后还要用 `VALIDATION_HEAL_ENVIRONMENT_RUNTIME` 指向 runtime JSON;只有候选/checkout/client 摘要、全部拓扑、当前 Heal SSH 节点和适配器摘要校验通过后才会运行分支。未绑定适配器仍为 `BLOCKED`;执行结果需要 source assertion 审核,当前实现不会自动生成 canonical PASS。
+
+能力清单示例:
+
+```json
+{"capabilities": ["dedicated_faultfs_or_device", "slow_io_short_read_eio", "namespace_isolation", "fault_layer_restore"]}
+```
+
+| Code | 用例 | 当前未满足原因 | 启用条件 |
+| --- | --- | --- | --- |
+| F08 | `heal.mixed-version-upgrade-rollback` | 没有经过审核的不可变双版本组合 | 两个兼容 release、混合 peer 状态、滚动升级/回滚和格式兼容矩阵 |
+| F09 | `heal.dedicated-host-forced-restart` | 共享 Heal 节点不具备 H6 专用主机及平台断电能力 | 专用 H6、平台 power-cycle、带外恢复、boot_id/平台事件证据 |
+| G07 | `heal.race-ilm-expiry-and-transition` | 缺隔离 ILM/tier 与真实操作交叠窗口 | 专用租户、Expire/Transition/Restore 窗口、远端版本映射、规则清理 |
+| G08 | `heal.race-replication-resync` | 缺独占复制两端与 resync 控制 | 隔离复制对、resync、跨端版本映射、复制/Heal 交叠证据 |
+| H01 | `heal.with-expansion-rebalance` | 缺完整 H5 扩容/rebalance 拓扑 | H5 扩容、rebalance task、逐对象 pool/set 映射、scanner 退休证据 |
+| H02 | `heal.with-rebalance-stop-resume` | 缺独立 rebalance 停止/恢复/重启控制 | H5 rebalance、独立 task、stop/resume 和 service restart |
+| H03 | `heal.with-decommission` | 缺可回滚的退役拓扑 | H5 decommission、survivor pool、写入限制、移动 ACK 与物理权威证据 |
+| H04 | `heal.with-decommission-cancel-clear` | 缺退役新代次与容量锁重试窗口 | cancel/clear/resume、#2423 锁窗口、代次绑定 ACK ledger |
+| H05 | `heal.after-retired-pool-removal` | 缺 retired pool 移除后的 survivor-only 拓扑 | retired pool 配置移除、源持续离线、survivor 权威、旧 pool 写保护 |
+| H07 | `heal.ec84-multi-set-restart` | H4 的至少 24 个独立 endpoint 不可用 | H3 12 独立盘、H4 24 endpoint、逐 set 身份与重启/崩溃证据 |
+| I01 | `heal.slow-io-and-eio` | 没有专用 faultfs/故障设备 | 专用故障层、slow/short-read/EIO、namespace 隔离、原状恢复 |
+| I06 | `heal.kms-and-tier-outage` | 没有专用 KMS/tier 租户与依赖隔离 | 独占租户、依赖网络隔离、key 脱敏、tier 版本映射 |
+| J05 | `heal.long-soak` | 缺可持续 24h/72h 的独占 lane 与资源预算 | 真实墙钟窗口、限速和监控、循环故障、到期回收;短跑不能替代 |
+
+`matrix.py list` 同时输出 `automaticExecutor`、`dispatchable` 和 `executionPolicy`:67 个 Heal 自动执行器、13 个环境门控、4 个证据/执行器阻断。调度覆盖共 80 个 Heal,连同 74 个非 Heal 为 154/158;这不代表 154 个 live PASS。
+
+第二批 4 个用例仍未进入 canonical 调度。C11/F07/J02 已有 candidate-bound 原生回归和显式诊断入口;诊断分支通过仍需完整矩阵和独立审计后才能解除门控。I05 仍缺精确服务端屏障,增加节点不能解决:
+
+| Code | 用例 | Block 原因 | 解锁所需证据 |
+| --- | --- | --- | --- |
+| C11 | `heal.bucket-incarnation-recreation` | H1/H2 旧元数据盘重返、原始 checkpoint/resume 与 committed MRF 重放编排已实现;精确候选的 9 项存储原生回归和 3 项 manager incarnation 前置证据已独立核验。完整现场分支尚无独立审计结果 | 旧/新 incarnation、正常 S3 删除与新 ACK、物理范围和原生 artifact 精确绑定;单分支不提升整例 |
+| F07 | `heal.checkpoint-and-mrf-corruption` | 已实现 H1/H2 checkpoint 截断、未来 schema、committed MRF CRC 故障及独立安全重扫;10 项原生回归作为前置证据。真实较早代次回放与私有限额卷的 journal ENOSPC 已实现;仍缺完整 native cut 审核,完整 live 尚未审计 | 真实 durable 记录故障与 publication 证据绑定;现有 interruption hooks 不等于每个 fsync 或断电 |
+| I05 | `heal.lock-timeout-and-caller-abort` | 同源码 native 子集已保留;H1/H2 timeout 与 queued-successor 诊断已通过独立 live 审计(39767336741/1、39817606025/1),完整用例仍 BLOCKED | 实际断连边界所有权、完整 native timeout/stale 屏障、stale-generation controller 和 H1/H2 DL/DV 完整对象/ACK/物理/锁/FD 闭环 |
+| J02 | `heal.outcome-counters-and-error-truth` | 全部夹具与 manifest 对账均已实现。候选 `7951bb629821246868ee3761b321dd07c06db45c` 修复整桶重试覆盖 repaired 记录,210 项 manager 与 5 项独立进程集成测试通过。新实机 `38874900846/1` 完成 20 个分支:16 个报告通过,4 个原始全元数据损坏探针返回永久 Failed,保留 NOT_OBSERVED;四组混合任务与全部 50 条节点清理记录已独立审计。真实 legacy Unknown 已观察,不能代替原始探针 | 原始元数据损坏分支的 Unknown 仍未观察;完整 source-procedure 验收尚未闭合。原始 Failed、NOT_OBSERVED 和完整用例门控保持不变 |
+
+I05 与 A09、G01–G06 的精确屏障证据允许使用同一不可变候选源码单独构建的 `cfg(test)` 原生测试执行文件。不得给优化服务候选加入隐藏 hook 冒充原包,也不能把缺少 release hook 本身归为外部环境阻塞。原生子集、完整服务调用和现场最终 ACK/物理检查分别验收;已验收 native 证据不能代替尚缺的 controller 或完整 live 结果。第一批 G07/G08 的 native 分支与 H04 容量锁窗口保留各自未完成条件;外部请求重试或最终对象正确均不能证明精确锁或提交窗口。
+
+2026-10-26 已独立复核 hosted-only tools [40086026324/1](https://github.com/rustfs/rustfs/actions/runs/40086026324):候选 `757b751482bb970bc321e92059690a7698554358` 上 10 项精确原生测试和 104 条有序 milestone 全部通过,覆盖 A09、G01–G06 与 I05 的原生子集。固定 helper `002473b` 的 runtime admission 在该候选上仅开放 A09/G05/G06;G01–G04 仍须选择各自审核过的候选及匹配工具,或先完成新的 runtime 源码审核。完整 live 用例和 I05 剩余控制流程均未因此通过,canonical 计数保持 114/158。
+
+资源适配器协议使用 stdin/stdout JSON,不调用 shell。runtime 中的 `adapter` 包含绝对可执行路径、SHA-256 和操作 allowlist;`environment_cases.required_operations(case_id)` 给出所需操作。每个请求携带 run ID、原候选 commit、sequence、拓扑/分支 scope、bucket prefix、租约和参数。响应必须回显上述身份、按 `heal_probe.canonical(request)` 的 UTF-8 SHA-256 绑定原请求,并把真实观察写在 `observation`,禁止返回 verdict。资源适配器需独立实现平台 power control、故障层、pool lifecycle 或 KMS/tier 隔离;本 PR 未向当前共享环境部署适配器。
+
+runtime 的 `binding` 使用现有 `heal_probe.validate_binding` 契约,另含 `schemaVersion: 1`、`provider: auto-testing`、`sshNodes`、`rcBinary`、`topologies`、`capabilities`、`adapter` 和对应资源字段。F08 需要 `releasePair`,F09 需要 `hostResourceId`,I01 需要 `faultDeviceId/ioLatencyMs`,G07/I06 需要 `kmsKeyId/tierEndpoint/tenantId/dependencyNamespaceId`,G08 需要 `replicaEndpoint`,J05 需要 `soakLimits`。H6 专用主机应由适配器按 `hostResourceId` 独占绑定;SSH fleet 仍记录当前 auto-testing Heal 节点。secret 通过环境传入,适配器诊断会脱敏,`redactionEnvironmentVariables` 可指定额外 secret 环境变量名。
+
+J05 的实际控制循环保留固定 86400/259200 秒窗口、精确版本写入/删除 ACK、三类循环故障、恢复、资源阈值和到期全量校验。单元测试用可注入的时钟验证窗口控制,不修改生产时长。G07/G08 精确 native 分支不调用资源适配器。清理失败保留原失败与 DIRTY 原因,停止后续分支。
diff --git a/docs/validation-heal-operations.md b/docs/validation-heal-operations.md
new file mode 100644
index 00000000..71247e0e
--- /dev/null
+++ b/docs/validation-heal-operations.md
@@ -0,0 +1,1130 @@
+# Heal 真机执行手册:84 项操作卡的公共命令
+
+此手册和 [84 项逐例操作卡](validation-heal-cases.md) 一起构成可执行的**人工操作方案 + 脚本辅助**。不是 84 个无人值守自动测试,也不是通过报告。操作卡列出 302 个分支模板;真实盘角色、阈值、损坏数量和拓扑仍需在每批 `parameterBindings` 展开,不能把模板数量当实测次数。
+
+公共验收契约见 [Heal 规范](validation-heal-contract.md)。源仓库历史 pilot 结果不计入当前 canonical 分母。自动入口 `scripts/run_validation_case.sh CASE_ID CANDIDATE_MANIFEST TESTING_COMMIT ARTIFACTS_DIR` 已接入 67 个 Heal 用例,完整清单通过 `matrix.py list` 查看,以 `automaticExecutor` 为准。入口使用当前 Heal 节点、候选 deb 和独立 H1/H2 服务;其余操作卡仍需按本手册操作留证。
+
+## 1. 使用入口与交付物
+
+```bash
+python scripts/validation_heal/matrix.py list
+python scripts/validation_heal/matrix.py show C02
+python scripts/validation_heal/matrix.py show F02,F03,H07
+python scripts/validation_heal/matrix.py plan --selection all \
+ --run-id manual-heal-20260910-a --out reports/validation-heal-84-plan.json
+python scripts/validation_heal/matrix.py plan --selection A01,A04,C01,C02,C04,J04 \
+ --topologies H1 --run-id manual-heal-20260910-b --out reports/heal-h1-plan.json
+```
+
+`plan` 只冻结选择/分支和对应操作,不部署、不评分。`show` 同时给出步骤、命令入口、验收和恢复要求。不同拓扑是不同实例;未选拓扑会保留 `scopeExclusions`,不能据局部结果声称全矩阵通过。
+
+自动用例通过 RustFS Functional Chain 的 `validation_case` 和精确 `auto_testing_sha` 分发。先用同一不可变候选的 `build_run_id`/`build_run_attempt`、`validation_tools_only=true` 构建只读 metadata inspector;live 分发提供该成功构建的 `validation_tools_run_id` 与 `validation_tools_run_attempt`。工具的 candidate/helper SHA、Linux 二进制 SHA 和独立 Cargo.lock 摘要均须通过检查,再复制到本例节点。普通 live 执行消费构建产物,不需要 RustFS checkout。
+
+| 工具 | 已实现的职责 | 不代表什么 |
+| --- | --- | --- |
+| `matrix.py` | 84 项/302 分支索引、逐例操作卡、冻结计划、证据完整性审计 | 不自动分配拓扑,不根据文件存在就判 PASS |
+| `heal_probe.py` | 带签名/匿名/坏签名的原始 Admin;受限 S3 操作;全版本/标签/内容摘要清单及比较 | HTTP 检查通过不代表 heal 成功;inventory 的 GET 会触发读修复 |
+| `heal_workload.py` | DB/DV/DM/DS/DX 真实夹具;有界并发、同key前台修改串行、请求前及ACK后持久落盘 | 不替用户推导竞态后的最终版本;超时写必须单独解析 |
+| `heal_fault.py` | Linux 描述符相对寻址、mount ID 保护、单文件 missing/bitflip/truncate/zero/header、快照和恢复 | 文件故障不是物理掉盘;不修改系统 metadata;不盲目恢复新代次 |
+| `prepare_heal_meta_inspector.py` | 为精确候选源码准备只读 Rust metadata 解析器工程 | 需在批准的构建机编译;解析 metadata 不是验证 bitrot/EC |
+| `shards.py` | C01–C04、D02、D07、E01、E02 全 24 分支以及 A03 的两个 H2 跨节点状态分支、A04 的六个 DB/DV dry-run 分支;静默故障、真实角色、同角色原片与 HighwayHash、先物理后 GET/Range、MPU control | 有自动入口不等于候选通过;任务 unknown、目标只恢复 metadata 或救援备份均不能记 PASS |
+| `check_bitrot_tool.py` | 在固定 MinIO/历史 RustFS 产物上检查 24 个健康片和 12 个坏片负例 | 工具验证不等于 live 用例通过;不调用被测 RustFS 服务 |
+
+## 2. 先绑定环境,禁止直接猜节点
+
+live 命令使用当前 auto-testing 的 `smoke-testing` runner、`RUSTFS_NODES` 和 `RUSTFS_SSH_USER`,复用 `rustfs_heal_test.sh` 的 Heal 节点。运行前取得与 Functional Chain 一致的共享节点并发锁。H1–H6 是测试拓扑要求,不是另一组 SSH 主机;不得按源仓库历史主机名或 IP 选择机器。
+
+### Runner 无法接单时的只读诊断
+
+RustFS Functional Chain 提供 `validation_runner_inspect_only=true` 入口。它在 `ubuntu-latest` 上读取 GitHub 的 runner 注册信息与活动任务,使用独立并发组,不占用或替换测试 fleet 队列。一次请求只能选择该模式,不同时传入 candidate、tools、case 或 recovery 参数;使用包含此入口的已审核 workflow ref:
+
+```bash
+gh workflow run rustfs-functional-chain.yml --repo rustfs/rustfs \
+ --ref "$VALIDATION_DRIVER_REF" -f validation_runner_inspect_only=true
+```
+
+下载该次 `validation-runner-inspection--` artifact,核对 `workflowCommit`、`runId`、`runAttempt`、`observedAt`、`complete` 和 `runnerAvailability`。`offline`、`online-busy`、`online-idle`、`unregistered` 是 GitHub 注册状态的不同观察;API 拒绝、分页超过上限或其他错误保留 `complete=false` 与失败阶段,不能解释为空闲。报告不连接节点、不重启服务、不清理文件、不取消 workflow,也不证明环境已恢复或共享 workflow 全局空闲。runner 服务恢复后,仍需重新执行资源检查及原有现场/并发门控。
+
+用 `matrix.py plan` 冻结当前 SSH 节点与所有适用操作分支,再根据本次只读 preflight 填写私有 `lab.json`。计划中的 `verified:false` 不可直接改成资源验证通过。H4 需要至少 24 个已分配独立盘端点;H6 需要独占主机及平台事件/恢复绑定。缺少资源的分支保留为 BLOCKED。
+
+填写内容须包含:
+
+- 本批 run ID、截止时间、独占服务/端口、jump route、实际主机名、每个磁盘的 mount UUID/major:minor、服务 unit/原 PID/数据根;
+- 与当前 validation lane 相同的不可变 candidate manifest、源码 commit、包与已安装二进制 SHA-256;当前 rc 路径与二进制 SHA-256;auto-testing commit;保存真实构建清单,不只填摘要字符串;
+- H1–H6 实際形状、每个对象 N/data/parity/R/W/lock quorum、pool/set/slot/盘 UUID;H4 至少 24 个实际盘端点;
+- 最长修复/重启/故障时限、对象/字节/请求/并发预算、所有危险动作的精确目标及恢复人;
+- 包含平台操作、独占故障设备、旧版本、KMS/tier 或原生屏障的实例,先提供能力及授权证据。
+
+`binding-check` 只是语法/租约检查,不是资源或包身份的远程证明:
+
+```bash
+python scripts/validation_heal/heal_probe.py --binding lab.json --out reports/binding-check.json binding-check
+```
+
+先在跳板只读核验 `hostname`、`findmnt -J`、`lsblk -J`、`ss -lnt`、二进制 `sha256sum`/`--version`、实际运行配置与 PID。保存输出为 preflight。租约没有发放、拓扑不符、端口占用、磁盘 UUID 漂移时在这里 BLOCKED,不先注入后跳过。
+
+### 独立实验服务的部署
+
+H1 使用当前 Heal 节点经核验的四个独立 mount,**另建** `/data/rustfsN/heal-validation//data` 和同级 `quarantine`,root-owned 0700;每个 `` 根写入包含 `runId` 的 root-owned `owner.json`。普通对象故障可以使用这些独立父盘下的 run 私有目录。B03 和 B04 还要求替换 target 和同组兄弟 endpoint 都能取得 candidate 认可的精确 mount lease;替换过程中必须证明旧 mount 与 quarantine 对服务不可读、新空设备挂到原 endpoint 后 candidate 取得新的 mount lease,并前后记录 mount ID、物理设备 ID 和 filesystem identity。独立文件系统或隔离 mount namespace 中的 bind mount 都可能作为实现方式,但仅创建 bind mount 不足以满足要求:adapter 还必须安全切换被替换 endpoint,且通过 candidate 对 root 和 sibling 物理设备身份的检查。Functional Chain 会先在 `stop_existing` 和候选 deb 下载/安装之前调用只读 `replacement-admission-preflight`,覆盖 H1/H2 endpoints;未通过时以 `BLOCKED` 保留 package/client provenance 和完整现场读数,不停服务、不碰已安装 package,也不创建远端 run namespace。`Fleet.deploy()` 在真正 reservation 前再重复该检查。缺少真实 endpoint mount 或已审核 mount-switch adapter 时应 BLOCKED;当前 runner 只把 run 私有子目录作为 endpoint,尚无此 mount-switch adapter,因此它现有的四个独立父盘不能证明 B03/B04 满足资源条件。不得延长等待时间或复制 quarantine 数据。先检查每个根不存在;已存在的根不能覆盖。复用 `rustfs_heal_test.sh` 的 candidate deb 下载、SHA 校验和安装流程,把已安装且摘要已核验的 `/usr/bin/rustfs` 放在本批 state 下;执行用例不需要 RustFS 源码 checkout。凭据文件为 0600。以下专用 unit 仍运行在当前 Heal 节点,使用已分配的 9000 测试端口:
+
+如果首次 gate 通过后、`Fleet.deploy()` 的 reservation-time 复检发现 mount 漂移,仍记录 `BLOCKED` 并保留已安装候选的 binary provenance;不得把该结果改写成产品 `FAIL`,也不得把它伪装成 pre-deployment 的 `NOT_REQUIRED_NO_MUTATION`。两阶段检查都必须跳过 reservation 和 fixture seed,并完成 cleanup。
+
+```bash
+# 在目标节点;所有变量来自已审核的 lab binding,不接受任意在线输入。
+sudo systemd-run --unit="$HEAL_UNIT" --property=Type=exec \
+ --property=PrivateMounts=yes --property=KillMode=control-group \
+ --property=TimeoutStopSec=30 --property=RuntimeMaxSec=3600 \
+ --property=MemoryMax=2G --property=CPUQuota=200% \
+ --property=WorkingDirectory="$HEAL_STATE" \
+ --property=EnvironmentFile="$HEAL_STATE/service.env" \
+ "$HEAL_STATE/rustfs" "${HEAL_DATA_ROOTS[@]}"
+```
+
+需重启的场景保留 unit 定义、env、二进制及原始 endpoint 参数,不使用 `--collect`;RuntimeMaxSec 按该批预算冻结,J05 必须单独分配长时服务。`HEAL_DATA_ROOTS` 是已审核的 Bash 数组,不从未审核字符串 eval。H2/H3/H4/H5 用各节点一致的 endpoint 序列/显式 pool 分组配置;启动后从每盘 `format.json` 读回,不能从启动参数猜拓扑成功。不得直接运行会清理旧 Pool 根的部署/reset 命令。
+
+### 命令约定
+
+下列函数在**跳板机**执行。`HEAL_BINDING`、`HEAL_REPORT` 是该实例私有绝对路径;`B` 是绑定 prefix 下的一个桶,`K` 为逻辑 key,`V` 为先前 ACK 的精确版本。每条命令的 label 唯一,已有证据拒绝覆盖。凭据只通过受控环境注入 `HEAL_ACCESS_KEY`/`HEAL_SECRET_KEY`/可选 session token,禁用 `set -x`。
+
+```bash
+set -euo pipefail
+umask 077
+hp() {
+ local label=$1
+ shift
+ python scripts/validation_heal/heal_probe.py --binding "$HEAL_BINDING" \
+ --out "$HEAL_REPORT/$label.json" "$@"
+}
+hw() {
+ local label=$1
+ shift
+ python scripts/validation_heal/heal_workload.py --binding "$HEAL_BINDING" \
+ --out "$HEAL_REPORT/$label" "$@"
+}
+B=$(jq -er .bucketPrefix "$HEAL_BINDING")
+RUN=$(jq -er .runId "$HEAL_BINDING")
+```
+
+特权故障命令在**经跳板连接的目标节点**执行。`ROOT` 是本例目标盘 `` 根;`NODE` 是已绑定 SSH alias;把该 SHA 的 `heal_fault.py` 放在 root-owned 的本批 state 下并核对摘要。不得从逻辑 key 拼 SSH shell。以下 `hf` 仅在该目标节点的交互 shell 定义:
+
+```bash
+hf() { sudo python3 "$HEAL_STATE/heal_fault.py" --root "$ROOT" --run-id "$RUN" "$@"; }
+hf init --bucket-prefix "$B" --lease-expires-at "$LEASE_EXPIRES_AT"
+```
+
+初始化只增加故障所有权标记,不创建/格式化数据盘。每盘只 init 一次。系统重启导致 mount ID 变化时暂停注入;先重新核对 UUID/major:minor/所有权并人工重新绑定,不绕过检查。
+
+## 3. 数据与客户端命令
+
+
+
+
+
+
+
+### 确定性夹具
+
+每个命令使用新桶,禁止在现有夹具桶上重复 seed 后称同一轮。
+
+```bash
+hw seed-db seed --fixture DB --bucket "$B-db"
+hw seed-dv seed --fixture DV --bucket "$B-dv"
+hw seed-dm seed --fixture DM --bucket "$B-dm"
+hw seed-ds seed --fixture DS --bucket "$B-ds"
+hw seed-dx seed --fixture DX --bucket "$B-dx"
+# 替换为候选确认的真实 inline/block 边界;可另外生成一桶:
+hw seed-boundary seed --fixture DB --bucket "$B-boundary" \
+ --count 128 --sizes "$BOUNDARY_SIZES"
+```
+
+DB 默认 128 项,包含 0/1/4095/4096/4097、1MiB±1、多MiB,非重复 SHAKE 数据;1MiB 不是预设产品阈值。DV 默认 4 key,每 key 三个版本,交替创建当前 marker。DM 有已完成三 part(前两 part 分别 5MiB+17、5MiB+93,末 part 1MiB+47)及两个独立 pending upload。DS 默认 257 特殊/多前缀 key;DX 默认 257 外部对象。自动 seed 不包括 suspended/null、特殊压缩、SSE/retention 等额外分支,按下列 S3 命令补充并保存 ACK。
+
+从 ACK 文件取得真正的 UploadId/VersionId/part ETag;不要在多个上传之间复用猜测的 ID。总字节预算包含所有版本/part,不足时先扩展经批准的预算,不自动跳过对象。
+
+后续命令的 `B` 必须改为该实例实际桶,例如 `B="$(jq -er .bucketPrefix "$HEAL_BINDING")-db"`,不要对没有创建的裸 prefix 做 inventory。根目录 owner 仍绑定原始 run prefix,以允许本run的各后缀桶。
+
+seed 完成后自动以完整 GET/版本清单对账确定性原文与创建 ACK,保存 `expected-fixture.json`、`baseline.json`;没有通过基线校验就不准进入注入。这个 GET 发生在故障前,不可拿 seed 命令监测 scanner/read-repair 故障窗口。
+
+
+
+
+### 单个 S3 操作与精确版本
+
+```bash
+hp put s3 --operation put_object --size 5243003 --seed "$RUN/new/1" \
+ --params "$(jq -nc --arg b "$B" --arg k "$K" '{Bucket:$b,Key:$k,ContentType:"application/octet-stream",Metadata:{nonce:"1"}}')"
+hp get s3 --operation get_object \
+ --params "$(jq -nc --arg b "$B" --arg k "$K" --arg v "$V" '{Bucket:$b,Key:$k,VersionId:$v}')"
+hp suspend s3 --operation put_bucket_versioning \
+ --params "$(jq -nc --arg b "$B" '{Bucket:$b,VersioningConfiguration:{Status:"Suspended"}}')"
+hp delete-version s3 --operation delete_object --expect-http 204 \
+ --params "$(jq -nc --arg b "$B" --arg k "$K" --arg v "$V" '{Bucket:$b,Key:$k,VersionId:$v}')"
+```
+
+请求复杂时 `--params-file` 使用 JSON。接口参数为 boto3 snake_case 操作名、原生 CamelCase 参数;只允许本 run 桶、受限操作,copy 源也必须在本 run。GET 只保存完整内容的 hash/长度,不把对象内容写入日志。PUT/Complete 超时记 resultUnknown,下一步用相同唯一 key/version/UploadId 查询解析,不静默重试。
+
+`--expect-http` 必须使用该请求预注册的精确值。例如 create/put/list 常见 200,delete/abort 常见 204,Range 206;负例按候选契约分别 304/403/404/405/412/416。CLI 返回 0 只说明该次预期 HTTP 匹配,永不宣称整个 case PASS。SSE-C 参数值可使用字面量 `$HEAL_SSE_C_KEY`,脚本只在内存替换,输出脱敏。
+
+
+
+
+### 全版本基线和最终核对
+
+```bash
+hp baseline inventory --bucket "$B"
+# 先完成触发归属和 PHYSICAL,再调用下面 GET 型检查:
+hp post-heal inventory --bucket "$B"
+hp s3-comparison compare --before "$HEAL_REPORT/baseline.json" \
+ --after "$HEAL_REPORT/post-heal.json"
+```
+
+inventory 分页遍历全部版本/marker,按每个 versionId GET 全内容、tags 和公开 metadata,再用分页 current LIST 交叉核对;对象上限 10,000、单对象 64MiB,批次字节预算来自绑定文件。更大对象场景需单独预注册流式工具和预算,不能截断后称全量。
+
+并发、生命周期、合法删除场景的预期清单会变化:人工先按逐请求 ACK 因果关系生成 `expected-post.json`,记录每项增删对应的 ACK 文件;再比较实际清单,不能直接拿静态基线判失败或拿实际输出复制成期望。未知写入未解析则不可通过。加密/归档对象先用授权 GET/restore 参数逐版本取证;默认 inventory 不会自动解锁、传 SSE-C 或 restore。恢复型 GET 必须保存密文/明文两层对应关系。
+
+
+
+### Range、HEAD、条件请求
+
+```bash
+hp head s3 --operation head_object --params-file request-head.json
+hp range s3 --operation get_object --expect-http 206 --params-file request-range.json
+hp if-none-match s3 --operation get_object --expect-http 304 --params-file request-if-none-match.json
+hp if-match-reject s3 --operation get_object --expect-http 412 --params-file request-if-match-bad.json
+hp range-outside s3 --operation get_object --expect-http 416 --params-file request-range-outside.json
+```
+
+JSON 都包含本例 Bucket/Key/可选 VersionId;另外分别添加 `Range:"bytes=5242879-5242882"`、`IfNoneMatch:<原ETag>`、`IfMatch:<错误但语法合法ETag>`、超出对象长度的 Range。时间条件用 `IfModifiedSince`/`IfUnmodifiedSince` 的 ISO 日期;从 HEAD 的真实 LastModified 推导早/晚两个控制值。验证 ContentRange、响应长度和从确定性原文计算的片段 hash,不能只验状态码。
+
+
+
+
+
+### SSE、Object Lock 和外部依赖
+
+```bash
+hp sse-s3-put s3 --operation put_object --size 5243003 --seed "$RUN/sse" --params-file sse-s3-put.json
+hp sse-kms-put s3 --operation put_object --size 5243003 --seed "$RUN/kms" --params-file sse-kms-put.json
+hp sse-c-put s3 --operation put_object --size 5243003 --seed "$RUN/ssec" --params-file sse-c-put.json
+hp locked-bucket s3 --operation create_bucket --params-file locked-bucket.json
+hp retained-put s3 --operation put_object --size 5243003 --seed "$RUN/lock" --params-file retained-put.json
+hp retention s3 --operation get_object_retention --params-file protected-version.json
+hp hold s3 --operation get_object_legal_hold --params-file protected-version.json
+hp reject-delete s3 --operation delete_object --expect-http 403 --params-file protected-version.json
+```
+
+SSE JSON 分别加 `ServerSideEncryption:"AES256"`、`ServerSideEncryption:"aws:kms",SSEKMSKeyId:<本例专用key>`、`SSECustomerAlgorithm:"AES256",SSECustomerKey:"$HEAL_SSE_C_KEY"`。SSE-C GET 也提供同一密钥,先用原请求证明可解密。所有外部 key/endpoint 是经过审批的测试租户,不复用生产依赖。
+
+锁桶 JSON 为 `{Bucket:...,ObjectLockEnabledForBucket:true}`。受保护 PUT 使用 `ObjectLockMode:"GOVERNANCE"` 或 `"COMPLIANCE"`、实际未来 UTC `ObjectLockRetainUntilDate`,或 `ObjectLockLegalHoldStatus:"ON"`。必须验证无 bypass 角色在保留期间真的不能删除;脚本拒绝 BypassGovernanceRetention=true。Legal hold 解除使用 `put_object_legal_hold` 的 `LegalHold:{Status:"OFF"}`,仅作用于本例版本。Compliance 等真实到期,不改主机时间、不内部删文件。
+
+KMS/tier outage 使用 NETWORK 在本例专用 net namespace 内仅阻断对应依赖的 IP/port,或仅撤销本例租户凭据;恢复原规则/权限后重试原 version。tier 缺失一个远端版本可通过远端本例桶 API 删除并保留ACK/原备份,但这是“远端对象丢失”负例,不等同网络中断。
+
+## 4. Admin 触发、状态与窗口
+
+
+
+实际服务配置要取证,但**不要转储包含凭据的全环境**。manual 模式记录以下非敏感字段:
+
+```text
+RUSTFS_HEAL_ENABLED=true
+RUSTFS_SCANNER_ENABLED=false
+RUSTFS_HEAL_AUTO_HEAL_ENABLE=false
+RUSTFS_HEAL_MRF_ENABLE=false
+RUSTFS_PUT_RENAME_EARLY_ACK_ENABLE=false
+```
+
+scanner/自动/MRF 场景按操作卡逐项开启相应字段,并冻结 scanner cycle/start delay/bitrot cycle。配置变更只改专用 service.env,重启本例 unit 后证明实际生效。read-repair 所需队列依赖按候选源码核实,不能为了“排除后台”关掉被测路径。
+
+
+
+
+
+
+
+### 原始 Admin 命令
+
+`SCOPE_PATH` 为编码后的 `/rustfs/admin/v3/heal//`;key 的每个字节按 URL 编码,root 为 `/rustfs/admin/v3/heal/`。JSON 中 pool/set 是实际零基索引,不能在 H5 固定填 0。例:
+
+```bash
+hp start admin --path "$SCOPE_PATH" \
+ --body '{"recursive":false,"dryRun":false,"remove":false,"recreate":false,"scanMode":2,"updateParity":false,"nolock":false,"readRepair":false,"pool":0,"set":0}'
+TOKEN=$(jq -er '.observation.response.clientToken | select(type=="string" and length>0)' "$HEAL_REPORT/start.json")
+hp status-001 admin --path "$SCOPE_PATH" --query "clientToken=$TOKEN"
+hp stop admin --path "$SCOPE_PATH" --query "clientToken=$TOKEN" --query forceStop=true
+hp force-start admin --path "$SCOPE_PATH" --query forceStart=true --body-file heal-options.json
+# 保留重复 query,真正送到服务端验证:
+hp invalid-duplicate admin --path "$SCOPE_PATH" --query clientToken=a \
+ --query clientToken=b --expect-http 400
+```
+
+WAIT 用有界循环保存每一响应,不仅保存最后绿色状态;普通 repair 900 秒、重启后 1800 秒是起始测试预算,须预注册,不是产品 SLO:
+
+```bash
+for i in $(seq 1 900); do
+ label=$(printf 'wait-%04d' "$i")
+ hp "$label" admin --path "$SCOPE_PATH" --query "clientToken=$TOKEN"
+ summary=$(jq -er '.observation.response.summary' "$HEAL_REPORT/$label.json")
+ if [ "$summary" != running ]; then break; fi
+ sleep 1
+done
+```
+
+循环退出后人工/候选 adapter 检查:execution completed、coverage complete;failed/unknown/attemptFailures=0;不溢出;逐对象 disposition 与设计匹配;非 dry-run 的 incarnation/版本/pool/set 正确。dry-run 只允许 observed,healed=0,随后无物理变更。负例以预注册错误/未完成契约判,不套正向 completed。
+
+`summary=finished` 或 HTTP200 单独不够;`truncated=false` 可以省略,null/true 不算 false;status.settings 是查询默认值,不代表原任务实际 scanMode。outcome 的 128 项窗口不是完整对象清单。当前 rc 会简化部分字段,以 raw Admin + 独立 manifest 为准。
+
+
+
+
+
+```bash
+hp background admin --path /rustfs/admin/v3/background-heal/status
+hp replacement admin --method GET --path /rustfs/admin/v4/heal/replacement-recovery
+"$RC_BIN" --json --no-color admin heal status "$RC_ALIAS"
+```
+
+rc 必须是本批冻结二进制,私有 RC_CONFIG_DIR/alias 只指向本例服务,stdout/stderr/exitCode 都留存。查询其他节点:直接运行 heal_probe 的全局 `--endpoint n1`/`--endpoint stable`;保存实际连接 peer 身份。健康状态需要 expected/responded/unknown 与所有 peer 覆盖,不允许缺 peer 时 idle 假绿。
+
+
+
+
+### 部分进度与增量游标
+
+```bash
+hp cursor-001 admin --path "$SCOPE_PATH" --query "clientToken=$TOKEN" --query sinceSeq=0
+NEXT=$(jq -er '.observation.response.nextSeq - 1' "$HEAL_REPORT/cursor-001.json")
+hp cursor-002 admin --path "$SCOPE_PATH" --query "clientToken=$TOKEN" --query "sinceSeq=$NEXT"
+```
+
+WINDOW 的必要证据是 running 且 `0
+
+### 权限和过期签名
+
+在专用服务先保存当前 rc 的 `admin user --help`、`admin policy --help`、`admin heal --help`。使用候选 IAM 支持的 Admin Heal action 创建最小策略(从候选 `HealAdminAction` 的实际字符串导出,不猜拼写);另建明确不含该 action 的角色。示例用户/策略动作按本批 rc help 的参数顺序执行并保存原始回执,凭据用私有输入通道,不开 shell trace。
+
+已核对参考候选 action 为 `admin:Heal`,最小策略主体为 `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["admin:Heal"],"Resource":["arn:aws:s3:::*"]}]}`;RustFS policy parser 要求带类型的 ARN,裸 `"*"` 会被拒绝。将这个权限仅赋予本例临时身份,不改共享管理员策略。新候选仍需核对 action 和 rc attach 语法。
+
+```bash
+hp anonymous-start admin --path "$SCOPE_PATH" --auth anonymous --expect-http 403 --body-file heal-options.json
+hp bad-signature admin --path "$SCOPE_PATH" --auth bad-signature --expect-http 403 --body-file heal-options.json
+# 无权限/最小权限实例:更换本次调用环境中的 HEAL_ACCESS_KEY/HEAL_SECRET_KEY,执行相同请求。
+```
+
+过期签名:保存一次已签请求,在签名有效期外重放到**相同本例地址/路径/请求体**,仅等待客户端签名超期,不改变服务器时钟。先用新签名同请求做控制。时限按候选签名校验窗口预注册;签名本身含敏感身份,原请求存私有0600文件,只归档脱敏结果和请求摘要。对 start/status/stop 都测试;token 不能替代权限。结束时通过正常 IAM API 删除本例用户/策略并验证旧凭据失效。
+
+## 5. 物理证据与故障后端
+
+
+
+
+### 真实布局、角色和物理快照
+
+在目标节点逐盘读取本例 `data/.rustfs.sys/format.json`(只读)及 mount UUID/major:minor,保存 pool/set/this 映射。不能把同一设备的多个目录作为独立盘。逐对象 xl.meta 使用与候选相同源码的只读解析器:
+
+```bash
+python scripts/validation_heal/prepare_heal_meta_inspector.py --source "$CANDIDATE_SOURCE" \
+ --commit "$CANDIDATE_COMMIT" --out "$INSPECTOR_BUILD"
+# 仅批准的构建机;生成独立 helper 的锁文件后冻结,和服务构建清单分开。
+cargo build --release --manifest-path "$INSPECTOR_BUILD/Cargo.toml"
+sha256sum "$INSPECTOR_BUILD/Cargo.lock" "$INSPECTOR_BUILD/target/release/heal-meta-inspect"
+"$INSPECTOR_BUILD/target/release/heal-meta-inspect" "$XL_META_EVIDENCE_COPY" "$B" "$K"
+```
+
+解析器不输出用户metadata或密钥,只输出版本、data_dir、parts、EC参数/分片角色。首先对同批健康样本和损坏样本分别证明成功/拒绝;不支持的版本不得换别的版本解析器后宣称同候选验证。源 checkout 必须为固定 SHA 且无已跟踪修改。
+
+```bash
+# 目标节点。所有输出重定向到本例证据文件,不能覆盖已有阶段。
+hf inspect --path "$TARGET_RELATIVE_PATH"
+hf snapshot --bucket "$B"
+```
+
+快照对整桶每个文件保存长度/hash,目录描述符逐层拒绝 symlink/hardlink/不同mount ID(含同设备bind mount);最多10,000文件/8GiB,每文件64MiB。四阶段 before/fault/after/restart 每盘各一份。外部part与**同一个角色、同版本的健康原片**字节完全相同是独立重建对照;数据应不重复且四片摘要不同。不得读取 quarantine 来辅助服务器重建。
+
+xl.meta 可合法重写;比较解析后的版本/引用,而非要求所有内部字节不变。允许的 xl.meta.bkp 必须属于同目标data_dir且摘要等于原xl.meta,不能泛化豁免任意额外文件。普通解析模式不验证 payload;`--verify-part XL_META BUCKET KEY PART_FILE VERSION_ID PART_NUMBER` 按候选 metadata 的实际版本、part 和 legacy/current framing 逐块验证 HighwayHash,同时拒绝短读、尾随字节和不支持的格式。分片执行器还逐项要求所有同版本片与故障前原始同角色长度/hash 完全一致,并确认基线全文、真实 set slot 及不重复分片摘要。它不支持加密/压缩、inline 或改变编码形态的物理验收;这些用例仍保留各自工具要求。
+
+
+
+### 单文件故障(已有脚本)
+
+先停止本例写入并让测试进程静止;格式映射已确认后选择精确物理路径,不能只找第一个part。注入/恢复时 `--service-quiescent` 是操作者确认,工具不会替你停止共享服务。
+
+```bash
+hf apply --path "$TARGET_RELATIVE_PATH" --fault-id C02-data-middle \
+ --kind bitflip --offset "$BYTE_OFFSET" --expected-sha256 "$BASELINE_SHA256" --service-quiescent
+# 其他实例各用新故障ID:--kind missing / truncate / zero / header
+hf snapshot --bucket "$B"
+```
+
+`truncate` 保留一半;`header` 翻转前32字节;bitflip 可指定任意有效offset;zero 若不会改变字节则拒绝。先持久化intent/原件,再变更,再见证;重复apply拒绝覆盖。备份在同run隔离目录,绝不在RustFS数据根中。
+
+文件故障为 F1/F2/对象级F3,不是整盘故障。helper 明确禁止 `.rustfs.sys`、format、IAM、任意shell或设备操作。用例需要系统记录/特殊格式时走下面人工白名单后端。
+
+
+
+
+
+### 格式/系统记录故障(人工白名单后端)
+
+先 `PROCESS stop`,从候选源码/真实task定位**本例**精确记录,生成 `record-plan.json`:绝对路径、run/task/bucket/incarnation、盘UUID、原sha/size、变更offset或替换源sha、允许新增/删除字段、备份和undo。第二人/批准的操作人逐项审核;禁止递归 `.rustfs.sys` 修改。
+
+可操作的配方:
+
+1. **near-tail/长度不符**:复制目标part到本例quarantine并校验sha;在该目标副本上 `truncate -s "$NEW_SIZE" "$TARGET"`,其中 NEW_SIZE=原size−1或指定长度。核对新size/hash和原metadata未变。停止服务期间执行,目标路径来自已审核清单。
+2. **外来format/set**:同run的第二个隔离小集群正常启动产生format,停止两服务。仅把其format安装到本例空替换根,不改健康盘;记录原/外来id、this、sets。启动后期待安全拒绝,原外来format不能被改写。
+3. **未来format版本**:在本例替换根的format JSON副本,用 `jq` 只更改已核实的版本字段为不支持值;原件与变更diff归档。未来对象/checkpoint schema 按候选编解码结构更改对应版本头,不能随机翻位后称“未来schema”。如果存在校验,必须重算后证明结构可解析且schema未知;否则只计坏校验实例。
+4. **冲突代次/quorum**:串行合法写V1/V2或两个incarnation,分别保存全套本例副本;停服务后按预注册盘分配表安装各自不足R的集合。重启前用FORMAT逐盘核对,证明没有任一同版本quorum。
+5. **checkpoint/MRF坏记录**:只读保存 `.rustfs.sys/root-heal-.json` 与候选实际journal路径;确认属于本run。截断、替换为从本run早期阶段保留的陈旧代次、或单独更改校验字段。不得凭目录名删除整个queue。写失败通过IO_FAULT构造,不改权限后未经见证就称失败。
+6. **bucket metadata**:只列入对应本例桶的记录;建立同run健康备份,分别缺失/截断后重启/heal。原策略、tags、版本/lock配置通过S3核对,其他系统文件摘要不变。
+7. **inline偏移**:用匹配候选的codec在证据副本中定位目标version的inline map/value;保存解析offset/长度及反向映射,之后只修改该value。不会解析时不能任意翻xl.meta头冒充inline损坏;改用已明确定义的“坏metadata”实例,原inline实例仍未覆盖。
+8. **orphan/dangling**:通过正常完成/覆盖/删除形成历史目录,FORMAT证明所有活version/MPU引用后再测试remove。宽限期按真实时间/本例服务配置等待,不改主机时间。共享引用控制必须保留。
+
+上述人工操作每一步均需精确原件备份、变更前后hash、操作返回值与复查;不能直接 `rm` 记录。用 `mv -n` 同盘移到隔离路径可保留原件,但必须核实源消失/目标摘要相同,因为 `mv -n` 跳过未必报错。恢复时先保留失败后新记录,若有新ACK/代次,禁止把旧metadata拷回去。
+
+
+
+### 逻辑盘不可达、只读、权限
+
+仅在已设置 PrivateMounts 的**本例服务 mount namespace**使用;先证明目标PID仍属于本例unit、`/proc/$PID/ns/mnt` 与host不同,且目标挂载非shared。不能进入host namespace卸载 `/data/rustfsN`。原理参考 [Linux nsenter](https://www.man7.org/linux/man-pages/man1/nsenter.1.html)。
+
+只读配方(在目标节点,DATA为本例完整数据根):
+
+```bash
+sudo nsenter -t "$HEAL_PID" -m -- mount --bind "$DATA" "$DATA"
+sudo nsenter -t "$HEAL_PID" -m -- mount -o remount,bind,ro "$DATA"
+sudo nsenter -t "$HEAL_PID" -m -- findmnt -T "$DATA" -o TARGET,SOURCE,OPTIONS
+# 以服务实际UID在该namespace做本例临时文件写探针,必须观察EROFS。
+# 解除仅此额外的bind层:
+sudo nsenter -t "$HEAL_PID" -m -- umount "$DATA"
+```
+
+不可达用经批准的该服务 mount namespace 故障层覆盖此数据根(返回EIO的专用device-mapper设备,见IO_FAULT);不能把空目录覆盖当作掉盘,因为可能触发新盘初始化。权限实例可使用本例非root UID +精确目录ACL拒绝,在相同UID实测EACCES;root服务单纯chmod不算有效注入。恢复后重新验证mount/磁盘身份和读写可达;在 namespace 内 helper 的跨mount拒绝是正确保护,不应关闭它。
+
+
+
+### 新逻辑盘替换
+
+停止本例unit及写入,校验 ROOT/data 与owner/run/mount;将**精确 data 根**同盘移至本例 quarantine 的唯一 old-disk 目录,新建同位置空data根。其他盘不动。重启后读取format,确认新disk UUID与slot;格式初始化失败要如实记录,不手动补出一个UUID。
+
+```bash
+# 仅已审核的本例 ROOT;先验证备份目标不存在,不递归删除任何目录。
+sudo mv -n -- "$ROOT/data" "$ROOT/quarantine/$FAULT_ID-old-disk"
+sudo install -d -m 0700 -- "$ROOT/data"
+```
+
+检查旧根确实移走、摘要清单完整、服务无法访问quarantine;再正常启动。rebuild期间不得从old-disk复制part。失败救援时先停止unit并把新失败data根移至另一个保留目录,再恢复旧根;该恢复明确为rescue,不是heal结果。真实设备detach/attach仅属于另行批准的H6,不由此命令模拟。
+
+
+
+
+### 进程停止、崩溃、重启、升级
+
+```bash
+sudo systemctl show "$HEAL_UNIT" -p MainPID -p ExecMainStartTimestampMonotonic -p FragmentPath -p ActiveState
+sudo systemctl stop "$HEAL_UNIT"
+sudo systemctl start "$HEAL_UNIT"
+# 独立 crash 实例,必须先 WINDOW、记录被杀的实际MainPID:
+sudo systemctl kill --kill-whom=main --signal=SIGKILL "$HEAL_UNIT"
+sudo systemctl show "$HEAL_UNIT" -p ExecMainCode -p ExecMainStatus -p Result
+sudo systemctl reset-failed "$HEAL_UNIT"
+sudo systemctl start "$HEAL_UNIT"
+```
+
+每次操作前核对 MainPID/exe 摘要/starttime,拒绝0或已替换PID,不 `pkill rustfs`。保存 `/proc/sys/kernel/random/boot_id`,SIGKILL前后boot_id相同,不能说断电。
+
+UPGRADE 对本例节点逐一stop,用已经验证的第二个optimized release二进制替换该unit的exec路径(保留旧二进制),原env/endpoint/data不变,start后核对每个peer实际版本。旧新两份构建清单、包sha、库格式兼容表都须预注册;未知格式禁止直接回滚,改测安全拒绝。精确升级期间责任按CHECKPOINT验证,不依赖版本字符串判断兼容。
+
+
+
+### 测试端口分区 / 依赖故障
+
+优先预分配专用net namespace,证明它与host不同;需要业务节点连接的路由已配置,SSH仍走host。只对本例API/RPC端口或本例KMS/tier endpoint加规则。先保存规则/路由,设置独立超时恢复任务。例:
+
+```bash
+sudo nsenter -t "$HEAL_PID" -n -- iptables -w -I OUTPUT 1 \
+ -d "$TEST_PEER_IP" -p tcp --dport "$TEST_SERVICE_PORT" \
+ -m comment --comment "$FAULT_ID" -j DROP
+sudo nsenter -t "$HEAL_PID" -n -- iptables -w -nvL OUTPUT --line-numbers
+# 相同namespace、完整相同规则删除,不flush任何链:
+sudo nsenter -t "$HEAL_PID" -n -- iptables -w -D OUTPUT \
+ -d "$TEST_PEER_IP" -p tcp --dport "$TEST_SERVICE_PORT" \
+ -m comment --comment "$FAULT_ID" -j DROP
+```
+
+双向分区分别在绑定双方namespace应用,不能扩大到22、管理平面或host OUTPUT。记录丢包计数与双端探针。无独立net namespace或专项host规则批准时该实例BLOCKED,不能全局断网凑结果。
+
+
+
+
+
+### 慢IO/EIO/容量与锁窗口
+
+此组**只在预分配独占故障盘/有硬上限的专用loop设备**操作,绝不对 `/dev/nvme*` 共享生产设备运行。操作前记录backing file、loop/device-mapper名字、major:minor、mount UUID、上限和唯一FAULT_ID。设备创建/format需要本批单独的资源批准,不是case默认授权。
+
+已分配专用 `HEAL_DM_NAME` / `HEAL_BACKING_DEV` 的device-mapper配方:
+
+```bash
+sudo dmsetup table "$HEAL_DM_NAME"
+sudo dmsetup suspend "$HEAL_DM_NAME"
+sudo dmsetup reload "$HEAL_DM_NAME" --table "0 $HEAL_SECTORS delay $HEAL_BACKING_DEV 0 200"
+sudo dmsetup resume "$HEAL_DM_NAME"
+# EIO实例用完全返回error的表;只用于专用故障层:
+sudo dmsetup suspend "$HEAL_DM_NAME"
+sudo dmsetup reload "$HEAL_DM_NAME" --table "0 $HEAL_SECTORS error"
+sudo dmsetup resume "$HEAL_DM_NAME"
+```
+
+恢复reload预先保存的精确原table,不猜linear偏移;在同服务namespace做定长读写见证(记录耗时/errno),并验证源设备可用。块设备EIO不等同应用层短读,短读实例使用FAULT truncate和真实读错误证据;若需“无内容变化的短读”必须有已分配的faultfs后端,另记能力条件。
+
+ENOSPC仅写本例**硬上限卷**中的占位文件,先 `findmnt`/`df -B1` 和 inode计数确认不会耗尽外层XFS;按上限分块填充至返回ENOSPC并记录实际服务写失败。inode实例用创建空文件耗尽该卷的inode预算,不能在共享根创建无限文件。释放只删除已列举的本例占位文件或回收专用卷,不使用宽目录递归删除。
+
+桶配额使用冻结rc的 `quota --help`,对本例桶 `quota set`/`quota info`;前后查询证明限额真实生效,结束恢复原配置。禁止把客户端本地限速当服务器quota。
+
+```bash
+"$RC_BIN" --json --no-color quota info "$RC_ALIAS/$B"
+"$RC_BIN" --json --no-color quota set "$RC_ALIAS/$B" 64M
+# 只在原配置没有quota且本例结束时清除;否则恢复原限额:
+"$RC_BIN" --json --no-color quota clear "$RC_ALIAS/$B"
+```
+
+LOCK_WINDOW:慢IO层作用于一个实际持有对象写锁的请求,另发同key heal/PUT。用日志/原生runtime fence证明锁获取、超时、调用方退出、在途提交退出顺序;没有可观察锁窗口则黑盒实例为NOT_OBSERVED。客户端断开可由有界 `timeout` 结束**本次请求进程**,不杀服务;之后继续采集服务端在途状态。精确所有权证明依赖NATIVE,不能从一次超时推导锁已释放。
+
+
+
+### 强制停机
+
+只对H6已批准的精确Azure资源ID执行,先核验该VM独占、没有其他服务、磁盘持久化策略及带外恢复;记录当前boot_id和平台状态:
+
+```bash
+az vm get-instance-view --ids "$APPROVED_EXCLUSIVE_VM_ID"
+az vm stop --ids "$APPROVED_EXCLUSIVE_VM_ID" --skip-shutdown
+az vm start --ids "$APPROVED_EXCLUSIVE_VM_ID"
+az monitor activity-log list --resource-id "$APPROVED_EXCLUSIVE_VM_ID" \
+ --start-time "$RUN_STARTED_AT" --end-time "$RUN_FINISHED_AT"
+```
+
+这是真实VM强制停止,不证明物理控制器掉电。物理主机power-cycle必须使用该独占机器已批准的带外控制台,记录BMC事件和电源周期;不能给未知机型编造通用命令。无授权不执行,也不把SIGKILL替代成通过。
+
+## 6. 并发、Pool、ILM、复制及原生证据
+
+
+
+
+
+
+### 有界负载与观测
+
+`trace.json` 是按序的S3操作数组,例如:
+
+```json
+[
+ {"operation":"put_object","params":{"Bucket":"rv-heal-20260910-a01","Key":"race-${worker}"},"size":5243003,"expectHttp":200},
+ {"operation":"head_object","params":{"Bucket":"rv-heal-20260910-a01","Key":"race-${worker}"},"expectHttp":200}
+]
+```
+
+```bash
+hw writer run --trace trace.json --rounds 100 --workers 2 --interval 0.1 --duration 300
+```
+
+同worker串行;不同worker用不同key可明确先后,测试真正同key竞争时人工按ACK/不确定结果建立期望。`${round}`、`${worker}` 只做JSON字符串替换,不eval。上传内容由run/worker/round/step确定,大小/摘要写入ACK。每次ACK立即独占写文件;意外错误停止后续入场并保留在途结果。不因服务返回错误而无限重试。
+
+长稳分支增加已批准 `--duration` 至86400/259200、足够rounds及限速,并核对实际elapsed,提前耗尽rounds/字节预算不算跑满窗口;每批最多1,000,000请求/16worker/64GiB写入预算,可按预注册多个分段运行并保留连续时序。不可通过事后拼接零散短跑冒充连续24h。
+
+Admin队列压力使用同样的限量并发方式调用hp(例如shell后台最多4个固定任务),先冻结请求集合,独立label保存每个返回;不能把S3负载当Admin admission覆盖。
+
+```bash
+# 在目标节点按固定频率采样,时间窗口与本例一致;不要采集全环境凭据。
+sudo journalctl -u "$HEAL_UNIT" --since "$RUN_STARTED_AT" --until "$RUN_FINISHED_AT" --no-pager
+sudo systemctl show "$HEAL_UNIT" -p MemoryCurrent -p TasksCurrent -p CPUUsageNSec
+pidstat -dru -p "$HEAL_PID" 1 60
+iostat -dx 1 60
+```
+
+同时记录FD数量、目标IO、队列/持久记录及临时空间;工具未安装先预检阻断所需性能证据,不跑完才省略。性能ABBA每组四轮、至少三组:warmup、并发、内容、缓存/初始队列、负载窗口和阈值固定。分别统计GET/PUT P95/P99、错误率(所有失败/超时都在总分母)、修复吞吐、CPU/IO。没有预注册门限只能出测量报告,不能记性能通过。
+
+
+
+### Pool交互
+
+使用**本例H5服务**的rc alias,已有13项Pool回归结果仅为前置控制,不直接调用其会reset旧fleet的部署脚本。扩容/移除拓扑复用已审阅的endpoint分组方案,但根目录替换为本例heal命名空间。所有poolId从实际status和format映射获得。
+
+```bash
+"$RC_BIN" --json --no-color admin rebalance start "$RC_ALIAS"
+"$RC_BIN" --json --no-color admin rebalance status "$RC_ALIAS"
+"$RC_BIN" --json --no-color admin rebalance stop "$RC_ALIAS"
+"$RC_BIN" --json --no-color admin decommission start "$RC_ALIAS" "$POOL_ID" --by-id
+"$RC_BIN" --json --no-color admin decommission status "$RC_ALIAS"
+"$RC_BIN" --json --no-color admin decommission cancel "$RC_ALIAS" "$POOL_ID" --by-id
+"$RC_BIN" --json --no-color admin decommission clear "$RC_ALIAS" "$POOL_ID" --by-id
+```
+
+不是把以上所有操作依次跑完:按H01–H06操作卡在相应窗口执行。每次保存rc stdout/stderr/exitCode、相关Admin状态及前后manifest。退役写入限制沿用既定产品契约;仅survivor验证时源必须真实停机且配置移除。#2422/#2423的临界点是否实际触发单独登记。
+
+
+
+
+### ILM、远端tier、复制
+
+使用本仓库已实现lane作为**控制实验**核对候选功能,并在本例独立服务重新建立专用桶/远端租户。配置JSON只含本例资源,API操作如下:
+
+```bash
+hp ilm-put s3 --operation put_bucket_lifecycle_configuration --params-file lifecycle.json
+hp ilm-get s3 --operation get_bucket_lifecycle_configuration --params-file bucket.json
+hp restore s3 --operation restore_object --params-file restore-version.json --expect-http 202
+hp replication-put s3 --operation put_bucket_replication --params-file replication.json
+hp replication-get s3 --operation get_bucket_replication --params-file bucket.json
+```
+
+`lifecycle.json` 使用已验证候选的真实期限;规则ID含run,Filter仅本例prefix,分别配置Expiration/NoncurrentVersionExpiration/Transition。不要用未合并的分钟级测试hook冒充release。远端tier的初始化依照当前rc `ilm tier add --help` 和已有 `scripts/validation_ilm/ilm_behavior.py` 的实际调用,填写本例endpoint/bucket/region与受控凭据;原始凭据不归档。
+
+复制配置含本例role/destination ARN、Filter和DeleteMarkerReplication;先证明对端版本映射/删除语义。resync使用本批rc `replicate resync --help` 所支持的实际命令,保存启动ID和两端状态。观察真实重叠窗口;没有tier/第二端,相关分支BLOCKED而非删除该项。结束通过API删除本例lifecycle/replication配置,远端本例数据按版本清单清理,不改共享服务规则。
+
+已核对参考rc的resync操作(仍须新批help核验):
+
+```bash
+"$RC_BIN" --json --no-color replicate resync start "$RC_ALIAS/$B" \
+ --target-arn "$TEST_TARGET_ARN" --reset-id "$RUN" --yes
+"$RC_BIN" --json --no-color replicate resync status "$RC_ALIAS/$B" --target-arn "$TEST_TARGET_ARN"
+```
+
+
+
+### 精确屏障与原生回归补证
+
+F9类窗口不能靠普通release黑盒保证命中。使用同源码SHA的**独立native回归二进制**,绝不换掉被测optimized release服务。先列出真实测试并固定完整测试名、crate、源码位置和预期屏障事件:
+
+```bash
+cargo test -p rustfs-heal -- --list
+cargo test -p rustfs-ecstore -- --list
+cargo test -p e2e_test -- --list
+# NATIVE_TEST必须逐字来自上面列表,日志必须显示恰好1项且屏障命中:
+cargo test -p "$NATIVE_CRATE" "$NATIVE_TEST" -- --exact --nocapture
+```
+
+已有replacement回归函数包含 `test_cluster_root_heal_recovers_remote_shards_after_background_target_restart`、`..._crash`、EC8+4和multi-set版本,完整限定名从该commit --list取得。它们默认创建本地E2E临时目录,**不是远端真实独立盘部署器**;不通过环境变量把这些会清理临时目录的测试指到现有fleet。原生锁/代次/checkpoint测试按所选操作卡在同commit源码中找到精确回归和屏障标记后运行,不存在对应屏障的分支要保留“需源侧增加确定性测试”的能力缺口,不能给计划生成器喂手写JSON声称measured。
+
+归档 candidate SHA、native测试源码SHA、完整命令、测试数量、exitCode、运行时间及屏障事件。native与fleet分别计分;缺失精确native证据不影响已经完成的黑盒分支真实性,但不能宣布该case所有分支齐全。
+
+## 7. 恢复、报告和安全交还
+
+
+
+先保存出错时的状态、日志和PHYSICAL,停止本例负载/任务/进程;再按fault receipt撤销规则/只读层/替换根。对已失效lease允许受限恢复,不允许继续注入。单文件恢复:
+
+```bash
+hf recover --fault-id C02-data-middle --service-quiescent
+```
+
+只恢复同run的备份,原件摘要不符则拒绝;目标如果已经是原始正确字节不重写;如果变成既不是原始也不是注入后字节(可能有新ACK或合法metadata重写),拒绝盲目覆盖,交人工保留现场。恢复工具输出 `physicalRepairPassAllowed=false`;任何 `restoredFromBackup=true` 都不能作为产品补片证据。
+
+For an externally cancelled Functional Chain, `validation_cleanup_run_id` accepts only a completed run on the same workflow branch. F07 recovery supports the bounded H2/DV stale-generation boundary after four missing-part injections and before Admin admission: all 18 earlier branches must have complete cleanup receipts, and all 282 fixture requests must have matching successful SDK acknowledgements. Any additional action, unknown write, lost root or physical drift fails closed. Recovery stops the owned services, restores only the four original part backups, and verifies the complete original business files and disk formats. It then retains every data root and private state directory by the existing non-overwriting, same-filesystem archive protocol, with a durable inventory bound to each node's original business-scene digest. Partial restoration and partial archive moves are retryable. `ISOLATED` releases the active namespace while preserving backups; it never rewrites the original cancelled/RUNNING result, restores old system records, or claims canonical PASS.
+
+强制故障设置第二条恢复通道/超时撤销;本地 finally 不是网络或进程被杀时可靠的唯一恢复办法。退出前枚举本例网络规则、bind层、dm表、测试unit、临时IAM和未完成MPU,逐条撤销并复核。保留失败数据/backup,retention未到期则保留专用资源;记录剩余路径/到期安排。未恢复为 DIRTY/QUARANTINED,不继续下一用例。
+
+每个**实际参数实例**都填写人工结论和证据索引,不只填一个基础case总绿:
+
+```text
+preflight/ 基础设施/包/rc/harness/lease/拓扑/预注册参数
+baseline/ S3版本清单、物理基线、ACK、桶配置
+fault/ 精确plan、intent、原件摘要、故障有效性见证
+trigger/ 原始Admin/GET/后台来源及活动窗口
+control/ token/epoch/coverage/counter/负向错误/游标原始响应
+physical/ after/restart分片、metadata映射、独立重建对照
+s3/ 所有应存/应删版本与ACK解析、完整内容和配置
+recovery/ 故障解除、停止服务、retained路径与安全交还
+logs/ 全部相关节点的有界日志/资源/平台或native事件
+```
+
+人工结论为 MANUAL_PASS/FAIL/INFRA_ERROR/BLOCKED/NOT_OBSERVED,列评审人、每条断言及引用。`evidence.json`包含runId、instanceId、九类evidence的数组,每个引用为相对于该实例目录的`path`和`sha256`。全部分支完成才能评审基础case;缺资源/窗口未命中/证据不全都不是PASS。
+
+```bash
+python scripts/validation_heal/matrix.py audit --plan reports/validation-heal-84-plan.json \
+ --evidence-dir reports/heal-84 --out reports/heal-84-evidence-audit.json
+```
+
+audit检查完整性/路径/摘要,输出`EVIDENCE_COMPLETE_REVIEW_REQUIRED`仍不是产品通过报告。原始失败批次永久保留,修复后新批次另记。本文提供全84项的可操作方案,但实际机器、专用依赖和native屏障的可用性必须单独核验,不能把“方案就绪”改写成“环境就绪”或“已实测”。
+
+
+自动正向夹具固定使用独立分片承诺配置;`integrity-fleet-attestation` 记录本例 endpoint/节点/候选摘要,启动前逐节点 reserve 校验候选,启动后实际进程环境读回两项开关。此配置只作用于 run-owned 服务。默认 legacy 配置的失败批次独立保留;保护模式不代表旧版本混部或默认环境能力。D07 的条件请求以服务器 HEAD 的 LastModified 和基线 ETag 冻结预期,不改主机时间;304/412/416 不能包含成功 payload 或未知传输结果。
+
+
+J06 的自动入口为 `heal.performance-abba`。执行器在部署前冻结三组 ABBA、每阶段 30 秒、2 并发、4 请求/秒、GET/PUT 各半与完整阈值;每阶段重启同一服务并按固定次数预热。每个 B 阶段的真实部分修复窗口必须包围 GET 和 PUT,否则整例不能 PASS。A 阶段持续检查完整 idle 与物理不变。所有排定请求(包括失败、未知和错过 admission 的请求)均计入分母;GET/PUT 独立报告 P95/P99,三组配对差异、实际修复分片吞吐与服务 CPU/IO/RSS/FD 分别留证。阈值保存在 `scripts/validation_heal/performance.py` 的 `PROTOCOL`,修改阈值必须产生新的 harness commit 和新验证记录;此入口不缩短 J05 的 24h/72h 要求。
+
+
+A02 的自动入口为 `heal.admin-scope-isolation`。H2 的专用 `scope-isolation` runtime 使用当前四节点/十六盘,按盘序交织节点并设置 `RUSTFS_ERASURE_SET_DRIVE_COUNT=8`,要求真实两个 set 各跨全部四个节点。普通执行器仍只接受原单 set。两个桶的完整 ACK/实际物理 set 清单冻结后,root/bucket/prefix/object/pool-set 各自新部署、注入选中与控制故障。完整物理和 streaming 证明必须先于 GET 和控制备份恢复。H1 保留原四盘单 set,明确记录不存在未选 set 的适用边界;不能拿同盘目录冒充第二个 set。未限制选择器的原始 receipt 在该固定候选中允许 null,单独保存实际物理映射,显式 pool/set 则必须严格匹配原始字段。
+
+### I04 durable responsibility during queue pressure
+
+The live `queue-pressure` permission fault targets only `-durable` on the first owned drive. The other four queue buckets and `.rustfs.sys` retain their original permissions. The live arm derives this fixed suffix from the root-owned reservation; callers cannot supply another bucket or path. Flapping keeps its separate whole-drive fault and F07 keeps its original dedicated bucket scope.
+
+Before the three degraded-write ACKs, the same original service/mount-namespace identity must observe business read/write `EACCES` and successful system metadata read/write controls with DAC bypass removed. A whole-drive refusal or unavailable system control is rejected before those writes. Full, Merged, Dropped, all three committed MRF responsibilities, exact source-task retirement, complete six-bucket snapshots and original `769/129/129/129` fixtures remain required. Blocking the MRF journal cannot isolate queue-Full retention. A corrected fault does not establish canonical PASS until a separate complete live run is independently audited; original failed results remain retained.
+
+Complete the owned permission probes before admitting the finite pressure workload, so SSH setup does not consume its queue window. The later actual MRF Full event after the SDK ACKs remains mandatory; scheduler pauses, deadline increases and a prior Admin Full response cannot replace that event. Terminal object identities use the existing source-reviewed mapping between Admin's all-zero UUID and S3's `null` version. Real version UUIDs, incarnation/set boundaries, complete counters and independent physical/S3 proofs remain exact; untyped version identities are rejected.
+
+Owned Heal services request a per-unit journal budget of 1,000,000 messages per second, then read back both effective properties and record the systemd version before source attribution. Missing, zero or different settings are rejected; merely passing launch arguments does not establish effective configuration. Host-wide journal settings remain unchanged. Cursor continuity alone does not establish complete source attribution: the reader retains journald's original dropped-message notice and marks that page incomplete, including notices identified by the stable journal message ID. Source observers reject the page before accepting any task events. Existing page/event limits, resource ceilings, workload counts and deadlines remain enforced; absent source receipts in a rate-limited original run cannot establish either successful repair or a product responsibility-loss diagnosis.
+
+### Exact-source native race subsets
+
+`race_native.py` builds and runs the reviewed tests in `race_native_contracts.json` on a hosted tools runner. The candidate checkout must match the exact commit, tree, lock and reviewed Rust files; the producer and registry must match the exact auto-testing checkout. The build uses a fresh target directory and rejects compiler wrappers or injected Rust flags. Each selected regression runs in a separate process with one exact nonignored test. Original logs, executable digest and ordered barrier milestones are retained. These artifacts are separate from `native_regressions.py`; existing F07/C11 artifact contracts remain unchanged.
+
+The first registered source is `6afdec9fe1b0b88007f2e5b445c19bf73d51d3ca`. G01 includes real Heal during committed overwrite cleanup in four disk/parity/mode scenarios and the existing stale read-repair final-rename test. The G02 subset covers the version-specific shared Heal owner and queued successor PUT. The I05 subset reuses the actual cancelled physical unlink owner and stale read-repair fences. These legacy subsets do not complete G02 or I05; I05 also needs its TCP disconnect, timeout and file-descriptor matrix.
+
+Source `bd0671ba781d8ebf509f4f03533228dfb44496f1` adds complete G02 and G03 native groups. The versioned PUT group requires both the historical lock owner and an actual new-version commit while historical reconstruction is paused. The deletion group requires exact historical and current version deletion before releasing stale reconstruction. Both use real `4/2` and `16/4` local geometries, physical write-tail drain, exact surviving encoded shards and complete version bodies. Its G01 fixture pauses every shuffled cleanup slot and checks every new encoded shard again after cleanup and before GET. These source contracts require fresh same-source hosted artifacts; local tests or an older source's artifacts cannot supply them.
+
+Source `8380466f3a27c3c3ef4115084178a0ab99a78f32` additionally requires the complete G04 marker/batch-delete group. In each geometry, creating a marker, removing an exact marker, deleting two exact data versions with separate item receipts and deleting an unversioned object all commit while older reconstruction is paused. The foreground physical tail drains before release. Every disk must retain the exact data/marker version set without duplicates, every surviving encoded shard and complete exact/current bodies; removed markers and data directories cannot reappear. Its 24 ordered milestones supplement the existing G01/G02/G03 groups.
+
+`race_native_evidence.py` admits the downloaded artifact without executing it. It checks the successful hosted workflow attempt, exact candidate/driver/helper identity, selected source contract, native executable, complete unique regression set and every original log. Each ordered-barrier regression must emit exactly the complete source-registered milestone sequence. Regressions marked as in-test assertions retain that narrower witness type; assertion-only evidence cannot invent emitted barrier events. The result states `requiredNativeSubsetPassed`; whole-case, fleet and canonical PASS remain false. Missing source groups, old-source artifacts, ignored or zero-test output and partial evidence are rejected. Updating another candidate's registry entry does not invalidate an unchanged selected contract. Changing the producer or the selected contract requires a fresh tools artifact.
+
+Runtime sources opt in to specific cases separately from the native registry. The `6af` source enables only G01; `bd0671b` enables G01/G02/G03; `8380466` additionally enables G04 with its complete, distinct native group. Source `757b751` enables A09 through `nolock.py`, G06 through `race.py`, and G05 through the dedicated `mpu_race_runner.py` controller. It does not substitute a different source into G01-G04. The workflow verifies the bundle and invokes the pinned helper's read-only runtime preflight before exporting it to canonical execution. A collector-only helper without the corresponding runtime adapter fails before fleet mutation. The executor repeats admission before package installation or service changes.
+
+All original H1/H2 black-box branches, SDK ACKs, manifests, overlap witnesses, exact Admin settings and physical/S3 checks remain required. Native logical disk shapes do not certify distributed VM execution. The original `072b57b37559fbe626aeda8abf61617fdcbdd926` route retains its blocked native rows. A09, G05/G06 and I05 still require their own complete native groups and executor evidence. G04 also remains unaccepted until its same-source hosted evidence and every original live branch pass independent audit. No source or case gains canonical acceptance from a registry entry or local native test; a successful exact-source hosted bundle, every live branch and independent original-evidence audit are required.
+
+The A09/G01-G04/G06 public race adapters retain the frozen fourth-physical-drive fault selection, independent of shuffled data/parity role. H2 selects that local drive on its first original host. After faults and restart, `race_io.py` applies the source-defined `65536` bytes/s write cap only to the run-owned unit on actual fault hosts. Exact process birth/candidate, all four original mount identities, systemd cgroup and `/proc` membership are checked; the actual kernel `io.max` must show the original fourth device and rate. Existing foreign limits, changed identity, wrong devices and unbound release reject. Intent is recorded before each single control request. After the foreground sequence, the cap is removed and kernel readback is verified before waiting for the same original task. This adds no sleep to SDK timing and changes no payload, overlap oracle or task timeout. An interrupted or uncertain control still uses existing owned-service stop and retained-scene rules. G05 refuses this cap and retains its source-defined unrestricted process I/O.
+
+### A09/G06 native groups and frozen G06 public fixtures
+
+Source `757b751482bb970bc321e92059690a7698554358` adds two collector-only groups. `default-heal-exclusive-owner` observes actual PUT and exact historical DELETE lock futures becoming pending while default Deep Heal exclusively owns a real staged external repair. Physical reconstruction precedes foreground publication; all acknowledged successor or retained-current shards and full bodies are checked in 4/2 and 16/4. Its sixteen events prove the storage fence, while Admin `nolock=true` normalization and public request timing still require separate evidence. `copy-and-metadata-final-rename` covers real streamed cross-key COPY source/destination, self metadata replacement and tags. Twenty-four events bind actual ACK and physical-tail completion before releasing shared reconstruction, exact stale refusal or unchanged-source repair, final source/target/control payloads, metadata and tags. The registry binds all five storage fixture/barrier source files. This candidate admits A09, G05 and G06 only with their distinct complete case-specific native group and matching hosted helper/source artifact. Both workflow and executor run read-only preflight before bootstrap. A09 always sends wire `nolock=true` and requires effective `nolock=false`, plus both original H1/H2 public branches and the separate storage-native proof. G05 uses the separate same-key runtime described below.
+
+The G06 public adapter uses the existing DS workload: all 257 original keys, including Chinese, spaces, literal `%2F`, actual slashes and parent-like prefixes. The observed missing key is `中文/文件/0252.bin`; its companion is `literal%2Fslash/0254.bin`. `copy-source` copies from the observed key into the companion; `copy-destination` copies in the reverse direction. Default COPY expectations inherit the source payload, content properties, metadata and tags. Both directions verify the unchanged source, exact destination coding-index shard bytes and a distinct destination generation before GET, rejecting retained old destination directories. Registry, fixture and local transition tests do not establish public overlap or complete canonical PASS.
+
+G06 reuses `KeepAliveStatusProbe` only while observing the original token and
+foreground operation. Existing readiness polls warm the same direct connection;
+the controller still starts each foreground observation from the real SDK send
+notification. Every connection attempt, actual socket identity, response and
+phase remains in `G06-status-transport`. A transport error poisons the session
+without an implicit retry; leaving the session closes its connection. The strict
+same-clock nested interval, complete ACK ledger and finished-task refusal remain
+unchanged. No delay is injected into the SDK call.
+
+Before race allocation and each explicit service restart, the runner and every
+participating node resolve the existing `rustfs-node1` alias (H1) or all four
+aliases (H2). `endpoint-resolution` retains bounded, single-attempt `getent`
+results and refuses a failed precondition. It never edits hosts files or DNS,
+substitutes IP addresses, extends the readiness budget or certifies node identity.
+Later topology and physical checks remain required; successful preflight cannot
+guarantee that a resolver will remain available during startup.
+
+G01/G02 retain every serial PUT ACK but follow the frozen source's overlap rule:
+at least one strictly nested running status sample per required workload role.
+G01's read-repair branch additionally journals every actual GET, verifies its
+complete body and joint ETag/length/type/metadata against one original or
+ACK-known foreground generation, and requires a reader overlap. Zero reads,
+unknown results and mixed generations cannot pass. G02 requires all six
+ACK-known historical versions and a separate reader overlap in its read branch.
+Sampling continues until an in-flight reader drains after the writer. These
+conditions preserve the source workload; they do not supply live acceptance.
+
+### G03 serial deletion authority
+
+G03's frozen `VersionLedger.history` determines the new current version after an explicit DELETE. The public S3 inventory is UUID-sorted and cannot supply that order. Each original foreground plan now carries the three seed ACK version IDs in order; the final oracle rejects missing, duplicate, foreign or differently selected identities, removes exactly the acknowledged version and promotes the last retained seed ACK. The physical oracle permits only that `isLatest` change while preserving every other retained descriptor, coding-index byte and data directory. Every original drive must contain exactly the retained version/latest population, with no extra marker or duplicate version. Focused tests use nonmonotonic UUIDs, both delete branches and corrupted physical populations. This source adaptation adds no native or live acceptance.
+
+### Frozen version and multipart race controllers
+
+G04 and G05 use the owned `frozen-race-legacy` runtime profile. It explicitly sets `RUSTFS_SHARD_INTEGRITY_WRITE=false`, `RUSTFS_SHARD_INTEGRITY_FLEET_CONFIRMED=false`, `RUSTFS_PUT_RENAME_EARLY_ACK_ENABLE=false`, and `RUSTFS_HEAL_MAX_CONCURRENT_HEALS=1`, matching the frozen source's legacy shard population and serial scheduling. The profile retains disabled scanner, automatic Heal and MRF. The regular `manual` profile continues to enable protected shard writes. Original physical file-population assertions remain strict; an unexpected integrity sidecar is a failure. Owned service configuration is attested through the existing runtime checks. G04 alone may use the frozen fourth-drive write cap under this profile; G05 only observes kernel limits. Retention supports both historical `manual` evidence and the new profile, with the same fixed bucket/key scope and stopped original scene binding. An older helper's tools remain bound to that helper; they cannot certify a newer controller.
+
+
+`version_race_fixture.py` preserves the frozen `heal_version_race_fixture.py` model at `524eb5b32c3236497fdd8a9271df0494fa3d9974` (source SHA256 `755f476ab567dc413e9798bb2bcac15252c3e8308e4b15b15a9eb3083d1df485`), with local import adaptation only. Its eight G02–G04 plans derive versions from serial ACK order. G04 uses flat keys, seeds a latest marker for both marker removal and batch deletion, and deletes exactly the oldest target version, newest target version, marker and control `objects-0002.bin` version. It preserves the middle target generation. A partially acknowledged batch retains each known effect and permanently blocks final acceptance and further mutations. Historical reads address exact versions; negative reads require the original deleted-version 404 and marker 405/current-marker 404 contracts.
+
+`version_race_physical.py` preserves the frozen physical oracle, replacing the source transport's fixed inspector digest with a mandatory caller-supplied immutable tool digest. `version_race_adapter.py` maps every owned drive to its real format set slot; H1 retains EC2+2 and H2 explicitly requires the deployed EC12+4 geometry. Every drive must match the ACK-derived version/marker population and coding indices. Deleted generations' unreferenced bytes remain separately reported; they are neither live redundancy nor a garbage-collection PASS.
+
+`version_race_runner.py` replaces the superseded generic G04 fixture and shard-fault path. Its 257 faults remove the first host's fourth-drive `xl.meta` files. The original 120-second partial window requires nonzero original-token progress, a physically reconstructed control, at least 32 pending objects and the selected target metadata still absent. One original SDK mutation runs with the source 90-second observation budget and 25ms sample pause; the owned fourth-drive 65536-byte/s write cap is released while the original call drains. Final checks prove the 253 other controls, exact version/marker/coding populations, full live part streams, every peer's deleted-version/marker negatives and complete inventory, post-GET physical immutability, ACK-bounded completion and original root retirement. Final assertion failures preserve independent diagnostics; evidence I/O failures stop execution. Seed or foreground uncertainty retains the stopped scene and original journals as DIRTY. Recovery can isolate that exact scene without restoration, retry, deletion or a PASS rewrite. Runtime entry requires source `8380466` and the complete marker/batch native group before bootstrap; the legacy source cannot enter this controller. These implementation/component checks add no canonical PASS; fresh exact-helper native tools and complete H1/H2 live acceptance remain required.
+
+`mpu_race_fixture.py` migrates the frozen validation repository's `heal_mpu_race_fixture.py` model at `524eb5b32c3236497fdd8a9271df0494fa3d9974`. It uses the existing auto-testing `Recorder` and probe directly. Imports are local; no source validation checkout is needed at runtime. Part sizes follow the frozen common DM recipe: `5MiB+17`, `5MiB+93`, and `1MiB+47`.
+
+The caller supplies a fresh versioned owned bucket. One completed three-part object and two distinct pending uploads share `objects-0256.bin`: A initially has parts 1/2, B has parts 1/2/3. The model binds each mutation to its actual UploadId/part ETags and durable request/ACK files. A continues with part 3 and completes; B aborts. Unknown or malformed mutation results poison the model and prevent later sibling changes. Bounded paginated listings, exact terminal `404/NoSuchUpload`, all acknowledged historical/current versions and complete GET payloads verify sibling isolation. The model does not clean up uploads with uncertain outcomes.
+
+Immutable native source `09fb813a003e7964bd1b893efb7446a3c41bd3bf` adds `multipart-continue-complete-abort-final-rename`: ten ordered milestones across 4/2 and 16/4 logical geometries, using real three-part commits, same-key A/B pending parts and a paused shared Deep Heal. Completion tails drain before stale Heal release; exact retry disposition, old/sibling preservation, new committed bytes and upload retirement are asserted. The native test's unversioned logical scope is supplemental to the full versioned SDK/fleet case.
+
+The historical `09fb813` entry remains collector-only. Runtime admission binds source `757b751482bb970bc321e92059690a7698554358`, its complete multipart native group and the exact hosted helper artifact. The old `072b57b` source is rejected before bootstrap. Component tests and native bundles do not add canonical PASS.
+
+`mpu_race_runner.py` replaces the generic unversioned MPU path with the frozen same-key fixture and schedule for all three public branches in H1/H2. It records 256 versioned flat-key controls, one committed three-part version and A/B pending uploads through the existing durable `Recorder`. The 257 faults target the fourth physical drive on the first host. All 259 original and 262 final part cohorts receive full streaming verification. Every companion ACK has an immediate independent staging checkpoint; only the selected first SDK operation requires a contained original running-token sample. The status transport preserves the frozen warmup, 1024-sample/90-second budget and 5ms poll pause, restricts requests to the owned bucket and original token, and records request/response persistence phases. Kernel I/O sampling reads the owned cgroup and its ancestors without setting or releasing a cap; it makes no continuous no-limit claim.
+
+Final committed/staging/control proofs precede all-peer S3 verification. Assertion failures retain the frozen source's remaining independent diagnostics while evidence I/O errors stop execution. Repeated terminal responsibility, ACK bounds, post-GET physical stability and exact original-root retirement must pass before cleanup authority exists. Any issued seed request without that authority retains the stopped raw MPU prefix, committed scene and original request/ACK files as DIRTY, with no retry, abort, restore or namespace cleanup. A missing overlap remains `NOT_OBSERVED` even after safe cleanup. Full hosted native acceptance and all six public live instances still require independent evidence before G05 can count as canonical PASS.
+
+The additive `mpu-race-scene` node action in `mpu_race_physical.py` adapts the frozen `heal_h1_mpu_physical.py` upload-ID and byte-continuity oracle to the current owned four-drive hosts. Each request binds the owned race runtime, bucket, candidate binary and all three ACK-selected uploads. It inventories the complete fixed key prefix, hashes only selected staging files under bounded entry/depth/byte limits, rejects unknown uploads and links, and preserves original process identity during a live capture. Stopped captures additionally verify the owned service remains stopped. Across H1/H2, pending part/sidecar populations and existing sibling bytes remain exact; a completed upload must lose its staging directory and retain all three previously witnessed encoded parts in committed storage. These are staging/continuity witnesses; independent inspector coding/bitrot checks, product Heal proof, real overlap and full SDK inventories remain required.
+
+### I05 caller-only TCP disconnect component
+
+`caller_abort.py` sends one signed, exact historical-version S3 GET to a bound endpoint and closes only that caller TCP socket. Invoke it with `python3 -B scripts/validation_heal/caller_abort.py --binding --bucket --key --version-id --output `. Credentials use the existing `HEAL_ACCESS_KEY`, `HEAL_SECRET_KEY` and optional `HEAL_SESSION_TOKEN` environment variables. The request intent is persisted before connection; a completed write checkpoint precedes shutdown, and the final receipt binds the original socket addresses, descriptor, request digest and monotonic clock. The evidence directory cannot be reused. There is no request retry, redirect, proxy or response-derived success. A persistence or transport error closes the socket and retains its uncertain result. Signed headers and credential values are not stored.
+
+The component always reports `resultUnknown=true`, `acknowledged=false` and `serverOwnershipProven=false`. Successful local command execution means the client transport completed its disconnect sequence. It does not prove that the server admitted the call, acquired a lock, continued after disconnect or released its owner correctly. I05 remains outside canonical dispatch until the controller binds actual server admission and owner events to this exact call, proves the registered timeout and stale-generation fences, and verifies final object generations, drained locks and file descriptors across every required H1/H2 branch. Existing exact-source native owner tests remain separate evidence.
+
+### I05 service admission and caller drain observer
+
+`lock_observer.py` observes an existing run-owned H1/H2 service using the explicit `lock-timeout` runtime profile and reviewed source `757b751482bb970bc321e92059690a7698554358`. The profile enables only the existing object-lock diagnostic target at TRACE, sets the actual object-lock acquisition budget to one second, disables early PUT rename ACKs, and preserves the standard disabled scanner/automatic-Heal/MRF settings. It does not modify the service binary. Run `python3 -B scripts/validation_heal/lock_observer.py --binding --nodes vm000 [vm001 vm002 vm003] --bucket --entry --output `. The entry contains the original `key`, `versionId`, `deleteMarker=false`, and complete ACK-bound `payload.size`/`payload.sha256`. This component neither deploys nor cleans a namespace; the enclosing case owns those steps and recovery.
+
+After persisting and sending the original single GET, the observer waits up to ten seconds for its actual server-generated request ID and `get_object` lock attempt. A unique User-Agent marker connects the exact version URI and method in the `http-request` ancestor span to that lock's owner/namespace/attempt. The marker is correlation data, not an identity asserted by the client. Its HTTP span must carry one exact historical version selector; an optional `requested_version_id` diagnostic field must agree with that selector. Free-text messages, nested native-test capture rows, client-supplied request IDs, repeated lifecycle events and foreign endpoint/owner/version records cannot prove admission. Cursor-bounded raw journald pages remain in evidence, and source message loss prevents completion.
+
+Before disconnect, two `/proc` samples must connect the exact client/server TCP tuple to an actual established socket descriptor of the original candidate process. A network-namespace table entry alone is insufficient: its inode must also appear in that process's FD links. The read-only adapter accepts no arbitrary PID, path, command or service port. Process birth, binary, environment and mount namespace must match, and its network namespace cannot change during sampling. A second journal capture must still show acquired without a diagnostic drop. An unavailable/already-ended window records incomplete evidence and closes the original caller socket without retry.
+
+After the caller closes, the observer follows the same request/attempt/owner until its `guard_dropped` event, requires two complete samples without the original owned socket, and waits up to thirty seconds for each peer's FD count to return to its baseline. It then reads and verifies the exact historical version's full content and samples resources again. Every transport result remains UNKNOWN. Diagnostic guard drop does not prove distributed release, snapshots do not prove ownership across the disconnect instant, and aggregate FD counts do not prove absence of every leak. Those claims stay explicitly false. The timeout/stale-generation native fences, queued-successor lock closure, complete DL/DV physical and ACK verification, every H1/H2 branch and independent live acceptance remain required; I05 stays outside canonical dispatch.
+
+For the I05 runtime, process resource baselines and later samples also retain a bounded `descriptorCensus`: FD numbers, descriptor kinds, hashed link targets and TCP rows whose socket inodes belong to that exact process. The caller socket's two samples include the same census. Raw descriptor paths and descriptor contents are excluded. Collection rechecks the original process birth and network namespace, counts descriptors that vanished during the read, and marks every census as a snapshot without a no-leak claim. These records help diagnose a count that does not return to baseline; they do not exclude any descriptor from the original FD threshold or extend the drain budget.
+
+### I05 object-lock timeout diagnostic controller
+
+`lock_timeout.py` extends the caller observer with one real contender PUT while the exact historical GET still owns its read lock. Run it only against the already assigned exclusive `lock-timeout` H1/H2 service and the current auto-testing nodes: `python3 -B scripts/validation_heal/lock_timeout.py --binding --nodes vm000 [vm001 vm002 vm003] --bucket --entry --output `. The original historical payload must be at least 32 MiB so the controller can apply and record a bounded 64 KiB client receive buffer, then observe the same established service-owned TCP socket with a non-empty send queue after the contender response.
+
+The contender body, exact PUT request, endpoint, structured 503/`ServiceUnavailable`/`Retry-After: 1` response and durable ACK are bound together, including the recorded operation, parameters, payload digest and ordered timestamps in the collector's clock domain. The pinned candidate's `rustfs/src/error.rs` maps typed `StorageError::Lock(LockError::Timeout)` to this response and directly tests its header. Cursor-complete server logs must associate that response's distinct server request ID and write-attempt UUID with the same bucket/key namespace and the exact one-second `put_object_commit` timeout. The same-node GET and PUT share the set's owner label; that label is not a per-request identity. The original GET attempt must still be acquired after the timeout and socket samples. Failure, UNKNOWN, a missing journal event, an empty send queue, a small fixture, or binding drift is retained as incomplete or `NOT_OBSERVED`, never retried or passed. The controller checks the original physical scene before any S3 read, compares complete versioned inventory before and after, and waits for process FD counts to return to the original baseline.
+
+This establishes a live timeout diagnostic subset only: it does not certify ownership at the precise disconnect edge, stale read-repair fencing, queued successor behavior, H1/H2 DL/DV source branches, or canonical acceptance. The native abort/timeout/stale fences and independent live audit remain required.
+
+`lock_timeout_runner.py` provides the enclosing diagnostic deployment through
+the existing Functional Chain workflow. After auditing prior cleanup and
+ownership, explicitly select `validation_case=heal.lock-timeout-and-caller-abort`,
+the exact `auto_testing_sha`, candidate build run/attempt and successful
+inspector build run/attempt. The existing shared-fleet concurrency group and
+auto-testing bootstrap install the immutable package. Local source, helper,
+workflow identity, vm000–vm003 binding and inspector checks run before
+bootstrap; Fleet verifies the installed binary digest before starting each
+owned service. H1 runs on vm000, followed by H2 on vm000–vm003 only after
+complete H1 diagnostic and namespace cleanup. A fresh artifact tree is required;
+the runner accepts the enclosing script's setup/test logs but refuses any old
+topology or summary before deployment.
+
+Each topology uses the existing Fleet, Recorder and DV seeder: two keys have
+three original versions each and one current marker; the selected historical
+version is 32 MiB to maintain bounded TCP backpressure. This small fixture is a
+diagnostic subset, not the complete source DL/DV matrix. After timeout and
+caller-drain checks, every endpoint must preserve the entire original ACK
+manifest, payloads, metadata and tags. Physical comparison brackets those
+reads, and every original process's FD count must return to its baseline before
+cleanup. Failed/unknown seeding, contention, observation or peer verification
+stops the owned services, saves the original physical/log scene and retains
+the namespace and original receipts. A failed diagnostic or cleanup prevents
+the next topology. SIGTERM follows the same failure capture and retention
+path. The summary records diagnostic completion while keeping whole-case and
+canonical claims false. The manual entry returns canonical `BLOCKED` after a
+successful diagnostic with clean finalization, preserves diagnostic failures,
+and reports `INFRA_ERROR` for an incomplete summary or failed finalization
+after a zero exit. The workflow gate therefore remains non-green even when
+the diagnostic succeeds. I05 remains excluded from the automatic matrix;
+this manual route does not certify its native barriers or complete source case.
+
+### J02 original opaque metadata probe
+
+The `unknown-negative` diagnostic includes the source procedure before the supplemental legacy-object diagnostic. A separate acknowledged object first receives an independently healthy Deep receipt. With the owned services stopped, the existing descriptor-relative fault backend flips byte zero of every original `xl.meta` copy: four copies in H1 and sixteen in H2. Each mutation has its own original backup, receipt and complete physical delta; the candidate-bound decoder must refuse every exact damaged copy. No other metadata, part or format may change.
+
+A separate protected prefix is checked during the fault. The original key-level task must retain complete truthful counters and an unresolved disposition, with no invented healthy, repaired, absent, version or incarnation claim. Unknown must be counted within skipped and carry the legacy warning. If the candidate reports another truthful unresolved disposition, record `NOT_OBSERVED`; the legacy diagnostic cannot promote it to PASS. Archive the original terminal result and unchanged complete fault scene before restoring original metadata bytes. Verify the full original physical and SDK inventories and the unchanged original token after restoration. Fixture rescue is never product repair. J02 remains outside canonical dispatch until the separate version-specific grace Deferred/new-token procedure and complete live evidence are available.
+
+
+### I05 retained-scene isolation
+
+The reviewed H2 startup failure `39764496504/1` has a separate preservation
+plan pinned to helper `31b787c51de23587da1f9a2ac0f910400ddf66b3` and the
+SHA-256 inventory of all 102 official artifact files. H1 completed its timeout
+diagnostic and actual root cleanup; H2 reserved all four peers, then failed
+before fixture seeding because vm002's bpftool wrapper could not find its
+kernel-specific executable. All four stopped original scenes contain no
+business files and retain the same complete sixteen-disk format mapping.
+The planner requires those exact original owners, candidate, inspector,
+cleanup receipts and failure boundary before allowing the existing
+audit/isolate/post-audit protocol. It issues no workload request and changes
+neither the original FAIL nor canonical acceptance.
+
+Isolation [39766457981/1](https://github.com/rustfs/rustfs/actions/runs/39766457981)
+completed `ISOLATED` with helper `fa0b659`. Independent audit byte-matched all
+102 original files and all four preflight/isolation/post-audit baselines.
+All twenty data/state archive parents are root-owned mode 0700 and on their
+original devices; original tree, owner and inode identities are unchanged.
+No S3 request, backup restoration, namespace deletion or original verdict
+rewrite occurred.
+
+`environment.inspect` now also records the selected bpftool path, installed
+`/usr/lib/linux-tools/*/bpftool` executables, resolved root-owned executable
+digests, kernel release, version exit status and a read-only root-cgroup JSON
+query. A working installed executable is distinct from a broken PATH wrapper.
+This inventory does not install tools, attach programs, change PATH or certify
+the I05 service filter; the actual owned-service attachment checks remain
+mandatory before another diagnostic.
+
+Read-only inventory
+[39766570688/1](https://github.com/rustfs/rustfs/actions/runs/39766570688)
+found broken PATH wrappers on both vm002 and vm003, which run kernel
+`6.17.0-1022-azure`. All four peers already have
+`/usr/lib/linux-tools/7.0.0-1017-azure/bpftool` with executable SHA-256
+`aabc337b58169d62c42b3c8f1fccf605e5fc1e445a23feb0156638e9d9606991`.
+The executable reports bpftool 7.7.0/libbpf 1.7 and its root-cgroup query exits
+zero on each peer. That empty root query alone does not verify service BPF
+support. I05 now uses this exact installed executable with protected root-owned
+path, digest and version checks, records its identity beside the actual
+service attachments, and checks every peer before reserving any namespace.
+It changes no installed tool, global PATH or kernel. Service-bound ingress and
+egress program identity checks remain mandatory on the running kernel.
+
+Diagnostic [39767336741/1](https://github.com/rustfs/rustfs/actions/runs/39767336741)
+completed both H1 and H2 with helper `8cbdd51` on the unchanged candidate
+`757b751482bb970bc321e92059690a7698554358`, package `39364893106/1` and
+inspector `39566504743/1`. Independent audit of all 211 official artifact
+files verified nine seed request/ACK pairs and seven versions/markers per
+topology, candidate/helper bindings, actual process-bound ingress/egress BPF
+programs on all peers, complete journal cursor chains and exact server
+request/lock attempt identities. Each contender returned a known non-retried
+503 after a 1001 ms write-lock timeout while the original GET remained
+backpressured. The caller connection disappeared and its diagnostic read
+guard dropped; original physical files remained identical before public
+verification and all one/four peer inventories agreed with seed ACKs.
+
+Final FD counts were H1 vm000 `22 -> 22`; H2 vm000 `106 -> 94`, vm001
+`108 -> 104`, vm002 `105 -> 101` and vm003 `101 -> 97`, with unchanged process
+birth and network namespace. These are bounded snapshots, not a global
+absence-of-leaks proof. All five node cleanup receipts show stopped services
+and removed owned roots. The original result is `BLOCKED/cleanup PASS`:
+the workflow fails its deliberately retained canonical gate despite successful
+diagnostic execution. Exact disconnect-edge ownership, distributed release,
+complete native timeout/stale fences, the stale-generation controller
+and full source H1/H2 DL/DV coverage remain unaccepted. The separately audited
+queued-successor diagnostic is recorded below. No new
+canonical case is counted.
+
+`validation_cleanup_run_id=` selects the narrow I05 preservation
+route with the original package build and an immutable helper containing
+`timeout_retention.py`. The driver must remain on the original Functional Chain
+branch. First check the shared current auto-testing fleet for other work. The
+planner accepts the original helper 704bd60 failure before contender
+submission and the reviewed helper d100abeb failure after one exact known 503
+contender refusal. It also accepts helper 1b88cad when H1 completed and cleaned
+successfully before H2 reached that same exact contender failure. The H1 path
+requires nine exact successful seed request/ACK pairs, complete
+version/marker/payload/public metadata baseline, a closed read-only historical
+GET, original owner/candidate/inspector and an unchanged stopped four-drive
+scene. The contender boundary also verifies its request/ACK digest, server
+request ID, full bucket-incarnation read-lock lifecycle and exact failed target
+write-lock lifecycle. The H2 path additionally requires four reservations,
+four inspector uploads, per-node lock checkpoints and balanced four-peer
+journal rounds before disconnect, after admission, and during contender
+observation, four stopped physical scenes and four service logs.
+Unknown or successful writes, missing peers, another action, an unclean H1 or
+physical drift refuse recovery.
+
+The recovery reuses retained-scene audit/isolate/post-audit, retaining all four
+roots and state under private per-run parents without S3 calls, backup
+restoration or deletion. Verify all original archived files byte-for-byte and
+all five final private parents independently before recording active namespace
+isolation. `ISOLATED` never changes the original FAIL or supplies canonical
+acceptance. H2 recovery requires the completed H1 diagnostic and its successful
+cleanup; it isolates all four H2 peers together when the H2 contender boundary
+fails. In run 39733064688, the server refused the contender after a 1001
+ms target write-lock timeout while the exact GET held its read lock for 4.148
+s; the validator failed because it combined the same PUT's bucket-incarnation
+read-lock events with the target write-lock events. Read-only inspection
+[39738071753/1](https://github.com/rustfs/rustfs/actions/runs/39738071753)
+observed vm000 with no RustFS service, validation unit or port 9000 listener.
+Recovery [39738426191/1](https://github.com/rustfs/rustfs/actions/runs/39738426191)
+completed `ISOLATED` for vm000: all 65 failed-artifact files match
+byte-for-byte, preflight/move/post-audit physical digests match, and four data
+roots plus private state are retained in five root-owned mode-0700 same-device
+archive parents. No backup restoration, namespace deletion, S3 request or
+original FAIL rewrite occurred. Inspection also observed old failed F07 units
+on vm001-vm003; the I05 plan did not touch those nodes.
+
+The first live run's GET already dropped its lock before observation. The
+version-field correction correlates those original events but cannot reopen
+that ended window. The exact candidate source and runtime log differ on the
+optional version field; the cause remains unresolved. Any subsequent I05
+controller must establish a real streaming/IO ownership window with evidence;
+repeating the same warmed full-read premise does not provide that proof.
+
+Source review narrows the next experiment without changing the oracle. The
+candidate defaults the object-data cache off, and an explicit `versionId`
+excludes the direct-memory GET path. The 32 MiB fixture is non-inline, while
+codec streaming rollout is off by default, so this run used the legacy duplex
+reader. `ObjectLockDiagGuard` carries the real namespace guard; the
+`SetDiskLockGuardedReader` empty-read path can release that wrapper before the
+HTTP response completes. The live journal shows `guard_dropped` before the
+seven-millisecond HTTP completion and a later `get_object_stream_body`
+`dropped_incomplete` event. This is a source-supported hypothesis for a
+controlled streaming/IO reproduction, not a confirmed cause or a canonical
+acceptance result.
+
+I05 descriptor run `39754893467/1` pins helper
+`dd7e54e6672cab76b5a6245ab3b4fbd051ddd39d` and the unchanged package from
+`39364893106/1`. H1 retains its original FAIL/cleanup FAIL at the unchanged
+30-second drain budget; H2 did not start. The baseline owns 22 descriptors.
+Every drain resource snapshot owns 23, with all baseline fingerprints intact
+and exactly one additional TCP descriptor from remote `168.63.129.16` to local
+port 9000. The caller's exact GET socket is absent, and its read-lock journal
+contains `guard_dropped`. These snapshots establish external connection
+interference; they do not prove the platform probe configuration, continuous
+socket absence, no leak, or complete canonical acceptance.
+
+Preservation recognizes only this additional exact helper and its original
+known 503 refusal, closed read-only GET, complete journal lifecycle, stopped
+physical scene and bounded request/ACK set. The original 101-file artifact is
+retained in `testdata/validation/i05-descriptor-drain-39754893467.zip`.
+Descriptor counts, unique FD identities, socket-link fingerprints, owned TCP
+joins, snapshot flags, process birth and network namespace must agree.
+Missing census, unknown/successful mutation, extra request and physical drift
+refuse preservation. Drain journal windows follow the numbered first-drain
+label rather than a fixed count of preceding caller windows. This change
+permits preservation only; it performs no network change and grants no PASS.
+
+Isolation run [39757904633/1](https://github.com/rustfs/rustfs/actions/runs/39757904633)
+completed with helper `f43151595648d83b184ab95fd3e908e7611d0dae`.
+An independent artifact audit confirms all 101 failed-run files are byte exact,
+preflight/move/post-audit baselines are identical, and all five archive parents
+are root/root 0700 on their original devices. Original business-scene digest
+`f921b69ea51622f89e9ad4a78de8c890caf0b7ffefa0b401457d6f9796fc7d31`
+is unchanged. Only the owned active paths were isolated; no namespace was
+deleted, no backup was restored and the original FAIL/cleanup FAIL remains.
+The inventory now reports existing `bpftool` availability before selecting a
+service-scoped network filter; this report installs no tool or filter.
+An earlier dispatch `39757736435/1` was rejected before candidate resolution
+because the submitted helper was abbreviated; no fleet action ran there.
+
+Read-only inventory [39758842602/1](https://github.com/rustfs/rustfs/actions/runs/39758842602)
+with helper `c5d1890467599e27aeffeb5dbf1ed4b37141fd06` confirms that all four
+current fleet nodes already provide `bpftool` and cgroup v2, and none listens
+on port 9000. This observation does not certify global idleness or historical
+scene cleanup. Existing failed F07 units remain outside this operation.
+
+The I05 `lock-timeout` runtime now starts its private transient systemd unit
+with `IPAddressDeny=168.63.129.16/32` and no allow override. Startup and every
+resource sample require exact effective properties, the owned MainPID's
+membership in that unit's cgroup, both actual `sd_fw_ingress`/`sd_fw_egress`
+attachments, and root-owned `cgroup_skb` program identities/tags from bpftool.
+Missing support, ignored settings, foreign programs, aliased cgroups and
+process changes fail closed. This policy isolates the observed external
+source from this test service in both directions. Other units and the host
+firewall are untouched; the policy has the private service cgroup's lifetime.
+It does not identify the platform's probe configuration. Resource evidence
+retains the effective properties and kernel program IDs/tags as snapshots.
+The total FD threshold, caller/request identity and 30-second budget remain
+unchanged; complete I05 canonical acceptance is still required separately.
+
+Filter startup run [39761856012/1](https://github.com/rustfs/rustfs/actions/runs/39761856012)
+used helper `a9725c88acd4977545b278eebe3653c916f907f8` and stopped at the
+kernel attachment check, before seeding or issuing the diagnostic requests.
+Its stopped H1 scene has four original format files and no object files; all
+22 original artifact files and FAIL/cleanup FAIL remain retained. The parser
+had accepted only the older bpftool `ingress`/`egress` attach type strings.
+Newer bpftool uses libbpf's `cgroup_inet_ingress`/`cgroup_inet_egress` names for
+the same kernel types. Both exact spellings now map to a checked direction,
+while evidence preserves the original attach type. Unknown, duplicate or
+missing directions and non-string flags still fail closed.
+
+The startup preservation route requires that exact run/helper and all 22
+original file hashes, original candidate/package/binary, stopped owner and
+empty object scene. It reuses the existing same-device, private-parent
+isolation backend and its live owner/physical/tree checks. No workload is
+replayed and no namespace is deleted. A successful isolation or corrected
+startup check cannot certify the I05 source case.
+
+### I05 queued-successor diagnostic
+
+Select `I05_DIAGNOSTIC_BRANCH=queued-successor` for the explicit I05 route.
+`run_case.sh` passes this branch to both preflight and the runtime controller.
+The Functional Chain input `i05_diagnostic_branch=queued-successor` downloads
+the pinned native artifact and verifies its original hosted run, candidate,
+helper, binary, exact regression logs and required I05 labels before fleet
+mutation. `VALIDATION_HEAL_NATIVE_DIRECTORY` supplies that bundle locally.
+Native success remains a prerequisite and never certifies the live case.
+
+`lock_successor.py` uses a separate `lock-successor` runtime: the GET lock
+budget remains one second, while the successor PUT commit acquisition budget
+is sixty seconds. It starts exactly one marked PUT while the original
+historical GET is admitted, observes the PUT acquiring the same namespace,
+then closes only the original caller socket. It requires the original GET
+guard-drop event before successor acquisition, an unretried server-request-ID
+bound PUT ACK, exact ETag/version/payload, preserved historical generations,
+a distinct new physical generation, all-peer S3 agreement and bounded FD
+drain. Observation windows remain ten seconds for queue admission and thirty
+seconds for completion. The fixture is a diagnostic subset of the source DV
+workload; it does not complete the source DL/DV matrix.
+
+Failure stops the run-owned service and retains its original requests, ACKs,
+logs and physical scene. `successor-retention.json` binds all diagnostic files
+and stopped per-node physical digests to the original owner. Recovery accepts
+only an unchanged sealed archive and the same stopped live scene, then uses
+the existing same-device private-parent isolation backend. An unknown PUT
+outcome is preserved without retry, deletion or old-generation restoration.
+Incomplete capture, missing reservations, changed evidence or scene drift
+refuse automated isolation.
+
+The exact-helper native/tools build
+[39783997956/1](https://github.com/rustfs/rustfs/actions/runs/39783997956)
+passed independent archive, source, executable and ten-regression audits.
+The first successor live run
+[39788321908/1](https://github.com/rustfs/rustfs/actions/runs/39788321908)
+retains FAIL/cleanup FAIL: the same PUT read its bucket's internal
+`.metadata.bin` after target write-lock acquisition; the controller
+incorrectly classified that read as a target write. The request returned
+one unretried 200 ACK, which does not complete the failed diagnostic.
+
+[39789732916/1](https://github.com/rustfs/rustfs/actions/runs/39789732916)
+isolated the unchanged stopped H1 scene without deleting it or restoring
+old generations. The parser recognizes only that bucket's exact
+`.metadata.bin` and `.bucket-incarnation` read paths, checks their original
+HTTP identity, namespace, one-second budget and ordered per-attempt
+lifecycle, and excludes them from the unique target write attempt. The
+original fifteen-event trace is a regression fixture; foreign paths,
+write-mode substitutions and reused or malformed lifecycles still refuse.
+A new helper requires a newly bound native/tools build before another live
+attempt. Fresh exact-helper native/tools
+[39791663588/1](https://github.com/rustfs/rustfs/actions/runs/39791663588)
+passed independent verification of all three official archives, 22 original
+members, ten exact tests and 104 ordered milestones. The next live run
+[39798325977/1](https://github.com/rustfs/rustfs/actions/runs/39798325977)
+passed the metadata-read classifier and acknowledged one new PUT version,
+then retained FAIL/cleanup FAIL because the controller invoked stopped-only
+streaming shard verification while the service was running.
+
+[39799657074/1](https://github.com/rustfs/rustfs/actions/runs/39799657074)
+isolated that stopped H1 scene. Independent official archive verification
+byte-matched all 71 original files and all five pre/move/post tree, device
+and inode baselines. The new shard's streaming checksums now run only after
+all online peer reads and FD observations, under the unchanged stopped-only
+verifier. Stopped physical snapshots must match the ACK-bound pre-read scene
+both before and after the checksum pass. Drift or checksum failure retains
+the original scene and prevents cleanup. Exact helper `6fbc3c4` tools
+[39803149039/1](https://github.com/rustfs/rustfs/actions/runs/39803149039)
+passed independent audits of all three official ZIPs, 22 original members,
+ten exact native tests, 104 ordered milestones and inspector contracts.
+Live [39810035277/1](https://github.com/rustfs/rustfs/actions/runs/39810035277)
+completed the new H1 stopped-integrity sequence and cleanup, then retained
+H2 because a slow-acquire WARN sharing the diagnostic target was parsed as
+a lifecycle event. The WARN has no event/state/attempt/namespace fields;
+it reports a 1214 ms acquisition against a 500 ms warning threshold. It is
+not a timeout or a separate write attempt. The selector recognizes this
+exact auxiliary shape only after the same request's acquired target write,
+retaining strict HTTP identity, owner, numeric timing and lifecycle checks.
+Unknown warnings and lifecycle substitutions still refuse; the auxiliary
+warning cannot establish acquisition or guard drop.
+
+Isolation [39811143272/1](https://github.com/rustfs/rustfs/actions/runs/39811143272)
+preserved all 180 original failed files without modification. Independent
+archive audit verified all four nodes' pre/move/post tree, device and inode
+baselines and twenty root-owned 0700 archive parents on their original
+devices. No S3 replay, namespace deletion, old-generation restoration or
+original verdict rewrite occurred.
+
+Exact helper `3012a65` tools
+[39812960916/1](https://github.com/rustfs/rustfs/actions/runs/39812960916)
+passed independent verification of three official ZIPs, 22 original members,
+ten exact native tests, 104 ordered milestones, actual hosted checkouts,
+source/tree/Cargo.lock, optimized ELF and inspector quorum/size contracts.
+Fresh live
+[39817606025/1](https://github.com/rustfs/rustfs/actions/runs/39817606025)
+completed H1 and H2 with five passing node cleanup receipts. Independent
+audit byte-matched all 246 official files and verified nine seed request/ACK
+pairs, eight final versions/markers per topology, one unretried successor
+PUT ACK, retained historical physical generations, actual lock lifecycle
+events and every peer's complete inventory. Stopped physical snapshots match
+the ACK-bound scene before and after all four H1 and sixteen H2 new-shard
+streaming receipts. Final FD counts were H1 vm000 `22 -> 22`; H2 vm000
+`108 -> 96`, vm001 `117 -> 113`, vm002 `105 -> 101`, vm003 `108 -> 104`.
+Both diagnostic branches are independently accepted, with the original
+`BLOCKED/cleanup PASS` unchanged. Precise disconnect-edge ownership,
+distributed release and global absence of leaks are explicitly unproven;
+the complete source DL/DV fixtures were not covered.
+Every successful diagnostic
+remains `BLOCKED/cleanup PASS` with `canonicalCasePassClaimed=false`. Exact
+disconnect-edge ownership, complete native timeout/stale fences, the
+stale-generation controller and full H1/H2 source DL/DV ACK/physical/lock/FD
+closure remain required. Migration acceptance stays 108/158.
diff --git a/docs/validation-migration-implementation-review.md b/docs/validation-migration-implementation-review.md
new file mode 100644
index 00000000..5c62603c
--- /dev/null
+++ b/docs/validation-migration-implementation-review.md
@@ -0,0 +1,123 @@
+# 用例迁移实现审查
+
+审查日期:2026-10-28。冻结源 catalog 提交为
+`524eb5b32c3236497fdd8a9271df0494fa3d9974`,SHA-256 为
+`463fdd02dee43e42fef4f3aacf7506709ce87af535e1a53d91c521bc2e361cad`。
+本次交付范围是 auto-testing 中的测试流程、原断言和证据采集。产品执行结果独立保留。
+
+## 实现和执行结果
+
+| 测试实现状态 | canonical 用例数 | 含义 |
+| --- | ---: | --- |
+| 已实现 | 145 | 各源分支已有执行路径、原断言及证据采集;其中 115 项已有完整 live 审计,30 项运行失败或尚未完整验收。 |
+| 环境条件限制,默认关闭 | 13 | 操作流程、能力检查、原断言和未满足原因已保留;执行需要明确的资源及经过审核的资源适配器。 |
+| 实现部分完成 | 0 | 已补齐 F07、I05 的测试接线和源流程总控。 |
+| 合计 | 158 | 保留全部源 canonical ID。 |
+
+独立 live 状态仍为 115 项 accepted、22 项 retained-failure、4 项 pending、
+4 项 blocked、13 项 disabled。共 43 项尚无完整 live 验收,其中 13 项按约定默认关闭。
+这些结果跨多个不可变候选和历史批次,不构成某个候选通过全部 158 项的产品认证。
+22 项失败需要逐例归因,不能统一宣称为产品缺陷,也不能直接据此认定测试没有迁移。
+
+`validation-migration-review.json` 列出全部 158 个 ID、实现状态、代码/测试引用、
+源分支、缺口及独立 live 状态。`reviewed_files` 固定引用文件摘要,变化后须重新审查。
+文件存在和本地测试通过不代替源语义审查或 live 审计。本文件不覆盖原始结果。
+
+```bash
+python3 -B scripts/validation_migration_review.py --require-complete
+```
+
+此门禁仅因测试实现缺口失败,产品 FAIL 不影响实现计数。13 项环境流程另行输出所需
+适配器清单。产品门禁 `validation_report.py` 仍要求源断言、清理、来源和完整 live PASS。
+
+## F07:持久化临界点
+
+冻结源 `docs/heal-cases.md#F07` / `scenarios/heal-recipes.json` 要求精确本 run 的
+checkpoint/MRF 故障、持久化责任保留、损坏记录保留,以及独立 native fsync/commit
+窗口补证。入口 `persistence.py` 组合以下已存在的能力:
+
+| 能力 | 证明范围 |
+| --- | --- |
+| `persistence.py` / `persistence_mrf.py` | 五种 live 字节损坏、写失败及重扫分支,保留原责任、失败和恢复证据。 |
+| manager checkpoint-page-order / checkpoint-stale-manager | 正常发布和 reload 次序;旧 manager 不能覆盖新 cursor。正常 reload 不冒称提交中断。 |
+| manager mrf-migration-boundaries | 实际 AfterPayload、BeforeManifest、AfterManifest、AfterReadback 四个中断窗口及保留/重试断言。 |
+| manager root-uncertain-publication | 实际终态写入后注入故障,保留 pending owner,重启隔离旧任务。 |
+| `persistence_native.py` | 六个 ECStore CAS/MRF 目录 fsync 故障回归,覆盖更新、首次创建、删除和恢复锚点。 |
+
+checkpoint 通过 `EcstoreDiskAPI::compare_and_update_file` 落盘。manager 的发布次序、
+中断/重启责任与 ECStore CAS 落盘失败分别补证;没有为每个 fsync 发明额外测试要求。
+审查源码为允许的不可变候选 `072b57b37559fbe626aeda8abf61617fdcbdd926` 和
+`1aa9be87d18cf941e5020f19e8622c26ba4b10ac`;完整目录 fsync 组合只允许后者。
+
+`collect_native_persistence` 在执行前核对 native manifest 与 fleet 候选,执行六项回归
+后独立验证并再次核对候选;`native_branch_result` 再核对 manager/native 同候选。
+成功和失败分别写入 `independent-native-persistence.json` 与
+`independent-native-persistence-failure.json`,原始 native 日志保留在 `native-persistence/`。
+缺少 artifact 时只允许原诊断子集,不能宣称完整 native coverage。
+
+使用当前 auto-testing 环境时,由现有 workflow 传入以下目录:
+
+```text
+VALIDATION_HEAL_MANAGER_DIRECTORY/
+ tool-run.json
+ manager-tools/
+ manager-evidence/
+VALIDATION_HEAL_NATIVE_DIRECTORY/
+ tool-run.json
+ native-tools/
+ native-evidence/
+```
+
+伴随 workflow 在精确目录 fsync 候选上下载已有 native artifact,并将目录传给入口。
+同一候选的原生结果仍是独立 prerequisite;不代表 H1/H2 fleet 命中了内部窗口,不声称
+物理断电或每个 file fsync hook 已执行。F07 的完整独立 live 审计待完成,canonical 门禁保留。
+
+## I05:源流程总控和代次证明
+
+`lock_source.py` 复用 `lock_timeout_runner.py`、`lock_timeout.py`、`lock_successor.py`
+及 `lock_observer.py`。默认 `source-plan` 顺序执行 timeout/H1、timeout/H2、
+queued-successor/H1、queued-successor/H2,每次使用独立 run ID、bucket 和证据目录。
+任何分支、证据链或清理失败立即停止后续部署,保留原请求、ACK、日志及停机现场。
+
+部署前只读验证同一成功 hosted artifact 中的三个精确源码 native 子集:
+
+| 子集 | 证明范围 |
+| --- | --- |
+| I05 owner/unlink/stale | 历史 read-repair owner、取消 physical unlink owner 和原有 stale final rename 回归。旧 stale 测试只检查 ETag。 |
+| G02 explicit-version append | 真实缺片重建在 final rename 屏障暂停;新版本 ACK 和物理 tail drain 后释放;检查类型化结果、全部存活 body、版本、dataDir 和逐盘 shard。 |
+| G03 explicit-version delete | historical/current 删除与最终提交竞态,保留上述完整内容和物理代次断言。 |
+
+G02/G03 的较强证明来自已有原生测试,不给旧 ETag 测试补写能力。两组覆盖 4/2 与
+16/4,分别保留 6 与 12 条有序屏障里程碑。append 可能安全修复原选定版本并保留新版本,
+不把所有类型化结果统一描述为 stale 拒绝。候选固定为
+`757b751482bb970bc321e92059690a7698554358`;三个 proof 的 package/source tree/
+Cargo.lock/二进制/工作流 attempt/helper/contract 身份必须一致。
+
+原生前提可单独检查,命令不执行下载的二进制:
+
+```bash
+python3 -B scripts/validation_heal/lock_source.py \
+ --directory "$VALIDATION_HEAL_NATIVE_DIRECTORY" \
+ --candidate candidate.json --testing-commit "$AUTO_TESTING_SHA"
+```
+
+fleet 控制器保留 DV 历史版本和 DL 单次并发请求/ACK ledger,继续核对所有 peer 的
+完整对象/版本、停机物理分片、锁事件和有界 FD 收尾。DL 是工作负载语义,不是新存储格式。
+新证据链关联请求写出、超时/排队、TCP 断开、原 guard drop、successor 获取和观察结束;
+对原始 bytes 逐一固定 SHA-256,核对请求/lock owner/binding 和同一协调者时钟顺序。
+不会比较不同主机 monotonic 时钟,也不把本地 guard drop 当成分布式 owner 释放。
+
+`source-plan-summary.json` 记录四个源分支的 fleet/native 证明范围;
+`sourceProceduresComplete` 只有 H1/H2 四个实例完整且清理通过才为 true。单独选择 timeout
+或 queued-successor 仍是诊断子集。全部控制器完成也不自动增加 canonical PASS;完整
+source-plan 的独立 live 审计仍待执行。
+
+## 失败归因和后续验收
+
+C11 的旧 metadata、checkpoint、MRF replay 和 native incarnation 均有执行路径;
+J02 的混合 outcome、dry-run、deferred 和 unknown-negative 均有控制器。其历史产品
+结果和证据限制保留,尤其不能修改 J02 的 Failed/Unknown oracle 来获取全绿。
+
+G01/G02 的失败 PUT 不重试,未知 mutation 不算成功。B03/B04 仍缺精确 mount/切换
+适配器,在部署前阻断。今后按产品缺陷、环境前提、证据缺失和测试实现问题逐项归因;
+有明确测试实现问题才重开实现缺口。实现交付完成不代替候选包的后续回归与产品验收。
diff --git a/docs/validation-migration-map.md b/docs/validation-migration-map.md
new file mode 100644
index 00000000..e92ecad9
--- /dev/null
+++ b/docs/validation-migration-map.md
@@ -0,0 +1,2861 @@
+# Release validation case migration
+
+Source: `rustfs-release-validation/scenarios/catalog.yaml`. The generated
+`validation-cases.json` preserves all 158 canonical IDs, source statuses,
+assertions, probes, capabilities, and target environments. Its source commit
+and SHA-256 bind the snapshot to the source catalog. `reuse_candidates` lists code to inspect;
+it is not a coverage claim. A case's run result remains `unverified` until a
+live run proves each source assertion on the required topology.
+
+## Current implementation scope (2026-10-28)
+
+The migration delivers test procedures and original oracles in auto-testing.
+Candidate live failures are recorded independently from missing test code.
+The explicit [implementation review](validation-migration-implementation-review.md)
+and `validation-migration-review.json` record **145 implemented cases,
+13 default-disabled environment procedures and zero partial implementations**.
+The 13 procedures require reviewed resource adapters to execute. F07 composes
+same-candidate manager interruption and ECStore directory-fsync evidence. I05
+runs timeout and queued-successor on H1/H2 in one source plan, with separate
+same-artifact native owner and full-content/physical generation checks. All
+30 implemented cases without full live acceptance retain their candidate
+failures or pending evidence independently. Source-path review and local
+contract tests do not certify every live branch.
+
+Run `python3 -B scripts/validation_migration_review.py --require-complete` for
+the independent counts and implementation gate. It exits zero when the source
+implementation has no recorded gaps; retained product FAIL does not change
+that decision. `validation_report.py` continues to enforce original assertions,
+cleanup and provenance for product acceptance. Unknown failure causes remain
+unclassified. Detailed execution prerequisites and evidence scopes are in the
+[implementation review](validation-migration-implementation-review.md).
+
+## Current live acceptance and retained evidence
+
+Count reconciliation (2026-10-23; updated 2026-10-28): the append-only ledger
+`canonical-migration-ledger-20261023-b08-39915022106.json` follows the
+B01 ledger and attaches the complete B08 live acceptance plus the latest B02
+failure evidence. The B02 complete acceptance below is a later independent
+record; it does not replace that retained failure or replay all 158 cases on
+one candidate.
+
+All 158 canonical IDs are retained. Independent complete live acceptance is
+**115/158**. The current review retains 22 live failures, four pending
+native/live race acceptances, four orchestration/evidence blockers and 13
+environment cases disabled by default. G01 and G02's actual failed PUTs are
+included in retained failures; historical ledger bytes are unchanged. These
+30 unaccepted active cases are a live acceptance backlog, not 30 missing test
+implementations. Counts span immutable candidates and historical audits;
+diagnostic, native, isolation and partial results do not increment PASS.
+
+| Reconciled case | Complete live run | Required coverage | Original result SHA-256 |
+| --- | --- | --- | --- |
+| C02 `heal.data-shard-bitrot` | [39281951263/1](https://github.com/rustfs/rustfs/actions/runs/39281951263) | Eight H1/H2 branch instances, 3,334 archive files | `009c96a4bbd1b8c711009c0223519f6f12d8762890d597db749f9e415ca23f1a` |
+| C03 `heal.parity-shard-bitrot` | [39286997859/1](https://github.com/rustfs/rustfs/actions/runs/39286997859) | Six H1/H2 branch instances, 2,904 archive files | `da2df2e15aa973b6a04a0cb40491413db17426b63d6367f11682ef579b84235a` |
+| C05 `heal.missing-or-corrupt-xlmeta` | [39322217398/1](https://github.com/rustfs/rustfs/actions/runs/39322217398) | Ten H1/H2 DB/DV instances, 3,132 archive files | `489b8179ee90a7412b223e669db8fd4f6520b9e69051cbf927b6f33299de423b` |
+| I04 `heal.queue-pressure-and-admission` | [39354651483/1](https://github.com/rustfs/rustfs/actions/runs/39354651483) | Four branches per topology, six exact durable tasks, 5,565 archive files | `4dcde2f34bb7df52f03f5a3ec1e87663145a7134d3ef1a513fda2da52d1709a2` |
+| B05 `heal.multiple-shard-loss` | [39843970314/1](https://github.com/rustfs/rustfs/actions/runs/39843970314) | All 38 DB/DV and H1/H2 instances, 9,059 archive files, ten node cleanups and four package purges | `2624f43fc8581eb1b533a477b1a869f9c72208861d7a268ea35ae8f36153fd7e` |
+| E04 `heal.scanner-cold-object-repair` | [39863505276/1](https://github.com/rustfs/rustfs/actions/runs/39863505276) | All six H1/H2 instances, 2,307 archive files, five node cleanups and four package purges | `8bfcfb03bfa5cc88649e98c4f9a6fa7a27d64f9fd0cdbe8d851f57fc00bc0702` |
+| A05 `heal.admin-overlap-and-force-start` | [39892106641/1](https://github.com/rustfs/rustfs/actions/runs/39892106641) | All eight H1/H2 overlap and force-start branches, 20 cleanup receipts | `4a8073b6fe129a5bf696ffbc74b0024f1b23e66232606df13c7f606e0511b036` |
+| B01 `heal.disk-loss-and-repair` | [39899825839/1](https://github.com/rustfs/rustfs/actions/runs/39899825839) | H1/H2 original-drive-return and offline-ACK repair, 272 fixture ACK/request pairs, five cleanups | `fe89fbb0e4e2d93aa5c4408b9218c66d6caa9120f067f3aca3560f4ca360c4e5` |
+| B08 `heal.below-write-quorum` | [39915022106/1](https://github.com/rustfs/rustfs/actions/runs/39915022106) | H1/H2 write/lock quorum loss, H1 lock-loss not applicable, 3,463 archive members, ten cleanups | `b5fbc87b2a9e7dab2b07301c8703f0d1733ee44f11ef984b3bce55b65b5df87e` |
+| B02 `heal.disk-readonly-and-permission` | [39927051614/1](https://github.com/rustfs/auto-testing/actions/runs/39927051614) | H1/H2 read-only and permission-denied, same-process gate release, exact shard restoration and five cleanups | `ba2f2569f3799a2aed014c9b89d37a80075774e76dfbfd1b3a06c04c38cb533e` |
+
+| Acceptance state | Cases | Remaining requirement |
+| --- | ---: | --- |
+| Independently accepted complete live PASS | 115 | Preserve original provenance and complete results. |
+| Retained live failures with existing executors | 22 | Preserve original results and attribute the cause; product failures do not imply missing test implementation. |
+| Native and live race acceptance pending | 4 | A09 and G03–G05 await complete live acceptance; existing native/runtime paths retain their candidate gates. |
+| Orchestration or evidence blocked | 4 | C11/F07/I05/J02 have source execution paths awaiting independent complete live acceptance; no implementation gaps remain in the current review. |
+| Environment gated, disabled by default | 13 | Control flows, oracles and unmet reasons are implemented; dedicated resource adapters and real execution remain conditional on the required resources. |
+
+The current delivery keeps the 13 environment cases disabled with explicit reasons. The other 30 unaccepted cases retain their original live states independently of their completed implementations. C11/F07/I05/J02 have explicit execution routes, while canonical admission stays blocked until independent complete live acceptance. J02's real four-disk regression reproduced scheduler-level whole-batch retries overwriting an earlier repair receipt. RustFS commit `7951bb629821246868ee3761b321dd07c06db45c` retains exhausted Admin batch results; its 210 manager tests and five process-isolated receipt integrations pass.
+
+### Native prerequisites and runtime admission (2026-10-26)
+
+Hosted-only tools [40086026324/1](https://github.com/rustfs/rustfs/actions/runs/40086026324) completed successfully on candidate `757b751482bb970bc321e92059690a7698554358`, auto-testing `002473b51f5ceeccadedf7325a1b1691e4c71dcd` and driver `b1f7fb276941d7128b5ddbbda9c3e6536931c89d`. Its immutable package is [40081956660/1](https://github.com/rustfs/rustfs/actions/runs/40081956660), SHA-256 `1a52387a8cbdef586be2616b02298f351aec4ac2d5c0f7a76d094409c89c0b3c`. Independent read-only verification checked the candidate source tree, registered source-file digests, Cargo.lock, executable digest, ten exact nonignored passing test invocations, all 104 ordered milestones and the inspector identity. The original artifacts and audit are retained in the migration evidence store. No downloaded native executable was run during this audit.
+
+The registry exposes eight native prerequisite groups: A09, G01–G06 and part of I05. This is separate from runtime admission. The pinned helper admits A09, G05 and G06 on this candidate; it intentionally rejects G01–G04 on the same source before bootstrap because their runtime candidate contracts remain pinned to other reviewed sources. G01–G03 may use their reviewed version candidate `bd0671ba781d8ebf509f4f03533228dfb44496f1`, and G04 uses marker candidate `8380466f3a27c3c3ef4115084178a0ab99a78f32`, each with its own exact package and matching helper/tools. Alternatively, a separate source review and matching tools build must precede extending runtime admission. Do not infer admission from registry membership or mix artifacts from different candidates or helpers.
+
+All ten native tests pass. G06 subsequently passed all eight H1/H2 public branches in [40427815826/1](https://github.com/rustfs/rustfs/actions/runs/40427815826); an independent audit verified its ACK, physical, peer S3, overlap and 20 cleanup receipts, adding one canonical PASS. A09 and G01/G03–G05 still need their complete source branches and clean finalization. G02's native/runtime prerequisites were admitted, but its first live run failed before completing the required matrix and is recorded below. I05 now composes the timeout/disconnect controllers and separate full-content/physical native generation checks through `lock_source.py`; complete independent source-plan live acceptance remains pending.
+
+### G02 retained live failure (2026-10-27)
+
+G02 `heal.race-versioned-put` run [40444207551/1](https://github.com/rustfs/rustfs/actions/runs/40444207551)
+used RustFS candidate `bd0671ba781d8ebf509f4f03533228dfb44496f1`,
+auto-testing `7094c16a5a5cf3b0f818c7be463f3bc0f8fd7645`, and the audited
+candidate-bound native tools from [40311010126/1](https://github.com/rustfs/rustfs/actions/runs/40311010126).
+The H1 `new-version-vs-historical-heal` branch failed at foreground ACK 000003:
+PUT `objects/0256.bin` returned HTTP 503 `ServiceUnavailable` after 5.026 s,
+with zero retries. The service log binds request
+`2b5289c8-0faa-472d-b5c6-fe9db521659d` to `storage_error_code=Some(Lock)` and
+records a contemporaneous `rename_data` operation pending for over 5 s on
+`/data/rustfs4`. This supports a Heal/PUT lock-contention diagnosis; the log
+does not correlate that rename operation to the request ID, so it is not a
+complete call-level causal trace. This was a real failed PUT, not a recorder
+false positive or successful ACK.
+
+The branch result is `FAIL/cleanup FAIL`; its last-ACK receipt records
+`unresolvedMutation=true` and
+`lastACKPhysicalAuthorityVerified=false`. Evidence-based recovery run
+[40444795627](https://github.com/rustfs/rustfs/actions/runs/40444795627)
+isolated the active namespace. It did not restore the scene, delete the
+namespace or rewrite the verdict, and it claimed no canonical PASS. The
+original failed scene remains retained and must not be replayed in place.
+
+The Auto Testing PR [#113](https://github.com/rustfs/auto-testing/pull/113)
+contains the migrated G02 executor and remains Draft. RustFS PR
+[#8455](https://github.com/rustfs/rustfs/pull/8455) completed its distributed
+4-node/4-disk E2E in run [40631115072](https://github.com/rustfs/rustfs/actions/runs/40631115072)
+on candidate `4454b029dd462e0f58f1b3acc0f4cd62137c24d6`: 92 tests passed and
+873 were skipped. The PR includes Heal lock-wait observation and extensive
+Heal regressions, but the E2E result does not bind those changes to the failed
+PUT request or establish that the G02 lock contention is fixed. It is
+supporting candidate evidence, not an immutable G02 package or case result.
+Retest only after reviewing a concrete source correction, building and
+auditing a package pinned to its exact candidate SHA with matching
+tools/helper, and confirming the current Auto Testing fleet is safe for a
+fresh run. A complete H1/H2 matrix, physical and peer S3 verification, and
+clean run-owned finalization are required before G02 can add a canonical PASS.
+Until then the 115/158 total is unchanged.
+
+### Hosted runner inspection (2026-10-26)
+
+Read-only [40256930895/1](https://github.com/rustfs/rustfs/actions/runs/40256930895) completed on a GitHub-hosted runner with driver `31c9d85645c1a8051562983f18631098b9bc34e7`. At `2026-10-26T02:00:24Z`, GitHub reported the only matching `smoke-testing` runner, `rustfs-smoke-testing` (ID 139653), as **offline**, with `busy=false`. The report retained two queued fleet jobs, including the Functional Chain summary and the health collector, plus pending workflow runs without jobs. This establishes the runner registration state at observation time; it does not diagnose the host's service, disk, network or power state.
+
+The new `validation_runner_inspect_only=true` dispatch path uses a separate workflow concurrency group, reads only GitHub API metadata and uploads an incomplete report if API access fails. It rejects simultaneous candidate, build, fleet, tools or recovery inputs; all fleet jobs were skipped in the real inspection. No queued workflow was cancelled or replaced, no test-node connection was made, and `mutated`, `cleanStateCertified`, `sharedWorkflowIdleCertified` and `canonicalCasePassClaimed` remain false. Live resumption requires a working maintenance entry to restore this runner, followed by a new resource inspection and the existing retained-scene and shared-workflow admission checks.
+
+### Interrupted G06 evidence and recovery boundary (2026-10-26)
+
+The re-registered `rustfs-smoke-testing` runner (ID 139936) was online in hosted inspections [40277791903](https://github.com/rustfs/rustfs/actions/runs/40277791903) and [40282750869](https://github.com/rustfs/rustfs/actions/runs/40282750869). Resource inspection [40278133051/1](https://github.com/rustfs/rustfs/actions/runs/40278133051) observed the current auto-testing fleet's 16 independent endpoints. Neither inspection certifies global workflow idleness or restores an interrupted scene.
+
+G06 [40281440070/1](https://github.com/rustfs/rustfs/actions/runs/40281440070) was cancelled during its selected-case step. The only annotation is `The operation was canceled.`; its cause is unconfirmed. The workflow uploaded no original artifact. Read-only collection [40284040202/1](https://github.com/rustfs/rustfs/actions/runs/40284040202) subsequently retained 1,777 original files (38,765,135 bytes), independently checked against every collection-receipt size and SHA-256. Collection provenance remains distinct from the original run; its unfinished result stays `RUNNING/PENDING`, unaccepted.
+
+H1 `copy-source` finalized as `NOT_OBSERVED` with cleanup PASS. H1 `copy-destination` retained all 258 seed request/ACK pairs and its one COPY pair, complete last-ACK physical authority and 1,028 streamed part proofs. It restarted before final peer S3 inspection, then was interrupted. Its recorded public-call overlap is false. No H2 or later source branch was completed, so this evidence adds no canonical PASS.
+
+`interrupted_race.py` admits only this bounded H1 COPY boundary: one unchanged unfinished case, one finalized predecessor, complete deterministic SDK journals, recomputed ACK-derived final state and physical/streaming proofs, and no later operation. Recovery checks all five original owner markers before stopping the exact owned unit, then requires the current stopped business scene to match the archived last-ACK scene. Existing same-filesystem private isolation preserves the original bytes, backup files, inodes and credentials without reading credential contents. Drift refuses isolation; no old-generation restoration or SDK replay is permitted. A new recovery receipt never rewrites the original verdict or supplies missing canonical assertions.
+
+The driver may explicitly select `validation_cleanup_retained_evidence=true` together with an exact failed cleanup run. It verifies that run/attempt and same-runner storage ownership, copies through no-follow file descriptors, checks every original and copied digest and records a new collection identity. This is not an original-run artifact fallback. The ordinary artifact-based recovery path remains the default.
+
+Isolation [40286516720/1](https://github.com/rustfs/rustfs/actions/runs/40286516720) completed with helper `8efc57d7ed89531a8f63cc4f98d06dafa2361499` and driver `3e337bad97a0263fa663f7909b33f247d6f76f17`. Independent audit verified official archive SHA-256 `11f691947bcbf6d707f0ed04ff7bfd625eeb4994221ad0f690bcc8c63cf43645`, all 1,777 original file bytes, the unchanged unfinished-case digest, stopped owned service and identical pre/move/post baselines for all five trees. All archive parents are root/root 0700 on their original filesystems. The disposition is ISOLATED; no namespace was deleted, no backup restored and no canonical verdict rewritten. Follow-up inspection [40287577171/1](https://github.com/rustfs/rustfs/actions/runs/40287577171) observed 16 eligible endpoints, inactive standard services, no port 9000 listeners and no active validation service. The original failed unit remains recorded; inspection does not certify global idleness.
+
+The original COPY SDK calls lasted approximately 23 ms and 17 ms. Their nearest running samples began before those calls or finished after them, so the frozen strict interval oracle correctly refused overlap. G06 schedules observation from botocore's actual `before-send.s3` notification inside the SDK call, after client preparation and durable request intent. The callback only queues a timestamp; it never waits, delays a response, repeats a mutation or alters the SDK call interval. A qualifying original-token running sample must still fit strictly inside one actual acknowledged SDK call. Real local HTTP/SDK tests cover nested running acceptance, finished-task refusal and response completion without any observer. The previous polling scheduler fails the positive regression.
+
+Complete G06 replay [40427815826/1](https://github.com/rustfs/rustfs/actions/runs/40427815826), pinned to helper `7094c16`, passed all eight H1/H2 public branches. Independent replay with the executing helper recomputed all six seeded branches' deterministic journals, original seed properties/tags, final manifests, physical transitions, complete streaming receipts, full peer S3 inventories, terminal outcomes and public overlap. All 20 cleanup receipts match the reserved hosts. G06 is now canonical PASS; the earlier failed run [40299213418/1](https://github.com/rustfs/rustfs/actions/runs/40299213418) remains retained unchanged.
+
+The next executor reuses the existing same-token status transport for G06, preserving its strict overlap oracle, and records read-only endpoint resolution before allocation and restarts. G01/G02 separately restore the frozen source's per-role overlap rule while requiring every PUT ACK and complete concurrent reader evidence. New exact-helper tools and complete H1/H2 live evidence remain required for acceptance.
+
+The source's `heal_h1_concurrency_batch.py::observe_race` at clean commit `524eb5b32c3236497fdd8a9271df0494fa3d9974` requires every planned request to have an ACK and returns `bool(matches)` for its nested running samples. The frozen G06 tag branch has exactly 16 distinct serial updates. The migrated controller incorrectly required all 16 calls to overlap. G06 tag aggregation now restores the source's requirement of at least one qualifying call, while retaining all 16 original requests, all 16 ACKs, exact intervals, final tags and full physical/S3 checks. The evidence retains every per-call overlap flag and explicitly records the required aggregation, so no match still yields NOT_OBSERVED. Other race aggregation is unchanged. Real local SDK tests verify one matching tag call plus fifteen nonmatching calls, all 16 durable ACKs and complete refusal when every sample is finished. The preceding migrated helper fails that positive test; no historical verdict is rewritten.
+
+### B08 complete live acceptance
+
+[B08 run 39915022106/1](https://github.com/rustfs/rustfs/actions/runs/39915022106) is independently accepted on candidate `1a3f038c90704f9ad9693eddcf7d861c9530f13c`, package `39898678364/1`, tools `39914732144/1` and helper `a3d3c62a9f6d29667a1a2b57d13f5cb9e166de3f`. The official 3,463-member archive has SHA-256 `59ddf692cdca495abcd37a1affacf25e4cd3da24c931509bb9b4facc386c2f11`; the independent audit has SHA-256 `63f143d278c5e6a43f0068a31c692cae1064fd2612b54060198c448f52b7e640`. H1 read-present/write-absent and H2 read-present/write-absent plus lock-quorum-absent passed; H1 lock-quorum-absent is explicitly NOT_APPLICABLE under the candidate's in-process local manager. The audit verified all peer inventories, unique PUT absence, physical target restoration, streaming shard proofs and ten cleanup receipts. The prior B08 failure remains retained in the append-only ledger.
+
+### B02 earlier live failure remains retained
+
+[B02 run 39916358715/1](https://github.com/rustfs/rustfs/actions/runs/39916358715) used the same immutable candidate package `39898678364/1` and helper `a3d3c62a9f6d29667a1a2b57d13f5cb9e166de3f`. The complete official artifact `12485117739` has SHA-256 `435fe989937adbad376c93506dfe7db7eec914f3fafb68ea98068d8c79ea5d7a`; its case result SHA-256 is `a92ab5c1e312f0e5f25b83f7f8802b1de3e43c5fd93bff05dd3651c9d1e1f978`. Read-only passed on H1 and H2, cleanup passed, and both permission-denied branches failed with `faulted version lacks a repaired responsibility`. The independent artifact audit `B02-live-39916358715-independent-failure-artifact-audit.json` (SHA-256 `8c9b2e03e437542f1d3bd6894cb8dab225e46e0e9f65aecf4b16f4a3db5d27eb`) confirms the follow-up queried the expected `objects/0017.bin` and returned `progress.objectsHealed=1`, while terminal responsibility reports `verified_healthy`, `healed=0` and `unchanged=1`. Physical snapshots show the faulted shard absent after the denied attempt and present with its baseline SHA after the writable follow-up sequence. The runner stops and restarts RustFS after releasing the gate and before that follow-up; there is no physical snapshot between service startup and the new Admin token, so the artifact cannot attribute the restoration to that token or establish a terminal receipt classification bug. The strict source assertion remained unsatisfied in that run; this original failure record remains unchanged.
+
+### B02 complete live acceptance
+
+Run [39927051614/1](https://github.com/rustfs/auto-testing/actions/runs/39927051614) independently passed both source assertions across H1/H2 read-only and permission-denied branches. It used candidate commit `1a3f038c90704f9ad9693eddcf7d861c9530f13c`, package `39898678364/1` (SHA-256 `33f97e40add31ebe9bc4e923f68bc11d2e8855b4eedf5aad7bc8fce1f07da9d6`), tools `39925186107/1` and helper commit `c98028b697f8f9349a2faca277c14810d9b3e466`. Official artifact `12490700181` has SHA-256 `ba2f2569f3799a2aed014c9b89d37a80075774e76dfbfd1b3a06c04c38cb533e`; the independent complete audit has SHA-256 `6e7a2c9e21ed3bd031d7189da0c2d8cfd607c56626745cb4a797e0099070771b`.
+
+For each permission-denied branch, the initial request was rejected before task admission and had no client token. After the gate cleared, candidate process birth identity remained unchanged and the pre-token physical snapshot matched the denied-attempt snapshot. A fresh Admin token then repaired the exact planned shard to its baseline size and SHA-256. The read-only branches also restored their exact planned shards. S3 inventories matched, all five H1/H2 cleanup receipts passed, and both source assertions were present as PASS. This adds one canonical acceptance while retaining the prior failed run.
+
+A05 scoped live [39875125045/1](https://github.com/rustfs/rustfs/actions/runs/39875125045)
+was interrupted by the self-hosted runner's exhausted filesystem. The official
+check annotation reports `No space left on device` while writing the runner's
+worker log. The selected-case step remained in progress in the completed job;
+cleanup and artifact upload stayed pending, and the official artifact listing
+contains no files. Neither product failure nor clean finalization can be
+concluded from that interruption. Its fixed candidate `c95bb396`, package
+`39867606969/1` and matching tools `39874908888/1` retain their independent
+byte/provenance audits. The J01 chain stopped before dispatch when A05 evidence
+collection refused the empty artifact listing. Its released-client fix remains pending a fresh live acceptance; the B01
+explicit disk-error correction is covered by the accepted B01 run above. The administrator has since deleted the interrupted runner temporary evidence;
+the user explicitly accepts retaining this attempt as unaccepted. A05 later
+passed on a separate complete run 39892106641; that pass does not reconstruct
+or change the status of run 39875125045. No product verdict or cleanup PASS is
+inferred for the deleted scene.
+
+### J01 startup window diagnosis (2026-10-23)
+
+J01 run [39929436131/1](https://github.com/rustfs/rustfs/actions/runs/39929436131)
+used the current auto-testing fleet, candidate `1a3f038c90704f9ad9693eddcf7d861c9530f13c`,
+and helper `c98028b697f8f9349a2faca277c14810d9b3e466`. Its official artifact
+`12491201856` has SHA-256
+`bd703e00ed88297794a6adb72760ede313f7e9177a30978cac45eb60eddbae5d`.
+Enabled-idle, disabled, missing-peer, recovery and all four cleanups passed;
+the complete case remains FAIL because H1 and H2 did not expose the required
+raw/rc `uninitialized` window. The artifact and original verdict remain intact.
+
+The H1 startup journal narrows the cause. RustFS began listening at
+`09:07:12.377958Z`. The background-heal endpoint returned HTTP 503 with
+`server context is not ready` at `.400036Z` and `.411255Z`. Heal runtime
+initialization followed at `.438696Z`, and `server_ready` at `.438747Z`. The
+first successful signed status response in the collector evidence arrived at
+`09:07:12.514298Z` and already reported `idle`. Thus the startup listener
+rejects status requests until the heal runtime is initialized; the source
+required HTTP 200 `uninitialized` state is not externally observable on this
+candidate during that transition. Connection refusal or pre-ready 503 remains
+non-evidence. Keep this branch `NOT_OBSERVED` and the canonical J01 result
+unaccepted unless a product/source change supplies the required truthful
+observable window; do not weaken the source oracle or promote this run.
+
+### B03 current auto-testing result and mount-admission diagnosis (2026-10-23)
+
+B03 run [39931810677/1](https://github.com/rustfs/rustfs/actions/runs/39931810677)
+used candidate `1a3f038c90704f9ad9693eddcf7d861c9530f13c`, package
+`39898678364/1` (SHA-256
+`33f97e40add31ebe9bc4e923f68bc11d2e8855b4eedf5aad7bc8fce1f07da9d6`),
+and auto-testing commit `dd53393e6ee713f5831254be92a7a90ea40bdf62`.
+Official artifact `12491823352` has SHA-256
+`df3770cf8f2b9a8ba22e6cc2e5064f96c030121e5bfe105f5de2a894adacad01`.
+All four H1/H2 × DB/DV instances retained FAIL and cleanup PASS. Their
+registered 90-second format observations never found the replacement
+`format.json`; candidate journals repeatedly report
+`Replacement target is not ready: target mount admission failed`.
+
+I re-downloaded the official artifact and checked its runner storage preflight,
+H1 deployment preflight, replacement receipt, post-start format observations,
+and candidate journal. The runner had about 258 GB free at preflight, and the
+four parent mounts were four distinct NVMe devices; this rules out the old
+disk-space concern for this run. The deployment record lists the actual
+endpoint roots under each `/data/rustfsN` mount, but does not establish a mount
+lease for those run-private endpoint paths. The service journal confirms that
+`renew_disk` attached the empty endpoint and queued the replacement request,
+which then failed admission. Thus the missing capability is endpoint mount
+binding and safe mount switching, rather than capacity or missing replacement
+bytes.
+
+The runner gives RustFS endpoints under
+`/data/rustfsN/heal-validation//data`. The parent `/data/rustfsN`
+mounts are independent devices, but the candidate's replacement admission
+requires a valid mount lease for the exact target endpoint and each local
+sibling endpoint, plus target physical-device IDs disjoint from `/` and those
+siblings. The run-private endpoint directories do not meet that exact-mount
+contract. The service logs show the actual sequence: `renew_disk` attached the
+empty endpoint and submitted an automatic replacement request; the task then
+started and repeatedly failed with `Replacement target is not ready: target
+mount admission failed`. `RUSTFS_HEAL_AUTO_HEAL_ENABLE=false` only disables the
+periodic scanner; it does not suppress this replacement request generated by
+`renew_disk`. The executor's `wait_format()` therefore expired after 90 seconds
+and the explicit manual `run_task()` call was never reached. The source
+procedure and strict UUID/set-slot and physical-byte oracles remain unchanged;
+do not lengthen the wait, copy from quarantine, or infer a product verdict from
+this environment mismatch.
+
+Before another canonical attempt, add a read-only preflight that checks every
+local endpoint's exact mount lease, mount ID, filesystem identity, and physical
+device separation from `/` and its local siblings. The replacement adapter
+must then demonstrate a candidate-supported way to switch the target endpoint
+to a blank mount, let the candidate obtain a new lease and initialize it before
+the manual token, while keeping the old archive unreadable to the service. A
+bind mount may be used only if the adapter proves this full lease switch and
+all candidate device-identity checks; a bind mount by itself is not evidence.
+The Functional Chain now runs a read-only `replacement-admission-preflight`
+before `stop_existing`, candidate package download/install, the normal
+parent-mount preflight, reservation, service start, or fixture seed. It
+inspects all H1/H2 endpoints and records each exact mountpoint, mount ID,
+filesystem identity, nearest existing ancestor, physical-device IDs and
+root/sibling overlap, along with whether a reviewed mount-switch adapter is
+registered. If admission is unavailable, the result is `BLOCKED` with
+candidate package and client provenance, `binary_sha256: null`, and an
+explicit `NOT_REQUIRED_NO_MUTATION` cleanup disposition; the existing RustFS
+package and service remain untouched. If admission passes, `Fleet.deploy()`
+repeats the read-only check immediately before reservation. The current runner
+reports ordinary run directories and no adapter, so B03 stops before bootstrap
+and any 90-second format wait. Once an adapter is installed and independently
+reviewed, the gate must pass before recording mount identities before and
+after replacement and rerunning the full H1/H2 DB/DV matrix. The source oracle
+and prior FAIL evidence remain unchanged.
+
+The new pre-deployment gate was exercised on the current auto-testing
+environment in B03 run
+[39942624978/1](https://github.com/rustfs/rustfs/actions/runs/39942624978),
+using the same candidate package and helper commit
+`49443e7559578cba8be285ed723bb68dfbaf8656`. H1 and H2 both returned
+`BLOCKED` before deployment: all endpoint paths were absent ordinary
+directories beneath the four distinct NVMe parent mounts, with no exact
+endpoint mount IDs or filesystem identities and no reviewed mount-switch
+adapter. Root and sibling physical-device separation passed, but that does
+not satisfy the endpoint lease contract. The case result records
+`execution_stage=pre-deployment`, `binary_sha256=null`,
+`mutationStarted=false`, deployment and fixture seeding skipped, and
+`cleanup=PASS` with `NOT_REQUIRED_NO_MUTATION`. The case result SHA-256 is
+`68ed1713fa3eab0e0d2d972369fca4af00ae58bdb4e6a1e64b072cfae6194621`; the
+preflight summary SHA-256 is
+`fb47d91ee77d21104be7a400bbfda1aa0d73b30e090476c853a9a53b5036ea12`.
+This is verified BLOCKED evidence only: it adds no canonical PASS and does
+not replace the retained B03 live failures. The next valid B03 attempt still
+depends on installing and reviewing the mount-switch adapter.
+
+B04 `heal.disk-replacement-unformatted` now uses the same replacement
+admission contract. Its runner checks H1/H2 exact endpoint mounts before
+stopping any service or installing the candidate, and its executor repeats
+the read-only admission immediately before reservation. If the current
+ordinary-directory layout remains in place, B04 is recorded as `BLOCKED`
+with `binary_sha256=null`, `cleanup=PASS`, and
+`NOT_REQUIRED_NO_MUTATION`; no blank or foreign-format fixture is created.
+If the first gate passes but the reservation-time recheck observes environment
+drift, the executor records the same `BLOCKED` verdict while retaining the
+installed candidate provenance, skips reservation and fixture seeding, and
+finishes cleanup. That post-bootstrap block is distinct from the
+pre-deployment `NOT_REQUIRED_NO_MUTATION` result and cannot claim canonical PASS.
+
+B05 acceptance uses candidate `7ed6c9320db491c84b54e8232152d7b1282c1b3f`, helper `76dbf7029e82c1e3878c9160090804d4afc38078`, package `39827676705/1` and tools `39841369555/1`. The complete official ZIP digest is `4eb3405af1f47583cd7c2d8e5b9ce9fa6910b76665df529d7dfbe4f1399a20d8`. Independent recomputation covers seed ACKs, full versions/markers, the actual 2+2 and 12+4 role/loss matrices, every endpoint's full online membership before each required boundary, sequential faults, one unique START/token per instance, two identical terminal outcome queries, strict integer scope, exact repaired/healthy responsibility, all physical shard and bitrot proofs, complete S3/HEAD/Range evidence before finalization, ten private-node cleanups and four package purges. No backup restoration was used. Original failure `38200772548` remains unchanged; its vm001 offline root cause is not established by the later pass.
+
+E04 live [39842352595/1](https://github.com/rustfs/rustfs/actions/runs/39842352595) retains FAIL/cleanup PASS. All 1,402 official files, three H1 branches and five node cleanups were independently verified. H2's healthy Scanner task completed on vm002 while the exact node3 token query was forwarded to vm000 and returned notFound. RustFS commit `553be4b4c62ae0c590e82f91817bc018bdfdc660` lets authenticated original local automatic-task tokens return the source/path-bound in-memory snapshot; Admin/Internal/unknown/alias identities retain coordinator routing. Fixed candidate `9d90884c304dcfbf97057c263c6f2ee110230ce7` adds only that reviewed patch to `7ed6c932`, with unchanged Cargo.lock. Package [39851206784/1](https://github.com/rustfs/rustfs/actions/runs/39851206784) and matching tools [39860218641/1](https://github.com/rustfs/rustfs/actions/runs/39860218641) passed independent complete provenance and byte audits.
+
+The subsequent E04 live [39860501067/1](https://github.com/rustfs/rustfs/actions/runs/39860501067) retains FAIL/cleanup PASS. All 1,795 official files, H1's three branches, H2's cycle-boundary branch, five node cleanup receipts and four package purges were independently verified. H2's missing-shard and bitrot branches failed attribution: real repair tasks ran on remote disk workers while complete Scanner cycles were logged on vm003. The old same-node cycle requirement omitted both exact tasks and queried later healthy tasks. Independent replay binds each missed task to its original remote Scanner RPC request and cycle, and verifies restored physical bytes before rescue; neither missing structured receipt is retroactively accepted. The corrected executor retains the task owner for exact token queries and requires the remote RPC identity, Deep disk scan, matching positive cycle number and one unambiguous complete leader cycle. Invalid or duplicate identities, wrong/failed/ambiguous cycles and healthy substitution remain rejected. The later complete six-branch acceptance of run 39863505276, detailed below, raises canonical acceptance to 110/158 while retaining this original failure.
+
+I05 progress (2026-10-22): four-node startup scene `39764496504/1` was
+independently verified and isolated by
+[39766457981/1](https://github.com/rustfs/rustfs/actions/runs/39766457981),
+preserving all 102 original artifact files and twenty same-device private
+archive parents. Read-only inspection identified broken kernel-selecting
+bpftool wrappers on vm002/vm003; the exact already-installed executable is
+now digest-bound and checked before reservations. Fresh diagnostic
+[39767336741/1](https://github.com/rustfs/rustfs/actions/runs/39767336741)
+completed H1 and H2 on helper `8cbdd51`, with all five node cleanups passing.
+Independent audit of 211 artifact files verified exact 1001 ms write-lock
+timeouts, closed caller sockets, unchanged physical content, full diagnostic
+seed ACK/peer inventories and final FD counts at or below baseline. The
+original canonical result remains `BLOCKED/cleanup PASS`; source DL/DV,
+exact disconnect/native fences and stale-generation coverage remain outstanding.
+Independent queued-successor diagnostic acceptance is recorded below. Exact-helper native/tools
+[39783997956/1](https://github.com/rustfs/rustfs/actions/runs/39783997956)
+passed independent audit. The initial successor live
+[39788321908/1](https://github.com/rustfs/rustfs/actions/runs/39788321908)
+failed at internal bucket metadata-read classification despite one
+unretried 200 PUT ACK. Its stopped H1 scene was preserved by
+[39789732916/1](https://github.com/rustfs/rustfs/actions/runs/39789732916).
+The parser fix retains exact request/owner/budget/lifecycle checks and has
+a regression over the original fifteen events. Fresh helper-bound tools
+[39791663588/1](https://github.com/rustfs/rustfs/actions/runs/39791663588)
+passed independent archive/source/native audits. Live
+[39798325977/1](https://github.com/rustfs/rustfs/actions/runs/39798325977)
+then reached the new acknowledged generation but invoked the stopped-only
+shard verifier on a running service. Its original FAIL/cleanup FAIL and
+71 files were preserved unchanged by isolation
+[39799657074/1](https://github.com/rustfs/rustfs/actions/runs/39799657074).
+The controller now finishes all online peer/FD observations before stopping
+the service and checking streaming shard integrity, with matching physical
+snapshots before and after verification. Tools
+[39803149039/1](https://github.com/rustfs/rustfs/actions/runs/39803149039),
+bound to helper `6fbc3c4`, passed independent official archive/source/native
+and inspector audits. Live
+[39810035277/1](https://github.com/rustfs/rustfs/actions/runs/39810035277)
+completed H1 including stopped integrity and cleanup. H2 started and queued
+its PUT, then the parser treated a same-target slow-acquire WARN as a lock
+lifecycle row. The original result stays FAIL/cleanup FAIL. Isolation
+[39811143272/1](https://github.com/rustfs/rustfs/actions/runs/39811143272)
+preserves all 180 original files and twenty same-device private archive
+parents; every four-node pre/move/post tree, device and inode baseline
+matches. The parser now recognizes only the exact auxiliary warning bound
+to an already acquired target write; it cannot replace lifecycle evidence.
+The original sixteen-event H2 trace and incremental prefixes are covered by
+positive and negative regression tests. Exact helper `3012a65` tools
+[39812960916/1](https://github.com/rustfs/rustfs/actions/runs/39812960916)
+passed independent archive/source/native and inspector audits. Fresh successor
+[39817606025/1](https://github.com/rustfs/rustfs/actions/runs/39817606025)
+completed H1/H2 and all five node cleanups. Independent audit byte-matched all
+246 official files, nine seed request/ACK pairs and eight final versions/markers
+per topology, one successor ACK, historical generations, lock events, every
+peer inventory, ACK-bound stopped physical invariance and four/sixteen streaming
+shard receipts. FD counts returned to or below each peer's baseline. The original
+result remains `BLOCKED/cleanup PASS`; exact disconnect-edge ownership,
+distributed release, global no-leak and full source DL/DV coverage are unproven.
+That I05 diagnostic added no acceptance. The separate complete B05 audit brought the historical total to 109/158; the subsequent E04 audit brings the current total to 110/158.
+
+Fresh D03 [39801936817/1](https://github.com/rustfs/rustfs/actions/runs/39801936817)
+retains FAIL/cleanup PASS. Independent H1 re-evaluation verified all four
+branch physical and strict version-responsibility oracles, sixteen seed
+request/ACK pairs, fourteen versions/markers, 48 HEAD observations and 144
+range payloads. H2 failed before seeding when a `pool.bin` write returned
+`SystemMetadataRepairRequired` and the pool transaction remained blocked
+at `transaction_unknown/prepare_cas`. All 392 original files and five node
+cleanup receipts were independently checked. The exact candidate's bounded
+owner-repair retries only cover identity metadata, so ordinary init retries
+do not discharge this `pool.bin` responsibility. The initial replica/network
+cause remains unproven; no D03 historical failure is disproved or promoted.
+
+[J02 diagnostic 38874900846/1](https://github.com/rustfs/rustfs/actions/runs/38874900846) completed all 20 H1/H2 × DB/DV branch instances on that exact candidate. Sixteen branches report PASS; the four original all-metadata-corruption probes report permanent Failed rather than Unknown and retain NOT_OBSERVED. Independent archive and provenance audits passed. Independent mixed-branch audits verified all four repaired shards against original complete bytes, exact receipts and counters, surviving below-quorum failures, original terminal preservation and post-rescue S3 inventories. All 50 node namespace finalization receipts passed audit. Real legacy writes separately produce Unknown with zero repairs and unchanged physical files, but do not replace the original source procedure. Full J02 canonical acceptance remains blocked; these partial results add no canonical acceptance.
+
+A10 run `39197088183/1` uses immutable helper `b16e6072679d6a60a044b9d4520b30b4a79b5557`, candidate `072b57b37559fbe626aeda8abf61617fdcbdd926`, package `38043283428/1` and inspector `38715612825/1`. All nine source variants across H1/H2 and DB/DV passed their 68 required execution instances. Independent audit checked original SDK request/ACK hashes and version/marker identities, physical set selection, exact task options/receipts/counter partitions, 27,096 complete shard-streaming proofs, physical closure before target GET or control rescue, complete peer inventories and all 170 node cleanup receipts. Full inventories are checked against ACK manifests; their endpoint iteration is established by the immutable executed helper source, since the inventory documents omit endpoint labels. The remove=true branch replays one complete below-quorum original object after exact DELETE and requires whole-orphan removal with unchanged live controls. Rescue restores only post-DELETE authority. The original failed A10 result remains byte-identical.
+
+The sections below retain earlier implementation and run history. Historical failed or partial results are not promoted by this summary.
+
+## E04 complete acceptance and A05 cancellation regression
+
+Fresh E04 live [39863505276/1](https://github.com/rustfs/rustfs/actions/runs/39863505276) passes complete independent acceptance on helper `cd36287f1abe42662333fab31cf45ac23de91c8e`, candidate `9d90884c304dcfbf97057c263c6f2ee110230ce7`, package `39851206784/1` and tools `39863244404/1`. All 2,307 official archive files were byte-checked against ZIP SHA-256 `ecbd537cb6422cc3af631d4a7d1145040ffd30c20da0008778e442752e0f193e`. All six source branches passed independently recomputed seed ACKs, healthy preflight, exact Scanner source/token/RPC/cycle binding, strict repaired responsibility, complete physical and bitrot proofs before S3 reads, 768 HEADs, 672 deterministic Range payloads, unchanged inventories and finalization without backup restoration. All five node cleanups and four package purges passed. Independent complete audit SHA-256 is `c96ab5fc91b13fecb475a0cee86c85a3f5c983ce125b912b4485f7cd080d3243`. This raises canonical acceptance to 110/158 and preserves every earlier failure.
+
+A05's cancellation after successful bucket storage I/O is deterministically reproduced at the receipt boundary: the unchanged code rejects the late positive receipt but falls back to Unknown. RustFS commit `78c803e1efd67cc04edc8fc4d08a8ea75af77ddf`, pushed to the existing PR, retains the positive-receipt rejection and routes observed cancellation through the existing typed Cancelled outcome, with partial coverage and zero positive or Unknown responsibility. The exact regression failed before the fix and passes after it; all 153 task tests and Rust formatting pass. This establishes the code defect and local regression coverage, not the exact historical live interleaving or a new A05 canonical result. Fresh immutable candidate and complete A05 live acceptance remain required.
+
+## Supplemental J02 evidence audit
+
+The 2026-10-23 supplemental independent audit closes twelve positive branch instances from original J02 run 38874900846: grace Deferred/new-token cleanup, read-only terminal failure/new-token repair and dry-run across H1/H2 and DB/DV. Every original archive member was byte-checked again, all fixture requests/ACK digests and version/marker manifests were rebuilt, and the exact executed helper was pinned.
+
+Grace uses both original ACK versions, natural 120-second expiry under unchanged candidate processes, the exact typed future retry time, two unchanged original-token terminal queries and every H2 peer, a distinct compensating token, physical post-DELETE authority before GET/rescue and complete peer inventories with old-version refusal. Read-only verifies actual private-namespace EROFS, the protected-control incarnation, unchanged failed and released scenes, a distinct repair token, original full shard/bitrot evidence before GET/rescue and all peer inventories. Dry-run retains every original typed disposition, the actual R-1 fault geometry and unchanged fault bytes. All twelve node cleanup groups bind their original reservations.
+
+Together with the earlier four mixed-branch audits, sixteen branch instances now have independent acceptance; the four original opaque-metadata Unknown probes still prevent whole-case acceptance. The supplemental report SHA-256 is `4424b3e681931527d244a4ca0211dde534a8145cd95ea0b460f388262ccb8c10`. This J02 audit preserves historical results and adds no canonical acceptance; the current total is 110/158 after the separate E04 audit.
+
+## Original reuse assessment
+
+| Source module | Cases | Existing auto-testing code to inspect | Current gap |
+| --- | ---: | --- | --- |
+| Pool lifecycle | 13 | `rustfs_pool_expand.sh` | Current steps combine operations; each validation ID needs its own assertions, object manifest, and result. |
+| ILM/scanner | 15 | `rustfs-tier-test.sh`, `rustfs-s3-compat-test.sh` | Existing tier/lifecycle CRUD tests do not prove the accelerated scanner deadlines and all 15 expiration outcomes. |
+| Bucket replication | 18 | `rustfs-replication-test.sh` REP-101..109 | Existing cases cover some operations; source filters, exact topology, sync behavior, failure recovery, and result evidence need individual checks. |
+| Site replication | 20 | `rustfs-replication-test.sh` SITE-101..106 | Existing cases cover part of setup, IAM, objects, policy, status, and removal; canonical case boundaries and missing assertions remain. |
+| Heal | 84 | `rustfs_heal_test.sh`, `rustfs-fault-tolerance-test.sh` | Existing fault flows are useful helpers, but each A-J procedure requires its own fixture, fault, public assertion, and cleanup evidence. |
+| Planned | 8 | KMS, Tier/Event/Audit, Security, S3, Storage suites | Existing tests may implement the behavior; compare each source assertion before promoting it. |
+
+`s3.basic-object-round-trip` now has a dedicated executor on `single-1disk`.
+Its Bash function combines bucket creation/removal, object PUT/HEAD/GET/LIST/
+DELETE, payload SHA-256, metadata, ETag, and `NoSuchKey` checks on one fixture.
+It has local mock coverage and live PASS evidence in RustFS Actions run
+`38042083422`.
+
+`scanner.usage-convergence` has a dedicated executor on `single-4disk`. It
+uses the storage suite's deployment and SigV4 Admin client, accelerates the
+scanner cycle, records the 20-object fixture, and checks a successful leader
+cycle after upload plus a newer complete usage snapshot with exact object and
+logical-byte counts. The old STOR-134 threshold remains a separate regression
+test. Its live candidate run passed in RustFS Actions run `38042200106`.
+
+`scanner.status-readiness` has a dedicated executor on two of the existing
+auto-testing nodes, each with four disks. It checks both nodes' accelerated
+scanner runtime settings and requires exactly one leader to finish a new
+successful cycle within 120 seconds. Its live candidate run passed in RustFS
+Actions run `38044125099`.
+
+The 14 remaining ILM IDs now dispatch to the source ILM black-box behavior
+checks under `scripts/validation_ilm`. The Storage suite deploys the same
+two-node/four-disk source topology on current auto-testing nodes with the
+source's accelerated scanner/ILM timing, and supplies a cluster restart hook
+for `ilm.restart-persistence`. `ilm.transition-and-restore` uses the third
+auto-testing node as an independent warm-tier target. Each selected case must
+pass its own harness result, evidence, and bucket-cleanup verification before
+the canonical result can become PASS. `ilm.configuration-roundtrip`,
+`ilm.invalid-configuration`, `ilm.current-expiration`,
+`ilm.tag-filter-expiration`, `ilm.disabled-rule`, and `ilm.rule-removal` have
+live PASS evidence in RustFS Actions runs `38044956285`, `38045047518`,
+`38045154019`, `38045412209`, `38045496042`, and `38045714246` respectively.
+`ilm.delete-all-versions`, `ilm.delete-marker-expiration`,
+`ilm.multipart-expiration`, and `ilm.newer-noncurrent-retention` also passed
+live in runs `38049740183`, `38049817172`, `38049890130`, and `38049965929`.
+`ilm.noncurrent-expiration`, `ilm.restart-persistence`,
+`ilm.transition-and-restore`, and `ilm.versioned-current-expiration` passed in
+runs `38050040606`, `38050434607`, `38050720293`, and `38051148291`.
+All 16 ILM/scanner cases now have live PASS evidence.
+
+All 13 Pool lifecycle IDs now dispatch to the source black-box pool checks.
+The driver reuses the existing auto-testing Pool suite's two-node seed and
+expansion operations, supplies marker-guarded reset/topology/restart/physical
+census hooks, and requires source case evidence plus physical cleanup before
+canonical PASS. `pool.status` passed live in RustFS Actions run `38047001285`;
+`pool.decommission` and `pool.decommission-cancel-clear-resume` also passed in runs `38051227313` and `38052304396`. `pool.decommission-live-io`, `pool.decommission-restart-recovery`, `pool.decommission-versioned-integrity`, `pool.expand-and-rebalance`, and `pool.expansion-restart-persistence` passed in runs `38053444515`, `38053814812`, `38054128863`, `38054208114`, and `38054287896`. `pool.decommission-multipart-integrity` failed in run `38053715867`: no fresh owned-bucket MPU drain failure evidence was found; cleanup passed. `pool.operation-guards` and `pool.rebalance-live-io` passed in runs `38054540360` and `38054696080`. `pool.rebalance-stop-resume` and `pool.retired-config-removal` subsequently passed in independently verified runs `38110959836` and `38111198632`. The multipart failure remains visible.
+
+The 18 bucket-replication and 20 site-replication IDs now dispatch to the
+source black-box checks. The driver uses the existing auto-testing four-node
+main cluster and second RustFS instance on port 19000, including the source's
+site-group fixture and site outage hooks. Logical bucket isolation and
+physical teardown are required for PASS.
+`bucket-replication.source-versioning-required` passed live in run
+`38049256196`; the other 37 replication cases still require live evidence.
+
+`kms.sse-kms-round-trip` has a dedicated executor using the existing local
+KMS single-disk deployment. It checks the same object's encryption key,
+metadata, and payload, then requires an invalid-key PUT to fail without
+exposing plaintext or key material. Its live candidate run passed in RustFS
+Actions run `38042359938`.
+
+`tiering.transition-and-restore` has a dedicated executor using the Tier
+suite's existing `single-single` source and warm-tier nodes. It verifies the
+same object's transition state, readable bytes after transition, restore
+state, and SHA-256 after restore. The case is recorded under the source
+`lifecycle-b` environment profile. Its live candidate run passed in RustFS
+Actions run `38042759074`.
+
+`events.delivery` and `audit.request-correlation` now have dedicated executors
+on the Tier suite's `single-single` topology. Event delivery is counted for a
+unique object after a quiet window; audit checks matching PUT/GET records,
+distinct request IDs, successful status, and absence of known secrets.
+`events.delivery` passed live in RustFS Actions run `38046470999`.
+`audit.request-correlation` passed in run `38050138583` with the candidate's
+`Records` envelope and exact S3 operation names. The audit fixture uses a
+distinct random secret and retains the secret-leak, request-ID,
+object-identity and successful-status assertions.
+
+`oidc.login-and-session` uses the Security suite's current single-node
+deployment and a run-owned Keycloak container with a pinned image. It performs
+password login, verifies issuer/audience/subject, exchanges the identity token
+for STS credentials, and checks the readonly identity policy plus a narrower
+session policy on existing objects. Missing/invalid session tokens, tampered
+identity tokens, and wrong audiences must fail. Fixtures, containers, and the
+RustFS deployment must all be cleaned before PASS. Runtime credentials are
+excluded from evidence. This executor does not require a RustFS checkout.
+Its complete live candidate run passed in RustFS Actions run `38051425083`.
+Policy denials require `AccessDenied`; absent or mismatched temporary session
+tokens require the candidate authentication contract's `InvalidRequest`.
+
+`iam.policy-enforcement` has a dedicated executor on the Security suite's
+`single-single` deployment. It uses one principal with a policy that allows
+PUT/GET and explicitly denies DELETE, then checks the denied DELETE did not
+remove the object. Its live candidate run passed in RustFS Actions run
+`38043548625`.
+
+"Existing validation environment" means the fleet used by `auto-testing`
+and the RustFS `smoke-testing` workflows, with `RUSTFS_NODES` and
+`RUSTFS_SSH_USER` supplied there. For Heal, reuse the nodes deployed by
+`rustfs_heal_test.sh`; the source H1-H6 names describe required topology and
+fault capabilities, not a separate SSH fleet. The ten `heal-extended` cases
+have no source target-environment field, so their runtime binding must record
+the actual auto-testing Heal nodes and topology before any result is scored.
+The source's standalone ZIP, port 9100/9200, and dedicated systemd profiles
+are reference implementations for assertions, not deployment targets here.
+
+The RustFS Functional Chain workflow has a manual validation lane on the same
+`smoke-testing` runner and `rustfs-shared-functional-tests-v2` concurrency
+group. Set `build_run_id` and `build_run_attempt` to one successful nightly
+build with its retained candidate artifact, `auto_testing_sha` to an exact
+commit, and `validation_case` to one migrated ID. The lane resolves the
+immutable package URL and SHA-256 from that build attempt, then calls
+`scripts/run_validation_case.sh`. The dispatcher rejects an ID without an
+executor before touching the fleet and runs the result report with
+`--gate-case`, so a missing, failed, or incomplete selected case fails the
+workflow. Each run uploads its candidate identity, canonical result, and
+case evidence. The 154 currently dispatchable IDs include all 74 non-Heal cases, 67 automatic Heal executors, and 13 disabled-by-default environment gates. The gates preserve the operator procedures and emit explicit BLOCKED results; they are not automatic live executors or PASS claims. The preceding census confirmed 101 unique complete canonical live PASS cases: 85 on source `072b57b37559fbe626aeda8abf61617fdcbdd926`, 12 on the older immutable source, two on source `16cf19817e0c81e24d0d787a916f734a34517ec5`, and two on source `e224eb48e5311cff022b04fd78c97a8572d5c270`. Its latest additions were C08 metadata/payload mismatch `38235978152` and E03 read-repair attribution `38237270640`. The independently verified Pool tail runs `38110959836` and `38111198632`, A03 run `38198122041`, C04 run `38203454300`, the completed multipart run `38204339623`, D07 run `38205235080`, D09 run `38205854024`, D10 run `38207381816`, and manual-normal attribution run `38207956101` are included. Native, read-only, recovery and duplicate runs are excluded; B07 run `38727279347` subsequently raises complete acceptance to 102, leaving 56 cases without complete canonical live PASS.
+
+All 84 Heal recipes, 302 operator branches and 534 applicable topology branch
+templates are preserved under `scripts/validation_heal`, with the public
+contract, operation book, signed probe, ACK-logged workload, guarded file-fault
+tool and source-bound metadata inspector source. `matrix.py plan` binds current
+auto-testing SSH nodes and required resources; its audit checks every branch
+and nine evidence stages without manufacturing PASS.
+`heal.admin-healthy-noop` now has an automatic H1/H2 executor using the existing
+Heal suite's immutable deb download, installed binary digest, independent disk
+mounts, run-owned services/data roots, exact normal/deep responsibility
+receipts, repeated status, physical hashes and complete S3 inventories. It
+failed live in run `38052673980`: the candidate returned an `unknown` receipt for a fresh zero-byte object with no bucket-incarnation identity, violating the source healthy-object contract. Cleanup passed; the oracle remains strict.
+`heal.admin-invalid-request` passed all six branches on both H1 and H2 in run `38052868969`, with complete peer coverage and unchanged physical/S3 fixtures.
+The external-shard executor also dispatches C01–C04, D02, E01 and E02 with all 24 source branches on H1/H2. It freezes exact version/dataDir/part, actual erasure role/set slot/disk UUID and full original shard hashes, stops all run-owned services before fault injection, witnesses the same fault immediately before the manual token, and checks complete physical repair plus streaming HighwayHash before any target GET. Full S3 inventories, cross-part Range, HEAD and pending MPU controls retain source semantics. Backups are used only for failed-case rescue or finalization; an unexpected new generation keeps the namespace DIRTY. Protected-mode C01 passed all ten H1/H2 branches in run `38057373420`; the other six executors still require live evidence. A03 also has both H2 branches: a non-coordinator shard fault, exact token queries through coordinator/peer/fixed current-fleet entry, a 30-second convergence budget for task start and full terminal outcome, physical proof and S3 inventories through all four entries.
+A04 dispatches all six dry-run branches with separate DB/DV fixtures on H1/H2. It freezes version and marker identities, requires complete dry_run_observed receipts and zero repaired counters, proves byte-for-byte no mutation, then rescues the injected fault before full S3 verification. It does not require a dry-run incarnation or claim backup restoration as product repair. The first C01/C02 runs (`38055041726`, `38055254447`) stopped before injection because the remote script had an invalid future-import placement; their FAIL records and clean finalization are retained. The script compiler regression is fixed, and fresh runs use the corrected executor.
+`heal.range-head-and-conditions` preserves all seven D07 branches, with real baseline/post-repair HEAD, cross-part Range, exact ETag conditions, and server-time-derived conditions including 304/412/416 negatives. Completed MPU target-side stale `part.N.meta` cleanup is allowed only on the repaired drive/dataDir and for parts still represented by the unchanged authoritative xl.meta; controls and payload shards remain exact.
+`heal.historical-versions` now preserves all four D03 branches: historical and
+current part faults on distinct physical roles/drives, a recursive bucket fault
+with missing historical part and another drive's complete xl.meta, and multiple
+versions faulted together. All acknowledged versions and markers must have
+exact, complete responsibility receipts and unchanged S3/physical references.
+`heal.repeat-idempotency` preserves all four J04 branches. It freezes the first
+repair's physical/S3 result, repeats normal/deep with independent tokens, and
+restarts the same run-owned units for the process branch. The versioned branch
+removes a whole xl.meta with three data versions and a marker; first repair may
+attribute all identities or a mixture of repaired and positive health, while
+repeat permits zero repairs and mode-specific health only. Counters must agree
+with the complete receipt set. D05 `heal.versioning-suspended-null` now uses real
+S3 versioning suspension, consecutive null overwrites and acknowledged null
+marker creation/deletion. It faults a live null data/parity part or an entire
+xl.meta containing the current null marker and historical versions, verifies
+every exact identity, then re-enables versioning and checks a new non-null
+version plus the unchanged original version set. These three executors still
+require live evidence.
+B07 `heal.below-read-quorum` now retains exactly R-1 same-version authorities
+and isolates all other part files, separately for never-versioned and historical
+fixtures. The exact candidate tool build freezes the recognized metadata read
+quorum rule, S3 HTTP/error mapping and source digests. Unrecognized or stale
+contracts cannot dispatch the negative case. GET must return the frozen refusal
+without payload or an unknown transport outcome; manual deep Heal must retain
+the exact failed version and complete truthful counters. All good parts and
+metadata must remain unchanged. Only after that failure proof may backups be
+restored and a new token verify recovered integrity; recovery never proves
+product repair during the negative window. This executor still requires live
+evidence and a fresh tool build containing the quorum contract.
+C05 `heal.missing-or-corrupt-xlmeta` also has all five automatic branches on
+H1/H2: missing/truncated/header-corrupt xl.meta, a flip inside the unique decoded
+inline value, and whole metadata loss with three historical data versions and a
+marker. The tool now freezes each inline value's physical offset/length/SHA-256
+and verifies its streaming HighwayHash framing. Complete original inline shard
+digests, version/dataDir/part/role references and all S3 versions must be restored.
+A decoder error can be captured only when its file length/hash exactly matches
+an injected fault receipt; it never becomes valid metadata or a repair proof.
+Legal metadata re-encoding may move an inline value while its exact length/hash
+stay fixed. The extended helper passed the 24/12 external shard oracle and one
+frozen released-process inline fixture with three corruption negatives locally;
+this executor still needs a new candidate-bound tool build and live evidence.
+D06 `heal.object-metadata-and-tags` preserves all three H1/H2 branches with
+three historical payloads per Unicode/space/literal-percent key, actual gzip
+content, encoded Chinese user metadata and two distinct tag groups. It checks
+the initial normalized metadata/Content-Type/Encoding/Disposition/Cache-Control
+and precise versioned tags, then faults an external data part, an entire xl.meta
+header, or a parity part. Exact physical/version references, complete S3
+inventories, HEAD and ranges must agree after repair. This executor still
+requires live evidence.
+Four Heal cases retain a canonical acceptance gate: C11, F07, I05 and J02. All have dispatch routes. All four have explicit diagnostic routes that preserve their blocking canonical verdicts; I05's H1/H2 timeout and queued-successor diagnostics have independently audited live results, exact-source native prerequisites and stopped-scene isolation. I05 now composes both H1/H2 controllers with separate same-artifact native owner and full-content/physical generation checks. The native proof does not establish that the fleet request held a distributed lock at disconnect, and the composed source plan still requires independent complete live audit. The 13 environment-gated cases have disabled-by-default control flows and oracles in `environment.py`; actual execution requires an assigned and reviewed auto-testing resource adapter, and canonical results remain review-gated. See [the environment gate reference](validation-heal-environment-gates.md) for each missing resource, capability and native barrier.
+H4 needs at least 24 independent disk endpoints; H6 and native-fence branches
+retain their platform and source-bound evidence requirements.
+
+The RustFS tool-build lane produced the initial candidate-bound metadata inspector in run `38052536366`; A07 also passed with that tool in run `38053214984`. The extended bitrot helper was successfully built against the same candidate in run `38054812014`, including the released-shard oracle checks. Set `validation_tools_only=true` with the pinned build and auto-testing SHA to build the current helper. Heal live runs consume `validation_tools_run_id`/`validation_tools_run_attempt`; the workflow verifies the successful build attempt, candidate/helper identities and binary/lock digests. The node adapter installs the verified tool under its run-owned root. Plain metadata decoding makes no payload-integrity claim; the new explicit part-verification mode validates current and legacy streaming HighwayHash. The build checks 24 healthy shards captured from external released processes and rejects 12 controlled corruptions. Ordinary live execution needs no RustFS source checkout.
+
+The eight planned cases have these concrete reuse candidates:
+
+| Validation ID | Candidate IDs | Specific item to verify |
+| --- | --- | --- |
+| `scanner.usage-convergence` | STOR-134 | Dedicated executor and live PASS evidence. |
+| `kms.sse-kms-round-trip` | KMS-104, KMS-301, KMS-214 | Dedicated executor and live PASS evidence. |
+| `tiering.transition-and-restore` | TIER-105, TIER-107 | Dedicated executor and live PASS evidence. |
+| `events.delivery` | EVT-103, EVT-104 | Dedicated executor and live PASS evidence. |
+| `audit.request-correlation` | AUD-102, AUD-104, AUD-106 | Dedicated executor and live PASS evidence. |
+| `iam.policy-enforcement` | IAM-105, IAM-109 | Dedicated executor and live PASS evidence. |
+| `oidc.login-and-session` | OIDC-103 | Dedicated executor using real Keycloak login and STS session boundaries; live PASS evidence. |
+| `s3.basic-object-round-trip` | S3C-101, 201, 203, 205, 206, 207 | Dedicated executor and live PASS evidence. |
+
+Migration work on an ID is complete only when its result contains the exact
+source assertion text in order, evidence paths for each assertion, the
+candidate/source/auto-testing/rc provenance, and a clean finalizer result.
+The report rejects a PASS that lacks any of those fields. Existing suite
+results cannot be renamed or counted automatically.
+
+The runner should use the catalog's target environment as a profile, keep
+one unique artifact directory per run, write `RUNNING` before invoking a case,
+and retain the primary failure when cleanup also fails. Tests that mutate the
+shared fleet need the RustFS workflow concurrency group and an environment
+lease. Special Heal topologies and source-bound tools are provided by
+separate RustFS build/environment jobs; ordinary case execution consumes
+their immutable artifacts.
+
+
+The corrected default-mode C01 run `38055597354` executed every H1/H2 fault branch with clean finalization. Single-part bytes and HighwayHash were restored, but the candidate returned unknown responsibility with no incarnation. The candidate explicitly defaults independent-shard writes off; current positive fixtures now enable both shard-integrity write and fleet-confirmation knobs only after every private peer is bound to the same candidate. Their values are read back from the actual processes and archived. The default-mode failure remains separate and never becomes a positive proof. New protected-mode runs are required before any new PASS claim.
+
+Protected-mode A01 run `38056636793` passed all 512 exact H1/H2 normal/deep
+object responsibility receipts and clean finalization. This proves only the
+explicit, uniformly enabled shard-integrity fixture configuration; the default
+deployment failure remains retained. A04 run `38056884022` passed DB branches
+but failed DV receipt completeness with cleanup PASS. The executor had used a
+nonrecursive object request, which gives latest-object responsibility rather
+than all historical versions. DV requests now explicitly set recursive=true
+for the exact object prefix; the original FAIL remains and a new run is required.
+The corrected recursive DV request passed all DB/DV H1/H2 branches in run
+`38057908339` with clean finalization, including complete dry_run_observed
+version/marker receipts and byte-for-byte no-mutation proof.
+
+D03 run `38058420193` passed historical/current/multiple-version branches on
+both H1/H2, but its bucket-recursive branch used an extra trailing slash and
+received HTTP 501 before admission. Cleanup passed. The task URL now uses the
+registered exact bucket route; this harness failure remains retained and D03
+needs a fresh full run before a canonical PASS claim.
+
+J04 run `38058699913` retained FAIL with clean finalization. Its DB checks hit
+an executor Range bug on exact/near-1MiB controls; positive ranges now use
+actual bounded intervals and have boundary regression coverage. The DV H1
+branch passed first repair and both normal/deep repeats. DV H2 physically
+restored all original bytes, decoded version references and S3 manifests, but
+the candidate attributed the current delete marker as unknown while three data
+versions were repaired. That genuine receipt failure remains strict and visible.
+Tool build `38058949922` successfully produced the current decoder plus the
+recognized exact-candidate quorum/error contract for B07.
+
+The corrected D03 run `38059278988` admitted every source branch, passed H1
+completely and passed H2 historical/current/recursive branches. H2's simultaneous
+multiple-version fault left one original current-version parity part missing.
+The exact physical oracle retained FAIL and clean backup-assisted finalization;
+that restoration does not count as product repair.
+
+Fresh corrected J04 run `38059567206` passed every DB/DV H1/H2 source branch,
+normal/deep independent-token repeats and same-unit process restarts, with clean
+finalization. The earlier same-candidate H2 unknown-marker failure in run
+`38058699913` is still retained; the new PASS does not rewrite that evidence or
+claim a product fix. Tool build `38060217084` successfully built the extended
+inline verifier and checked the 24 healthy/12 corrupt external shards plus the
+one healthy/three corrupt released-process inline samples.
+
+D05 run `38060400818` exercised all H1/H2 suspended/null branches and cleaned
+up successfully, but its receipt oracle compared the Admin typed nil UUID with
+the S3 `null` spelling literally. The candidate source defines these as the same
+null identity. The oracle now normalizes only those representations and still
+rejects every other version mismatch. The original FAIL remains; a fresh full
+run is required. Auto-testing CI passed all six lanes at commit `f9c153f`.
+
+B05 `heal.multiple-shard-loss` now expands 1..M losses from the actual decoded
+EC geometry, including every possible data/parity role count. Each parameter
+instance uses a distinct acknowledged object; DB and DV both run on H1/H2.
+All drives remain online for the write/lock condition while selected same-version
+parts are isolated. Exact full original shard hashes, streaming checksums,
+complete version/marker responsibility and S3 controls must all pass. B05 has
+not yet been validated live.
+
+B07 run `38060686737` retained FAIL with cleanup PASS. At R-1 shards H1
+returned HTTP 500/InternalError instead of the recognized candidate read-quorum
+refusal. H2 returned 503/SlowDownRead, but Heal recorded a grace-window deferred
+receipt and completed execution rather than the source's required failed
+responsibility. Good-shard preservation and the raw task outcome remain
+archived; neither branch is promoted to PASS.
+
+D10 `heal.special-keys-and-pagination` now creates 265 unique objects, including
+Unicode, spaces, literal percent/encoded slash, real slash, nested paths and
+adjacent `a/`/`ab/` controls. Both version and current LIST explicitly request
+MaxKeys=17 and retain observed page sizes; the complete ACK manifest must match
+without missing or duplicate keys. Exact logical-key physical mappings precede
+part/xl.meta faults and signed object requests. Every control remains exact.
+D10 has not yet been validated live.
+
+C05 run `38061075879` exercised all ten DB/DV H1/H2 branches and retained FAIL
+with cleanup PASS. Nine branches passed. H1's truncated xl.meta remained at the
+exact injected 1866-byte length instead of its 3732-byte baseline and could not
+decode; the physical oracle rejected it before target GET.
+
+D06 run `38061589839` passed all H1/H2 semantic branches with cleanup PASS:
+three distinct versions for each Unicode/space/percent key retained exact public
+metadata, actual gzip bytes/headers and versioned tags through data, metadata
+and parity faults. Physical original-shard/bitrot and full S3/HEAD/Range checks
+passed before receipt promotion.
+
+D01 `heal.object-size-boundaries` now has a complete automatic parameter matrix
+for zero/one, 4KiB, actual inline and EC stripe boundaries plus a multi-block
+external object. A tool-build contract freezes recognized candidate policy and
+source/extractor digests; live process environment and actual EC metadata derive
+the limits. H1/H2 need different inline limits under the recognized current
+policy (the runtime derives these rather than guessing 256KiB). Each size tests
+both actual data/parity roles with missing and bitflip faults. Zero objects use
+metadata loss/header faults and never invent a payload part. Exact byte hashes,
+complete responsibility, HEAD/LIST and actual first/tail/stripe Range checks
+are required; zero Range is a strict S3 416/InvalidRange negative. Tool build
+`38062137032` successfully produced the recognized size and quorum contracts
+for the fixed candidate, with current extractor/source and binary identities.
+
+Corrected D05 run `38061843290` passed every H1/H2 suspended/null branch and
+clean finalization. Real enabled-period history, null overwrites and markers
+retain precise S3 identities; the typed Admin nil UUID is mapped only to that
+acknowledged null version. The candidate then re-enabled versioning and created
+a distinct non-null version without changing old payloads or version/marker
+identities. The original literal-identity harness failure is still retained.
+
+A08 `heal.admin-authorization` now dispatches every H1/H2 source principal
+branch. Anonymous, deliberately bad, correctly signed but expired, and denied
+IAM requests each test start/status/stop with exact HTTP/error refusals and
+recursive JSON/XML task-disclosure rejection. A temporary principal with only
+`admin:Heal` admits, queries and stops its independent token while S3 reads stay
+denied. Temporary users/policy/tasks are finalized and credential revocation is
+verified; original physical/S3 fixtures must remain unchanged. Evidence redacts
+root and temporary credentials without altering oracle inputs. Nine focused
+tests cover independent SigV4 verification, denial leakage, lost IAM admission,
+cleanup failures and revocation. A08 still requires full live evidence.
+
+D01 run `38062610870` retained FAIL after 35 H1 parameter attempts. Three
+physical comparisons rejected changed target-side `xl.meta.bkp` files; archived
+per-drive comparison shows each new backup is the exact pre-repair authoritative
+`xl.meta`, while payload shards and decoded logical identities agree. The
+original verdict remains retained pending a new complete run with a bounded
+target-backup oracle. A fourth attempt was rejected before injection because
+its expanded fault ID exceeded the tool limit; retrying that invalid ID during
+finalization caused cleanup FAIL. Fault IDs now use branch/index digests within
+the allowed length. Recovery run `38063798238` independently proved stopped
+owned service, no injection intent, exact archived pre-fault physical scene and
+removed run roots. It records cleanup PASS, `originalVerdictRewritten=false`
+and no canonical/product-repair PASS claim. Serial deployment resumed only
+after inspecting that recovery evidence.
+
+B09 `heal.node-outage-rejoin` now has both source H2 branches with independent
+DB and DV deployments on the existing four nodes. The candidate tool freezes
+the recognized same-set read/write quorum policy; actual metadata maps the
+four lost drives before admitting degraded I/O. Exact unit exit and refused
+endpoint connections witness the outage. PUT/overwrite/marker/historical-delete
+ACKs define the new manifest, while the stopped node's complete physical scene
+stays unchanged. Only the explicit run-owned `offline-replay` profile enables
+MRF; scanner and automatic disk-rebuild remain disabled. After the same node
+and roots rejoin, physical polling precedes any target S3 reads or manual task.
+Every drive must contain the exact acknowledged version/marker set; surviving
+shard bytes, committed streaming checksums and all-node S3 manifests must agree.
+Deleted identities receive exact S3 absence checks. No copied backup contributes
+to repair. Six focused tests cover profile boundaries, source-quorum recognition,
+ACK ambiguity, per-drive omissions/resurrection and deleted-read refusals.
+B09 requires the fresh write-quorum tool artifact and full live evidence.
+
+D01 run `38064187494` completed all 96 H1/H2 parameters: 76 passed and 20
+failed the then-strict existing-backup oracle; cleanup passed. Independent
+forensic comparison of all 20 scenes found only target `xl.meta.bkp` updates
+to the exact pre-repair authoritative metadata. The corrected oracle now
+bounds both new and existing backup updates to that drive/key/dataDir and
+exact size/hash. The original FAIL is retained; a fresh full run is required.
+
+A08 runs `38066188001` and `38066437827` failed policy creation because bare
+`Resource:["*"]` is not an ARN. The fixed candidate accepts `admin:Heal`
+without Resource. Corrected run `38066970195` reached every branch: bad and
+expired signatures, denied user and least-privilege user passed on H1/H2.
+Anonymous traffic returned 403/AccessDenied from S3Router::check_access before
+the handler, exposing an incorrect 400/InvalidRequest harness expectation.
+The route-level expectation is now exact; all original FAILs retain cleanup
+PASS and none is promoted without a fresh complete run.
+
+Tool build `38065628505` succeeded against the same fixed candidate and carries
+the recognized `data-plus-one-if-equal-parity` write-quorum policy. Its helper,
+extractor, candidate and binary digests were verified locally; it is used by
+the serial live queue for D01/B07/B09. Ordinary runs still need no RustFS checkout.
+
+D09 `heal.object-lock-retention` now dispatches all four H1/H2 source branches
+on real Object Lock/versioning buckets. Three versions per key retain exact
+Governance/Compliance metadata or ON legal hold; the ten-minute UTC window is
+anchored to the server HTTP Date. A run-owned principal can read but explicitly
+cannot bypass Governance. Baseline and pre/post-fault DELETE must return exact
+403/AccessDenied while retention is active. Data/parity faults and whole xl.meta
+loss cover current/historical versions; all affected version shards, public
+properties, tags and responsibility receipts remain independently checked.
+Finalization releases hold and Governance only through normal APIs, waits up to
+twelve minutes for real Compliance expiry and deletion ACK, proves the bucket
+empty/absent, and removes IAM resources. Only then may physical roots be removed.
+Incomplete finalization retains a stopped namespace with cleanup FAIL. Partial
+seed ownership, real server time, same-drive multi-version proof selection and
+protected cleanup failure boundaries have focused tests. Full live evidence
+is still required.
+
+C08 `heal.metadata-payload-mismatch` dispatches every H1/H2 branch with three
+real versions per distinct key. The donor is another acknowledged version's
+same-role/same-slot part: a different physical length for length mismatch,
+the same length with different bytes for payload mismatch. Candidate-bound
+streaming verification proves both original and donor cohorts before injection.
+The descriptor-relative fault layer admits only another owned same-number part
+on the same disk, exact donor/target hashes and quiescent service; it rejects
+metadata, traversal, symlinks, hardlinks, unowned buckets and identical bytes.
+Intent and original bytes are persisted before replacement. Every physical
+fault receipt must equal the frozen donor; metadata and controls stay unchanged.
+The negative branch leaves exactly R-1 correct same-version shards and requires
+exact read refusal, truthful failed responsibility and no physical mutation
+before rescue. Restored backups certify cleanup only; a later-generation target
+cannot be overwritten. Positive branches require original shard bytes, full
+version metadata/contents/tags, streaming checksums and repaired receipts.
+This executor has local fault/recovery/authority tests and still needs live proof.
+
+E03 `heal.read-repair-attribution` dispatches GET-missing and Range-bitrot DB
+branches plus an exact historical-version DV read on H1/H2. Its explicit
+profile leaves scanner, automatic rebuild and best-effort MRF ingress off,
+while enabling candidate JSON debug events; the candidate's durable consumer
+stays initialized independently of that ingress switch. Before each single
+GET/Range the exact fault remains physically present. Correct bytes, exact
+version/ContentRange and a server request ID are required. A new structured
+`Read-repair heal request admitted` event must match bucket/key/part/pool/set
+and accepted task ID; stale events, text mentions, merges and ambiguous tasks
+cannot prove attribution. Only status queries observe that automatic task;
+no Admin start/deep verifier repairs the fixture. The task's exact version
+receipt, physical original shard hashes, streaming checksums and full public
+S3/version controls must all agree. Runtime credentials are redacted from
+archived debug journals. Focused tests cover source identity, stale-task
+refusal, single-read/no-mutation dispatch, exact content/range/request identity
+and runtime profile isolation. Full live proof remains pending.
+
+Corrected D01 run `38067139720` passed all 96 H1/H2 parameters (48 per
+topology), with canonical PASS, complete summary and cleanup PASS. Its exact
+candidate/package/binary/harness identities and each parameter's evidence were
+downloaded and independently checked by the canonical report gate. It covers
+zero/one/4KiB, actual inline and EC-block boundaries, multi-block payloads,
+both roles, missing/bitflip faults and exact Range outcomes. The three prior
+D01 failures and independent recovery record remain unchanged.
+
+B02 `heal.disk-readonly-and-permission` dispatches both H1/H2 branches. The
+read-only gate uses systemd PrivateMounts/ReadOnlyPaths for the selected
+run-owned data root; host mounts are never remounted. The permission gate
+records/restores that root's original 0700 mode and drops both DAC override
+capabilities from the root service, so chmod cannot be bypassed. A write probe
+enters the actual service mount namespace with the same effective UID/GID and
+DAC boundary and requires exact EROFS/EACCES. Original shard isolation precedes
+the gate. Failed/deferred exact-version responsibility must show zero repaired
+counters and the complete physical fault scene must remain unchanged. After
+service exit and gate release, a distinct task must actually repair the
+original target; full physical, streaming and public checks follow. Gate intent
+precedes chmod/start and is recovered before any fault rescue or physical
+cleanup. Root drift or release failure retains the namespace. Focused tests
+exercise permission restoration, write refusal identity/errno, no root bypass,
+truthful unresolved receipts and cleanup retention. Live proof is pending.
+
+E04 `heal.scanner-cold-object-repair` now dispatches all three H1/H2 branches.
+It reads back the actual private process settings for five-second scanner
+cycles, every-cycle bitrot, zero deep cooldown and full object selection, while
+replacement auto-heal and best-effort MRF stay disabled. Before any fault, each
+cold target needs an exact healthy scanner task and unchanged physical files.
+Faults are injected with all run-owned processes stopped; the stopped scene
+proves damage immediately before restart. Cursor-bounded journal windows bind
+the exact object discovery to an accepted Scanner source request, task start,
+completed cycle and terminal task. The cycle-boundary branch requires two
+complete post-fault cycles. Observation issues status queries only; no target
+GET/HEAD or Admin mutation contributes to the repair. Exact repaired version
+responsibility, original physical shard bytes, streaming checksum and complete
+S3 inventories still gate PASS. Truncated logs, another source, a later healthy
+receipt or cycle counters alone cannot certify repair. The executor passed its
+local source/timing/cursor negatives; full live evidence remains pending.
+
+D04 `heal.delete-marker-semantics` now dispatches its four H1/H2 source
+branches as six real instances. Each key has three separately acknowledged
+data versions and, where applicable, a current or historical marker. Missing
+replicas cover both physical roles. Stale creation and deletion isolate one
+original drive with a service-identity read/write EACCES gate, perform one
+exact marker mutation, and prove the gated replica stayed unchanged while all
+other members carry the ACK authority. The expected metadata is derived only
+for comparison from that authority and the original target-drive data
+references; the harness never writes synthesized metadata. Formats, data
+shards, controls, exact version receipts and streaming checksums must agree.
+Every peer checks current-marker 404 and explicit-marker 405 with exact marker
+identity headers. Explicit current-marker deletion must reveal the precise
+previous data version, remove the marker identity and preserve the full
+remaining version/public-property manifest. A failed or ambiguous stale ACK
+stops the topology and retains its result; gate cleanup precedes physical
+finalization. Local ACK/order/authority/header/gate negatives passed; full live
+evidence remains pending.
+
+J01 `heal.status-partial-disabled-unknown` now has a dedicated current-fleet
+executor for H1/H2. Fresh manual and disabled deployments retain actual process
+configuration, complete physical fixtures and both raw Admin and rc JSON/text
+outputs. Peer coverage must balance exactly; absent peers need an explicit
+unknown count/reason and cannot report idle. Any raw or CLI sample that erases
+uncertainty fails immediately, including during bounded convergence. H2 stops
+one original peer, witnesses its process exit and connection refusal, and
+queries every survivor before restarting the same member. The source's step 2
+explicitly restricts that branch to H2; H1 records its applicability reason
+and instead proves an original-service restart followed by complete idle.
+A startup observer begins before any service starts and requires raw
+uninitialized status to bracket matching rc/replacement observations. A
+transport error or an already initialized service cannot prove that window;
+NOT_OBSERVED makes the complete case fail. Original formats/object files and
+full S3 manifests must remain unchanged. Temporary rc credentials use a 0600
+configuration outside artifacts and are removed during finalization. Local
+coverage/uncertainty/startup/config-cleanup negatives passed; live evidence
+remains pending.
+
+J01 selects the official rc v0.1.36 Linux amd64 musl release in a new private
+artifact directory. Both archive and executable SHA-256 values are fixed and
+checked before execution; its manifest records the release source, target,
+version output and exact bytes. This release includes the existing CLI fix
+that preserves incomplete/unknown status in human-readable output. The musl
+build avoids depending on the runner's glibc version. Other Heal cases retain
+their existing client selection. The original failed J01 result is retained;
+the complete raw/CLI startup, disabled, missing-peer and recovery observations
+still require live acceptance with the fixed client.
+
+
+J03 `heal.incremental-cursor-and-truncation` now has a current-fleet DX
+executor with 1025 pre-registered external objects of 1MiB+73 bytes. The
+reference candidate retains 1024 result items, separately from its 128-object
+outcome diagnostics; a 257-object task cannot prove the retention-gap branch.
+The actual terminal minSeq must move and an old cursor must carry truncation.
+Incremental queries treat sinceSeq as exclusive and advance only over returned
+items, including when the response payload itself is cropped. Same-task bounds
+cannot move backwards or duplicate object/version identities. At least two
+useful running windows are mandatory; NOT_OBSERVED prevents a complete PASS.
+H2 queries every actual node with the admitted path/token and uninterrupted
+deployment epoch; H1 records why a cross-node query is inapplicable. The
+candidate renders startTime anew on each response, so A03 also binds its
+original query path/token instead of comparing that timestamp. Canonical
+counters must cover all 1025 ACKs with the exact 17 real missing-shard repairs,
+no failures/unknowns, and complete traversal. Bounded diagnostic receipts remain
+explicitly incomplete. Full physical manifests, every original same-role shard
+and streaming checksum, and all-peer S3 manifests independently cover every
+object before PASS. The case has a fixed 16384-file snapshot budget, 128-part
+verification batches, a 45-minute service/lease budget and 2GiB free per drive.
+Local cursor/gap/counter/scope/part-omission negatives passed; live evidence
+remains pending.
+
+
+A06 `heal.admin-stop-and-stale-token` now uses the same pre-registered 1025
+external-object DX fixture on H1/H2. Cancellation requires actual running,
+nonzero partial canonical progress and both repaired and still-missing targets.
+The frozen candidate policy requires active stop acknowledgement, retained
+aborted/cancelled partial responsibility, and exact HTTP 200/stopped
+idempotent acknowledgements for repeat/stale stops of a durable terminal token.
+Unknown tokens remain a separate exact 500/InternalError contract. Three complete physical snapshots with
+five-second gaps keep the service running after cancellation; stopping the
+process cannot substitute for task write quiescence. Actual restored shard
+counts must equal the retained cancellation counters, and remaining shards
+must still be missing. A distinct new token must be running before and after
+old-token STATUS/STOP; unobserved brackets prevent PASS. The new task repairs
+only the remaining defects, and its completion must preserve the original
+cancelled outcome. Full streaming/physical and all-peer S3 verification happens
+before backup rescue. All four source branches remain explicit. Local
+cancellation/counter/quiet-physical/token/error negatives passed; full live
+proof remains pending.
+
+
+F06 `heal.cancelled-task-restart` extends the same real partial cancellation
+and write-quiescence proof with a graceful restart of every original H1/H2
+unit. A read-only node action accepts only a canonical admitted task UUID and
+three exact candidate journal names under the private run roots; it records
+all four drives on every node, bounds terminal/intent to 64KiB and report to
+8MiB, and rejects links, hardlinks, oversized or changing files. The actual
+cancelled terminal and report need one matching physical owner, exact task and
+prefix scope, no runnable intent anywhere, and the settled partial outcome and
+valid sequence window. It never synthesizes a checkpoint or edits system
+records. After same-root restart, every actual process must be new while its
+unit/candidate/formats/objects remain original, all peers must be idle, old
+status must still be cancelled, and the exact original journal bytes must be
+preserved. A fresh forceStart task remains independent under old STATUS/STOP
+and repairs only the remaining witnessed defects. Full physical/streaming/S3
+proof remains mandatory. A06 also now preserves both its initial cancellation
+and allowed monotonic in-flight report refinement before freezing the report
+after actual drain; later quiet/restart/new-task checks cannot rewrite it.
+Local journal-owner/schema/scope/cursor/read-boundary negatives passed; full
+live evidence remains pending.
+
+A05 `heal.admin-overlap-and-force-start` now dispatches all four source
+branches on both H1 and H2, with an independent deployment/ACK ledger/fault
+set for each instance. Two real prefix scopes contain 257 external objects
+each; five missing shards per selected scope retain positive authority.
+The actual process environment freezes `merge`: equivalent starts must return
+the same token, parent/child requests must return exact 409/OperationAborted
+with `overlapping_paths`, and disjoint requests must receive independent
+tokens. Every control request needs a nonzero running bracket; a missed
+window remains NOT_OBSERVED. forceStart must cancel the old task with partial
+coverage, admit an independent replacement token, and resist old-token STOP.
+Initial and settled cancellation reports are retained; exact old/new repair
+counters, incarnations and diagnostic identities cannot leak or duplicate
+responsibility. All 257 original item sequences are retained while running
+and reconciled with the exact terminal outcome and retained suffix. The
+completed-report byte budget can evict an early prefix below the 1024-item
+count cap; an unobserved prefix still fails completeness. Both disjoint tasks
+are sampled concurrently, and replacement drain samples are preserved. Three
+live physical samples after actual all-peer drain must remain unchanged,
+with the same processes. Complete same-role shard bytes, streaming checksums
+and all-peer full S3 manifests remain mandatory before PASS. Local
+scope/error/window/duplicate-repair/item-omission/cleanup-stop negatives
+passed. Run `38199371461` retained FAIL with clean finalization: H2's first
+three branches exposed the early-prefix accounting gap, H1 force-start had a
+typed cancelled receipt counted as skipped, and H2 force-start contained a
+real unknown receipt. Replaying the original running records verifies all
+three 257-item primary ledgers; H1 cancellation now requires its exact typed
+receipt count, while H2 unknown remains rejected. This diagnostic replay
+does not change the original FAIL or claim a complete new live PASS.
+
+F01 `heal.root-graceful-coordinator-restart` now dispatches both source
+branches on H1/H2 with the same fixed DX physical/S3 oracles. The coordinator
+comes from the unique admitted physical root intent, rather than a guessed
+node alias. Actual nonzero task progress and both restored/pending target
+shards must precede interruption. Linux pidfd binds pause, SIGTERM and resume
+to the original process birth; the original run-owned unit retains actual
+normal-exit-zero evidence. All peers retain their original process except
+that coordinator. The exact schema-3 root intent stays on the same drive,
+with original task/scope/options/priority/admission time and a finite
+remaining timeout that cannot increase. Pending peer writes must settle
+before same-unit/environment/volumes/candidate restart. The original token
+must recover, report complete exact responsibility and repair the remaining
+physical defects; no new START compensates for lost responsibility. Runnable
+intent must retire on every drive, while completed terminal/report remain on
+the original owner. A second coordinator restart preserves their bytes,
+completed outcome and all user files and remains idle. This proves durable
+admission replay, and makes no cursor-resume or record-disk failover claim.
+Local owner/budget/process-birth/exit/old-token/journal negatives passed;
+full live evidence remains pending.
+
+
+B06 `heal.read-quorum-boundary` now dispatches all three source branches on
+H1/H2 with three independent 5MiB+123-byte never-versioned ACK objects. Its
+read and write quorums come from the recognized candidate artifact and actual
+decoded geometry; the reviewed candidate lock policy counts pool host:port
+clients, so four H2 hosts have exclusive quorum three, not nine disk locks.
+The exact-R GET archives read-repair interference and never certifies manual
+repair. The separate exact-R manual object must reconstruct every missing
+original shard and return exact completed responsibility. F1 only isolates
+payload files: all drives, metadata and locks remain online, so it cannot
+represent insufficient writable-drive or lock quorum. That condition is
+explicitly non-applicable here; B08 retains its independent real fault
+requirements. The restored-W branch first proves the candidate's exact R-1
+read refusal with no physical changes, then restores only W-(R-1) original
+backups. Those sources never count as product repairs. A distinct manual
+token must reconstruct all N-W remaining targets, followed by full physical,
+streaming checksum and all-peer S3 checks. Live evidence is pending.
+
+A08 `heal.admin-authorization` passed live in RustFS Actions run
+`38069648003`, harness `0789743d439fd91b967c2d6fba85c06e29f844c5`. All five
+source branches passed independently on H1 and H2; the canonical gate,
+complete summary, cleanup and workflow all passed with the fixed candidate.
+The prior A08 failures remain recorded. Thirty-seven Heal executors remain
+to be migrated; automatic dispatch and local tests do not claim live PASS.
+
+
+C06 `heal.stale-version-rejoin` now dispatches all three source branches on
+H1/H2, with never-versioned overwrite and independent versioned historical
+delete/marker fixtures. A private permission gate removes both DAC bypass
+capabilities and activates only after the original candidate has started.
+Same-process read/write EACCES proves that the unchanged member cannot
+participate in the subsequent ACK transition. Every online metadata replica
+must agree on the complete acknowledged namespace, the isolated member's
+entire physical disk remains exact, and controls retain their original bytes.
+The same disk UUID and set slot must rejoin without harness metadata writes
+or restoring old-generation backups. An independent deep token must correct
+the stale namespace; exact outcome counters, retained same-drive shards,
+streaming checksums, deleted-version absence, current marker headers and
+all-peer S3 manifests are required. Original payload nonces remain attached
+to ACK versions after deletion so Range checks cannot silently renumber
+history. The candidate's dangling grace is explicitly zero for this rejoin
+profile; C10 retains the independent grace-policy requirements. Complete
+cursor-bounded generation-selection logs are archived. Live evidence is pending.
+
+B02 run `38079787495` retained FAIL and cleanup PASS on both H1/H2. The
+readonly window produced `completed_with_errors` with one failed receipt,
+zero repaired targets and `summary=stopped`, but omitted bucket incarnation.
+The stopped summary is now interpreted only with explicit failed execution,
+complete coverage and exact counters; missing incarnation still fails. The
+permission setup previously chmod'ed the data root before service startup,
+causing the candidate to exit with EACCES before injection could be witnessed.
+Permission gates now retain an armed recovery marker during initial startup
+and inject chmod only into the same live candidate with DAC bypass already
+removed. This fixes the injection window without certifying the old run or
+relaxing physical/refusal/responsibility assertions. A fresh run is required.
+
+
+B01 `heal.disk-loss-and-repair` now dispatches both source branches on H1/H2
+with the full 128-object DB manifest, including zero/inline/external objects.
+The same-service permission gate must deny reads and writes on one exact
+original disk, and signed read-only Admin info must independently identify
+that exact formatted UUID/set slot/endpoint as offline while all other
+members remain online. The double witness brackets every unique-key PUT ACK
+and degraded GET. Original member files, controls and formats remain exact
+through the offline window; all N-1 online new-key metadata replicas and
+payload shards must form one protected ACK authority. Original-drive-return
+admits one new key and offline-ack-repair admits six independent new keys.
+The same disk returns without restoring backups; each ACK needs its own
+manual task, complete repaired receipt, actual new target metadata/part,
+unchanged donor shards, protected streaming checks and full all-peer S3
+manifests. Ready/GET alone never prove repair. Live evidence is pending.
+
+B06 run `38081025407` retained FAIL and cleanup PASS. Its exact-R GETs on
+both H1/H2 returned correct full payloads, but the collector omitted
+`resultUnknown=false` on successful responses and the boundary check rejected
+them. Successful complete S3 responses now explicitly expose known results;
+partial bodies still fail. Both exact-R manual branches and H2 restored-W
+passed within that failed run. H1's R-1 read returned HTTP 500/InternalError
+instead of the frozen 503/SlowDownRead contract; that failure remains visible.
+No failed run is promoted to PASS.
+
+C06 run `38081269532` retained FAIL and cleanup PASS before injection. Its
+original ACK nonce was incorrectly compared as if it were an S3 property.
+Inventory checks now validate the nonce as private collector state while
+retaining exact version/marker/currentness/length/digest comparisons. A
+regression check exercises nonce-bearing ACKs against real public inventory
+shape, including wrong content and version negatives. Fresh live evidence
+is required for both corrected collectors.
+
+
+B03 `heal.disk-replacement-formatted` now dispatches both source branches on
+H1/H2, with full DB and historical/marker DV fixtures. A stopped, owned adapter
+moves exactly one complete private data root into same-mount quarantine and
+creates a genuinely empty new root. Before/after whole-tree digest and original
+inode/device preserve every old file, including system records; no shared
+mount is detached. The actual service uses a private namespace with quarantine
+masked and both DAC bypass capabilities removed. Same-process EACCES proves
+that the original cannot supply rebuild bytes. The candidate must initialize
+a distinct new disk UUID in the same original pool/set slot without replacing
+another member or changing geometry. Every original target version, marker,
+inline value and external part must rebuild through one exact deep task, with
+truthful complete receipts/counters, original same-role hashes, protected
+streaming checks and full all-peer S3 manifests. Missing/unchanged UUID or early
+automatic repair cannot satisfy this manual case. Partial rename/mkdir intent
+remains recoverable; archive drift retains the namespace. Live proof is pending.
+
+B01 run `38081858808` retains its original FAIL and cleanup PASS. Both H1/H2
+had real same-service read/write EACCES. Independent replay of all 46 H1 and
+45 H2 original Admin responses finds the exact original slot, endpoint and UUID with
+`state="drive access denied"` and cached `runtimeState="online"`. The candidate's
+`DiskAccessDenied` error renders that exact state, and its Admin disk-count
+logic counts it as offline. The executor omitted this spelling from its
+explicit error set, so it rejected the recorded witness and never started
+degraded ACKs. The state mapping now recognizes that exact error while still
+requiring the original identity and every other member's healthy UUID. Unknown,
+initializing, unrecognized error and online-only observations remain refused.
+Replaying this offline check does not supply the unexecuted ACK or repair
+branches; complete fresh H1/H2 live evidence is still required.
+
+
+B06 corrected run `38082447868` retained FAIL and cleanup PASS. Both exact-R
+GET and independent manual branches now pass on H1/H2; all three H2 branches
+pass. H1's R-1 GET still returns 500/InternalError rather than the frozen
+503/SlowDownRead, so no complete canonical PASS is claimed.
+
+C06 corrected run `38082227039` retained FAIL and cleanup PASS after proving
+ACK authorities: full fleet restart allowed physical changes before the manual
+token and the attribution check stopped execution. Rejoin now restores only
+the exact active permission gate in the same live process. Two identical live
+physical observations freeze the settled ACK authority; every peer's actual
+process birth/unit/candidate/environment must remain unchanged, and the fault
+must still exist immediately before the independent manual token. Startup
+reconciliation cannot contribute to the manual proof. Fresh live evidence is
+pending; the original attribution failures remain unchanged.
+
+E04 journal evidence now reads the first bounded page after each exact cursor
+using `journalctl -n +10000` and per-entry journal cursors. Full pages are drained
+in order, with six pages per capture and a hard failure on incomplete or
+nonadvancing windows. Raw pages remain evidence; only source-attribution targets
+are retained in the scanner's in-memory index. The healthy checkpoint starts
+after seed, baseline and the initial physical snapshot, so a fresh complete
+scanner cycle must actually occur in the registered observation window. Earlier
+log-budget failures remain FAIL; this harness change has no live PASS claim.
+
+F05 `heal.full-cluster-restart` now has an H2 executor for all three source
+branches. It seeds DB, historical DV/markers, completed multipart objects,
+exact pending UploadIds/parts and a 1,025-object DX observation fixture on the
+current four Heal peers. A root task must have physically repaired some but not
+all injected shards before the original coordinator is paused. Every original
+peer then exits with witnessed SIGTERM/normal code zero in a registered order.
+The same task ID, physical intent owner, non-timeout options and remaining
+budget must survive all units being stopped. Recovery uses no new root start.
+Complete canonical counters, bounded exact diagnostics, durable terminal/report,
+full same-role physical manifests, streaming shard checks and all-peer S3
+inventories must agree. All four processes restart again after completion;
+terminal records and user files must remain unchanged. Pending MPU listings
+remain exact across both restarts and their original parts are then completed
+and checked by full payload hash on every peer. Multi-bucket snapshots and
+streaming verification bind each run-owned bucket explicitly. F05 has no live
+PASS claim until that complete sequence succeeds.
+
+C06 run `38083035055` retained FAIL and cleanup PASS: all three H1 branches
+passed with the same live service rejoin, but H2 failed the older 10,000-line
+journal bound. The paginated journal implementation must be validated in a fresh
+run. B03 run `38083328049` retained FAIL and cleanup PASS on all H1/H2 DB/DV
+instances: the candidate had no replacement `format.json` at the first physical
+snapshot. The executor now observes a registered 90-second, read-only format
+initialization window before requiring distinct new UUID/set-slot evidence;
+service health still cannot certify replacement acceptance. Neither result
+increases the complete live PASS count.
+
+E06 `heal.replacement-auto-rebuild` now dispatches both H1/H2 DB/DV branches
+using the same whole-root replacement isolation and complete shard oracles as
+B03. Its isolated runtime enables only automatic disk replacement (10-second
+interval), with scanner and MRF disabled. The current lab's private directory
+roots explicitly use the candidate's directory-backed replacement fallback;
+physical-device replacement readiness is not claimed. Every peer must expose
+one fresh, stable generation bound to the exact target endpoint and set, with
+completed state and a fresh verification timestamp. Missing/partial/unknown
+status, a changed generation or divergent terminal records fail. No Admin heal
+mutation or target S3 read occurs before full same-role physical reconstruction
+and streaming verification. The old archive remains inaccessible to the service
+and unchanged. A healthy service or empty queue cannot finish the case. This
+executor still requires complete live evidence.
+
+E04 `38083897141` and C06 `38084060749` retained FAIL/cleanup PASS because the
+first paginated collector used journald's default JSON field-size limit, which
+can omit a long MESSAGE. The collector now requests `--all`, decodes complete
+UTF-8 byte-array messages and rejects missing fields; original failures remain.
+
+F02/F03 now execute both H2 source branches using automatic replacement. The
+unversioned observation fixture has 1,025 external objects; the versioned
+fixture has 64 keys, 192 data versions and their delete markers, with a 45-minute
+private-service lease. Only node1 enables automatic disk discovery; the other
+three nodes retain enabled Heal with discovery disabled. Before interrupting
+node4, fresh cursor-bounded logs must bind the original generation to accepted
+`auto_heal` admission and exact erasure-set task start on one distinct
+coordinator. Physical census must show both committed original same-role
+external shards and pending data versions. The target is paused through its
+exact MainPID/birth identity and the partial window is checked again. F02 then
+requires witnessed SIGTERM/normal exit zero; F03 requires pidfd-bound SIGKILL,
+signal 9 and a non-graceful systemd exit, with no platform power event. No other
+peer process may change. Restart uses the same unit, environment, roots and new
+disk UUID, reproves old-archive refusal and finishes the same automatic
+generation. Complete physical/streaming and all-peer S3 proof remain mandatory.
+A fast completed task, missing distinct coordinator, changed generation or
+missed physical window cannot PASS. External target verification uses bounded
+128-part batches to keep the DX run within its registered resource budget.
+
+B03 bounded run `38084312163` retained FAIL/cleanup PASS in all four instances:
+the candidate still did not initialize the replacement format within 90 seconds.
+That explicit acceptance failure is not relabeled as a completed reconstruction.
+
+
+E05 `heal.mrf-offline-write-replay` now has a dedicated H2 executor for all
+three source branches: unique version PUT, exact historical-version DELETE,
+and marker creation while the original fourth peer is stopped and its endpoint
+refuses connections. Every ACK must have a fresh active durable responsibility
+in the candidate's committed MRF snapshot and matching lifecycle companion.
+The read-only observer opens only six exact snapshot/commit/lifecycle filenames
+per private root. It validates full journal CRCs, both commit SHA-256 bindings,
+owner/sequence, responsibility ID and bucket incarnation; it never enumerates
+IAM or credential records, changes journals, or accepts operator risk.
+
+After the same node and disk identities rejoin, fresh manager admission with
+source `mrf`, an exact-version automatic object task, complete successful
+responsibility and `repaired`/`verified_healthy`/`authoritatively_absent` must
+agree with committed responsibility retirement. Missing/held/unknown results,
+queue emptiness, copied backup bytes or a manual verifier cannot certify this
+case. All 16 replicas, retained same-role bytes, deleted data directories,
+streaming checks and complete all-peer S3 ACK inventories precede PASS. The
+record/proof decoder is explicitly bound to candidate source
+`072b57b37559fbe626aeda8abf61617fdcbdd926` and reviewed source file digests;
+ordinary execution consumes package/tools without a RustFS source checkout.
+E05 has no live PASS claim yet.
+
+F05 run `38084904969` retained FAIL/cleanup PASS before any restart because its
+combined DB/DV/DM/DX physical census exceeded the cursor resource profile. It
+now registers a distinct bounded 24,576-file census with the original 2 GiB
+per-drive free-space requirement and 45-minute service lease. E04
+`38085241447` verified complete journal collection on the corrected `--all`
+reader, then failed its healthy task's missing public pool/set responsibility
+identity; cleanup passed. The original failed runs remain unchanged.
+
+
+C06 `heal.stale-version-rejoin` passed complete H1/H2 unversioned overwrite,
+historical-version deletion and marker creation in run `38085554143` at harness
+`8d78ff32188412644a55776be7a794024a4f5614`. All six applicable instances have
+PASS and cleanup PASS; downloaded canonical evidence passed the independent
+selected-case gate. This raises the full live PASS total to 44. Older incomplete
+journal-window runs remain FAIL.
+
+
+B04 `heal.disk-replacement-unformatted` now dispatches both H1/H2 source
+branches with independent 128-object ACK fixtures. A second released-process
+H1 service generates the actual foreign format; the read-only exporter retains
+its exact bytes, digest, candidate binary and cluster/member identities before
+complete donor cleanup. The replacement adapter copies only those verified
+format bytes into the witnessed empty run-owned root, rejects reused original
+identities, and never manufactures a disk UUID or edits the old archive.
+
+Blank initialization must expose the candidate's new UUID in its exact original
+slot, complete the same automatic replacement generation, reconstruct every
+original same-role shard, pass streaming verification and preserve all peer
+ACK inventories. Refusal instead needs the exact Admin slot to remain explicitly
+offline/unformatted/corrupt throughout a registered 90-second window, with all
+other UUIDs/formats/user files unchanged and no target user writes. Foreign
+format bytes must stay exact. The negative branch then retains the rejected
+scene and restores the original archive only for recovery; it explicitly makes
+no product-rebuild claim. Complete all-peer S3 and physical checks are still
+required. B04 has no live PASS claim yet.
+
+E06 `38085937937` retained FAIL/cleanup PASS in every H1/H2 DB/DV instance.
+Three instances generated a format without a distinct replacement UUID; the
+H2 unversioned instance instead exposed partial/unknown/incompatible cluster
+replacement status. Neither directory-root fallback nor a format file alone
+certifies actual replacement readiness or responsibility.
+
+
+The cursor resource budget now includes the candidate's `part.N.integrity.*`
+sidecars: 1,025 external objects on four roots already require at least 12,300
+files. The explicit limit is 16,384 and the registered DX budget reads the same
+configuration. F02 `38086220066` and F03 `38086565751` retain FAIL/cleanup PASS:
+their DB branches exceeded the old 12,288-file allowance; the DV branches
+failed to expose a distinct replacement UUID. Budget correction permits fresh
+verification and does not change these original results.
+
+E05 `38086919036` retained FAIL/cleanup PASS on all three branches. PUT/marker
+reached durable ACK binding but the replay caller passed an unsupported label
+to `JournalWindow.capture`; the actual shared interface and replay observer
+are now covered together by a regression test. Exact historical deletion
+failed to expose committed durable responsibility within the registered
+30-second persistence deadline. That failure remains; no substitute manual
+mutation or inferred queue record is accepted.
+
+J06 `heal.performance-abba` now dispatches three complete H2 ABBA groups on
+the same candidate, rc, four nodes, original roots and disk UUIDs. A frozen
+protocol sets 30-second phases, two workers, four requests per second, a 1:1
+GET/PUT ratio and the same 16 logical 64KiB load objects. Every scheduled request
+has durable intent/receipt evidence; unknowns, failed samples and missed
+admission deadlines remain in both error and latency denominators. Each phase
+restarts the same services and uses the same fixed warmup without changing host
+caches. Every A phase proves idle throughout and unchanged repair files. Each
+B phase injects the same 17 missing data/parity shards in an independent
+1,025-object repair bucket and binds one actual task's partial running progress
+to both foreground operations. No observed overlap means NOT_OBSERVED and a
+complete-case failure.
+
+Exact owned-process birth, boot and settings bind read-only CPU, logical/storage
+IO, RSS and FD samples. Candidate completion, actual reconstructed shard bytes,
+streaming verification and full peer S3 manifests must agree. Repair throughput
+uses physical target bytes, separately from logical S3 throughput. Frozen gates
+require zero errors, GET/PUT P95 <= 5s and P99 <= 10s, paired B/A ratios <= 3,
+positive verified repair throughput and bounded resources. All twelve phases
+and every paired difference are reported; no phase may be omitted or reordered
+and functional success cannot substitute for performance acceptance. The first
+candidate run expired its 45-minute fault lease before group 3 phase 3; its
+physical failure scene showed no applied fault and the retained namespace was
+later independently finalized. The executor now binds a 120-minute lease for
+the full evidence window. J06 has no live PASS claim yet and needs a fresh
+candidate run.
+
+
+F05 `38087158034` passed the expanded physical census and reached the real
+all-peer graceful exit, then retained FAIL/cleanup PASS when the original
+process exited between systemd MainPID and `/proc/PID/stat` reads. The node
+observer now refreshes the complete service observation up to three times
+only for disappearing `/proc/PID` entries. It never infers PID zero from
+absence, accepts arbitrary file loss, or relaxes exact normal exit code zero,
+service identity and pidfd birth checks. A fresh run is required.
+
+
+B04 `38087657754` retained overall FAIL/cleanup PASS. H1 foreign-format passed
+its complete structured refusal window, unchanged original format/controls,
+rejected foreign bytes and all 128 ACK/S3 checks, with archive restoration
+explicitly excluded from product repair. Both blank branches lacked a distinct
+new candidate-generated disk UUID. H2 foreign-format did not make every bound
+peer ready; a startup failure alone is not relabeled as structured safe refusal.
+
+A02 `heal.admin-scope-isolation` now dispatches all five selectors with fresh
+H1/H2 fixtures, two owned buckets and exact `a/`, `ab/`, `b/`, nested, Unicode
+and literal encoded-slash keys. The candidate-bound H2 runtime preserves the
+current four nodes and sixteen independent mounts, explicitly configures two
+8-drive sets and interleaves original endpoints so each set spans all four
+hosts. Actual format UUIDs, complete set membership, metadata distribution and
+same-role shard identities bind every ACK to its physical set. Existing
+single-set executors retain their strict default and reject this layout unless
+it is explicitly requested. No format, UUID or second control set is invented.
+H1 retains its original four-drive set and records the physical applicability
+boundary for a nonexistent unselected set.
+
+Root covers the entire owned service manifest; bucket, exact object and `a/`
+prefix selectors preserve other buckets, adjacent keys/prefixes and control
+faults; pool/set selectors require real selected-set reconstruction and an
+unchanged missing shard in the other actual set. The complete responsibility
+set and canonical counters must match all selected ACKs, including healthy
+objects. On this exact candidate, unrestricted request selectors may remain
+null in raw receipts; independent complete physical mapping resolves their
+scope in separate evidence, without rewriting public fields or weakening
+shared validators. Explicit selectors require exact integer receipt scope;
+partial nulls, wrong sets, mixed incarnations, duplicates and unknowns fail.
+
+Full physical comparison and selected streaming integrity precede any target
+GET or unselected-control backup rescue. Rescue cannot certify product repair.
+Both complete S3 manifests through every peer and a final unchanged physical
+scene are required. A02 has no live PASS claim yet. Ten scope/topology/receipt/
+control negatives join the focused local checks; 226 checks now pass.
+
+
+D11 now dispatches all four source branches separately on H1/H2 (eight fresh
+instances). Its dedicated node backend derives only two owned bucket-config
+object paths and their incarnation controls. It never widens the general
+user-object fault allowlist, walks IAM records, or prints bucket-policy/inline
+payload bytes from physical metadata. The candidate startup persists a non-sensitive
+`config/iam/format.json` object even in a fresh root-only service. Its exact
+physical files and streaming proofs are read-only controls before and after
+execution; mutation never targets that object. The six exact sensitive IAM
+subtrees are checked for absence without traversal or payload reads. The reviewed
+IAM manager source hash is bound alongside the bucket/XL2 contracts. Exact system object geometry
+binds each shard index to the original disk UUID/set slot; current inline or
+external bytes require candidate-bound streaming verification.
+
+The fixture preserves Enabled versioning, bucket-only policy, tags, an Object
+Lock default set after fixture writes, and all acknowledged historical business
+versions. One config-envelope replica is missing or truncated for positive
+recovery; the candidate's startup/config reads and explicit bucket Heal are
+recorded as allowed mechanisms. Every original system file must be restored
+byte-for-byte before backup rescue or business GET, while incarnation/control
+records, business files and formats remain unchanged. The unknown-record
+negative changes only outer XL2 major 1 to 2 on the exact primary config
+replicas; it claims no inner msgpack schema coverage. It requires structured
+public refusal, zero successful repair responsibility, preserved unknown
+bytes and healthy control-bucket APIs. A startup/readiness failure alone is
+not safe-refusal evidence. Rescue archives remain separate from product
+repair, and an unrecognized config rewrite retains the stopped namespace.
+D11 still requires live evidence.
+
+F02 corrected-budget run `38088240693` retained FAIL/cleanup PASS in both H2
+DB/DV branches: neither reached partial-rebuild interruption because the
+candidate did not expose a distinct new replacement UUID. F03 corrected-budget
+run `38088777413` also retained FAIL/cleanup PASS in both H2 DB/DV branches:
+replacement source status was partial, unknown or incompatible. The bounded
+file census no longer prevented either run; these product/evidence failures
+are not converted into successful restart coverage.
+
+
+The optional native-regression tools job builds the unmodified candidate's
+optimized ecstore test executable on hosted Linux, retains Cargo/lock/toolchain
+identities, enumerates actual full test names, and runs each selected name with
+`--exact` in a fresh process and a fixed 120-second deadline. It currently
+collects two real read-repair/namespace barrier regressions and two uncontrolled
+overwrite/delete concurrency regressions. Zero-test filters, ignored tests,
+foreign source/collector identities and other-name success are rejected. The
+four regression verdicts have their own archive and never increment a fleet or
+canonical-case PASS count. Existing tests assert captured lock events internally;
+the collector does not fabricate emitted barrier logs. Missing critical-point
+fixtures, Admin nolock semantics and required fleet overlap remain explicit gaps.
+The current implementation has 310 focused Heal/catalog checks, including the
+Linux-only ownership/mount/recovery checks. Hosted native compilation and all
+four exact regressions completed in run `38096433036`; their independently
+verified identities and limits are recorded below.
+
+
+J03 corrected-budget run `38089238767` passed every applicable within-window,
+past-retention-gap and terminal-truncation branch on H1/H2. The cross-node
+branch is explicitly NOT_APPLICABLE on single-node H1 and passed on H2. It retained complete DX responsibility, real running-status windows,
+physical/streaming integrity, all-peer S3 evidence, cleanup PASS and the
+independent canonical selected-case gate. This increases complete live PASS
+to 45/158; branch-only successes and native regressions remain outside that
+count. A06/F06/F01 and A02/E05/F05/J06/D11 queues continue serially on the
+current auto-testing fleet. The optional native build uses the existing
+Functional Chain tools-only driver on hosted Linux, because the reusable
+workflow is still in a draft PR and cannot be dispatched directly from the
+default branch.
+
+
+B08 now has an automatic current-fleet executor for both source branches.
+The write-negative branch measures every original metadata read and root write
+inside the exact candidate process mount namespace. Precisely N-W+1 private
+roots return EROFS, including the missing payload target; all N metadata
+members and all lock clients remain online. The H2 lock-negative branch
+isolates two exact run-owned peer processes, witnesses their exited MainPIDs
+and refused endpoints, and keeps R metadata/payload sources and shared-lock
+quorum while losing exclusive-lock and object-write quorum. It does not claim
+host network partition or physical disk loss. H1 uses an in-process local
+manager; its distributed peer-loss branch is explicitly NOT_APPLICABLE with
+the reviewed candidate locking policy, while its write-negative branch remains
+mandatory. Both original branch IDs and H1/H2 contracts are retained.
+
+Every negative scene is frozen before removing the fault. A single unique-key
+PUT has no automatic retry; all unknown outcomes and success ACKs are queried
+on every recovered peer. Unresolved outcomes retain the stopped namespace and
+fail cleanup. The negative Heal must preserve complete failed/deferred exact
+responsibility with zero repaired counters and no changed physical control.
+After gate removal/source return, an independent task must reconstruct the
+original missing target before any target GET or backup rescue. Streaming
+verification, all-peer complete S3 manifests and clean finalization remain
+required. Ten boundary, identity, mount, cleanup and unknown-ACK negatives join
+the focused checks. This implementation section predates the complete B08 acceptance above; its earlier no-PASS statement remains historical.
+
+A06 run `38090151509` retained FAIL/cleanup PASS on H1/H2 after its repeat-stop
+assertion. It exposed a harness contract defect: the reviewed manager's
+`root_recovery::cancel_pending` treats a retained terminal record as an
+idempotent stop, so the public response is HTTP 200/stopped, rather than the
+unknown-token HTTP 500 assumed by the harness. The original result remains
+unchanged. Fixing the candidate-bound assertion and repeating the complete
+canonical run are required; the observed acknowledgement alone is not PASS.
+
+
+The A06/F06 durable-retired-token assertion is corrected against all four
+reviewed source hashes, including `root_recovery::cancel_pending`. It now
+requires the exact signed old-token request and the candidate's HTTP 200,
+stopped acknowledgement shape. Its default acknowledgement settings and new
+response timestamp are not mistaken for task identity or original task options.
+The old settled canonical outcome must remain byte-equivalent immediately
+after both repeat and stale stops, and the new task must stay running across
+the stale request. Unknown-token queries and forceStart-retired tokens without
+a retained durable terminal keep their independent exact 500/InternalError
+checks. Both archived A06 acknowledgement samples satisfy the corrected shape;
+that replay is not fresh live acceptance. Complete A06/F06 reruns remain required.
+
+
+C09 `heal.foreign-format-and-unknown-schema` dispatches all four canonical
+branches on H1/H2. An isolated second released-process deployment exports a
+real foreign format. The disk branch replaces only `xl.this` with its witnessed
+donor UUID; the set branch copies the exact donor bytes, including its real
+four-member set when the target has sixteen members. This topology difference
+is retained explicitly. The future-format branch changes only the top-level
+V1 semantic version to `999`. Only one empty, owned replacement root is
+modified, while its complete original root is quarantined and inaccessible to
+the candidate. Shared formats and IAM are outside the mutation scope.
+
+A registered 90-second window requires exact Admin slot/endpoint refusal and
+stable original peer identities, followed by deep Heal and target GET. Startup
+failure alone cannot earn PASS. Manual Heal may verify the surviving object or
+report its complete failure, but must never claim reconstruction of the refused
+member. All surviving files/formats and the injected bytes stay frozen.
+
+The object branch changes only the reviewed XL2 envelope's little-endian major
+from 1 to 2 on every member of one ACKed target. It does not fabricate a future
+inner msgpack schema. Per-disk metadata hashes bind each actual decoder refusal;
+all peer GETs must return a structured server refusal, and deep Heal must retain
+complete failed responsibility without a missing/healthy/repaired claim. All
+payloads, control objects and formats remain exact. Only after the negative
+proof may original bytes/identity be rescued. All 128 ACKs, every external shard
+with streaming checksums, and full S3 inventories through every peer are checked
+again. Backup rescue never earns product-repair credit; failed recovery retains
+the namespace and stops subsequent fleet use. C09 still requires fresh live
+evidence. Eleven targeted oracle/guard tests are added; its actual Linux
+FD/mount guard test runs in CI, not as a simulated macOS syscall claim.
+
+
+F01 `heal.root-graceful-coordinator-restart` run `38091619866` retained
+canonical FAIL with cleanup PASS. H1 passed both branches; H2 exceeded the
+collector's 2 MiB response limit during original-token recovery. The reviewed
+candidate `crates/heal/src/heal/channel.rs` permits an 8 MiB status payload
+(source SHA-256 `7f669ca43b9559db9b97fba6b419a6c76a94d76acd8526db132d74b7c534d6b0`).
+The collector now uses that explicit 8 MiB bound, records complete byte counts
+and still rejects overflow or Content-Length mismatch. A boundary regression
+covers a complete 8 MiB JSON response, one-byte overflow and truncated bodies.
+No F01 canonical PASS is claimed until a fresh complete rerun.
+
+A06 run `38092515125` retained FAIL/cleanup PASS on both H1/H2 because the
+collector represents signed query pairs as Python tuples in memory and as
+JSON arrays on disk. The exact durable-stop validator incorrectly accepted
+only the latter. It now normalizes either pair representation while retaining
+the exact pair count/order/token, signed POST, empty body, response shape and
+old/new responsibility assertions. Tests reject duplicated tokens and mapping
+queries. All six CI lanes passed at `62d0ca9`; fresh complete A06/F06 runs are
+still required. Earlier FAIL results are preserved.
+
+D08 `heal.encrypted-objects` dispatches SSE-S3, SSE-KMS, SSE-C and encrypted
+historical versions on both H1/H2. It configures one fresh AES-256 static KMS
+key across the run-owned stopped services, passes material through SSH stdin
+and retains it only in a private 0600 file inside each run namespace. Every
+snapshot attests the file/configuration identity and the actual process
+settings, including explicit per-key authorization enforcement. Shared KMS
+resources are not modified and secret material is excluded from evidence.
+
+Each branch independently ACKs sixteen external encrypted objects; the
+versioned branch ACKs three distinct generations per key. It removes a data
+shard and corrupts a parity shard, including historical and current versions,
+then requires a manual deep token with complete exact responsibility. Before
+any decrypted target GET, the original same-role ciphertext hashes/lengths and
+streaming integrity must be reconstructed. An encrypted-only mapper preserves
+the distinct plaintext actualSize and ciphertext part size without relaxing
+the ordinary plaintext mapper. Every peer then verifies the entire plaintext
+ACK/version/tag/property inventory and re-freezes the ciphertext scene.
+
+SSE-C missing/wrong keys must produce the reviewed 400/InvalidRequest refusal.
+SSE-KMS principals have the same working S3 GET rights to an independently
+verified plaintext control, but no decrypt grant or only an unrelated key
+grant; the encrypted GET must return 403/AccessDenied before and after repair.
+SSE-S3 records that its service-managed key has no caller decryption-key
+argument. Failed IAM cleanup retains a DIRTY stopped namespace. Nine targeted
+geometry, authority, version, redaction and Linux file-ownership tests pass;
+D08 still requires fresh complete live evidence and has no live PASS claim.
+
+
+A02 `38094094085` passed every applicable H1/H2 root, bucket, object, prefix,
+recursive and actual two-set selector branch. H1 keeps the source's selector
+inapplicability explicit; H2 tests the actual independent sets. Its complete
+physical/streaming and all-peer ACK manifests, cleanup PASS and independent
+canonical gate increase the complete live total to 46/158. F06 `38093199055`
+retained FAIL/cleanup PASS on the already queued old harness; its durable-stop
+query assertion is fixed at `62d0ca9` and remains queued for a fresh run.
+B08 `38093907057` retained FAIL/cleanup PASS: H1 lacks the exact incarnation
+receipt, while H2's actual default layout cannot preserve R with the selected
+write/lock-loss plan. No layout or missing identity is assumed sufficient.
+
+F04 `heal.coordinator-process-crash` now dispatches its three separate source
+mechanisms on actual H2. It records a candidate-bound persistence support table
+and only sends SIGKILL through Linux pidfd after freezing the unique original
+coordinator at nonzero and unfinished physical progress. Target and other peer
+process birth identities must remain unchanged throughout recovery. No host or
+platform restart is issued, and an unobserved window cannot pass.
+
+The root branch ACKs the full DX fixture, places missing original data shards
+outside the coordinator, freezes the exact durable admission owner and requires
+the original token to replay with complete responsibility. SIGKILL must leave
+the original intent bytes unchanged; it makes no graceful elapsed-time or cursor
+checkpoint claim. Repeated terminal restart, original record disk, full physical
+hashes/streaming and all-peer S3 inventories remain mandatory.
+
+The MRF branch ACKs sixty-four independent never-versioned DB writes while one
+original node is offline. Its isolated profile enables only MRF, with one worker,
+one task per set and a replay batch of one. Actual process settings and the
+configuration source digest are retained. Fresh accepted MRF task/start logs,
+exact version/null representation and a partial target census bind the paused
+coordinator to a still-committed original ACK responsibility. The complete
+CRC/commit/lifecycle checkpoint remains byte-identical across the crash.
+The lost volatile token must return its explicit unknown-token error; a separate
+new automatic task must prove the interrupted ACK, while every ACK's original
+responsibility retires with a verified receipt. Automatic rescan success is never
+reported as original-token recovery. The final census, all surviving original
+shards, streaming checks and S3 inventories gate the branch.
+
+The replacement branch independently ACKs 1025 external objects and preserves
+an inaccessible original drive archive. Its coordinator is distinct from the
+live target. An exact-name read-only journal captures the schema-seven sealed
+intent for the original generation, endpoint, physical device and filesystem
+identity before and after SIGKILL. The original generation must finish on every
+peer after the same coordinator unit restarts, with the original replacement
+UUID/set slot, full same-role shard hashes, streaming and ACK inventories.
+No manual replacement compensation or backup rescue earns repair credit.
+
+Eleven focused F04 tests cover process continuity/exit, exact root intent,
+replacement seals/name/identity and filesystem guards, partial MRF authority,
+source attribution, bounded configuration and mandatory three-mechanism scoring.
+F04 has no live PASS claim yet. The source-bound MRF log reader also accepts the
+reviewed Rust Option debug spelling without merging different version identities;
+existing E05 receipts and every original branch remain independently required.
+
+
+B08's read-only plan now measures writable drives independently of readable
+sources, including the candidate's actual default H2 R=12/W=12 geometry. Five
+owned read-only roots leave fifteen payload sources and all sixteen metadata
+authorities readable while only eleven roots remain writable. Numerical R` commitments, independently observed in the retained
+A06 physical evidence. Only canonical non-nil commitment UUIDs, one commitment
+per existing part and the bounded part/commitment budget are accepted. Every
+commitment's exact bytes and name follow its chosen archived generation;
+payload-less, repeated or ambiguous commitments are rejected. Two additional
+regressions verify complete A/B commitment preservation and invalid-leaf
+refusal. The updated local suite runs 377 tests with seven macOS skips; the
+real Linux generation test also includes a commitment file.
+
+C10's shared-version branch requires the candidate's real metadata codec,
+two independent same-payload ACK versions and physically identical original
+parts. Its encoder is built as an optional immutable helper alongside the
+read-only inspector, without modifying the candidate source. It reads bounded
+original metadata from stdin and emits a separately encoded result on stdout;
+it has no filesystem mutation API and makes no physical byte-equality claim.
+Only the destination data-directory reference and its source commitment
+descriptor may change; all version identities and unrelated metadata are
+reloaded and compared before output. Three actual codec tests must pass against
+the candidate before the artifact can be consumed. Python guards additionally
+bind source/tree/Cargo.lock/helper/binary/role and the successful build attempt,
+and reject incomplete, ignored or zero-test output. This artifact is the prerequisite for the complete C10 executor described below;
+tool success remains independent of the canonical live verdict.
+
+
+C10 now dispatches all five source branches on both H1 and H2. Runtime profiles
+freeze the source's 0/300/30-second dangling policy, and each trigger records
+server LastModified, original-node clock agreement and the actual elapsed age.
+All independent original roots archive their complete objects, including real
+integrity sidecars and absent post-DELETE trees. Durable arm records precede
+controlled replay or shared conversion. Rescue uses the last acknowledged
+post-DELETE authority on every original root, preserves the failed scene and
+cannot restore the deleted old generation. A shared seed is retired before
+later ACKs; unknown writes, lost arm receipts and partial recovery retain DIRTY
+namespaces and block cleanup.
+
+The shared branch proves two independently acknowledged original generations
+have identical physical parts before the candidate codec changes only their
+shared data-directory/commitment references. Both full peer S3 inventories and
+all physical references must agree before the later third version is created.
+The retained shared references and delete-marker controls survive orphan
+removal. Pending MPU uses a bounded, descriptor-relative, read-only census of
+the isolated run's complete multipart tree, exact all-peer UploadId/list_parts
+checks, and an acknowledged completion followed by full payload verification.
+Product physical and S3 assertions run before any fixture rescue. Every live
+part receives candidate-bound streaming verification before final S3 checks.
+
+Hosted tools run `38103474555` attempt 1 succeeded at driver
+`9b4cc40abd5ea3a0a5ee6c52fb7e113b57102d6e` and helper
+`6cc8cb79a0f8024b0089a43d31b0708a46bbae4c`. Independent original artifact/API
+verification confirms all three codec tests, source/tree/Cargo.lock/helper/role
+and binary `eb2991cf6f84b6998a474c51cd142c2b1c84106fd7f49f569fce7a241dc4ecea`.
+Runs `38102872003` and `38103273662` failed on invalid synthetic timestamp and
+non-persisted/non-marker rejection inputs; these failures remain retained.
+The corrected inputs keep all transformation/refusal assertions. The earlier
+wrong-SHA dispatch `38102848357` was cancelled and is not tool evidence.
+Ordinary C10 execution consumes this artifact without a RustFS checkout.
+
+The complete local validation suite now runs 397 tests with eight macOS skips.
+C10 has 20 tests covering source completeness, typed deferred/refusal results,
+all-member live reference/part/commitment/control safety, absent-authority
+recovery, foreign device/candidate refusal, uncertain ACK retention, shared
+seed retirement and pending-MPU alias refusal. Its real Linux descriptor,
+root-owned recovery and nested bind-mount test is mandatory in CI. C10 still
+requires canonical live evidence; there are 132 automatic executors and 26
+remaining Heal executors.
+
+F06 `heal.cancelled-task-restart` passed in run `38102031029` at harness
+`f248cb63d032cfc1658d32c508bce745f96b885f`: both H1/H2 partial cancellations,
+quiet physical windows, persisted cancellation after restart, no old-task
+replay, new-token repair, all-peer S3 and cleanup passed. Its independently
+rerun canonical gate passed, increasing complete live PASS to 49.
+F04's coordinator process-crash run `38103118033` remains canonical FAIL with
+cleanup PASS: root restart readiness, the MRF collector event budget and
+replacement disk-UUID observation did not satisfy their gates. The serial fleet queue continues under its existing lock and
+unclean-environment guard.
+
+
+All six auto-testing checks passed at C10 executor commit
+`b0fc7d15ba170bd592e6f4012c1ba15125186afb`. Original Linux contract logs from
+run `38104232517` show 397 Python tests in total with no skips, including real
+C10 absent post-DELETE recovery and nested bind-mount refusal. The serial C10
+live plan binds this harness and successful shared tools run `38103474555`;
+it waits for the existing queue and does not change an active plan or guard.
+
+
+C10 verifies each ACK version separately when two versions share one physical
+part path. The general scope verifier keeps its independent-path requirement;
+C10 reports version-addressed streaming checks and distinct physical parts
+separately, without inventing independent disks. New regressions verify both
+VersionId calls and complete shared-conversion/seed recovery with retained
+original metadata, parts and commitments. The updated complete local suite
+passes 399 tests with eight macOS skips; C10 now has 22 tests. Its queued live
+plan is updated to this corrected revision before fleet dispatch.
+
+
+A10 dispatches all nine source option branches on H1/H2 and fresh DB/DV
+namespaces. The frozen source `524eb5b32c3236497fdd8a9271df0494fa3d9974`
+executes its proven orphan only with `recursive=true,remove=true`; its
+`remove=false` branch independently heals a repairable ACK-bound object.
+The former migration added four synthetic remove=false orphan baseline
+instances. Those extra instances are outside the source procedure and have
+been removed; all original branches, both topologies and both fixture types
+remain mandatory, for 68 execution instances. Other option probes retain
+baseline/selected pairs changing one option or the pool/set selector pair.
+Exact candidate Admin/selector and validation-source hashes are recorded;
+this is API coverage. H2 scope probes use two actual eight-drive sets across
+the original four nodes. Recursive probes include child/adjacent/outside
+controls; selectors bind every ACK version to its measured physical set.
+Historical versions, markers, exact responsibility, counters, retained settings
+and structured rejections remain required.
+
+The source-aligned remove=true probe seeds one orphan ACK version and retains
+all control versions/markers, streams every original shard, archives the full
+original tree, performs an exact DELETE, and proves S3 and all-member physical
+absence. It then replays the complete original object including xl.meta and
+integrity sidecars on exactly one below-quorum member. A recursive bucket
+sweep must remove the whole orphan object before GET or rescue and leave
+all post-DELETE controls/formats unchanged. Every original orphan/control
+version must have an exact complete canonical receipt and counter partition;
+only the removed ACK may report repaired or authoritatively_absent. Rescue
+restores only post-DELETE authority. Lost mutation ACKs and incomplete restore
+retain DIRTY namespaces. The old data-only/exact-object construction and its
+original live failures remain archived; they do not establish a source remove
+product failure. No new live or complete canonical PASS is claimed here.
+
+B08 live run `38104014806` remains FAIL with cleanup PASS: both write-loss
+branches produced incomplete target identities; H2 lock loss also failed a
+snapshot that incorrectly required a live PID on stopped peers. The collector
+now attests online processes normally and reads every stopped original member
+only with the quiescent check. Two regressions retain all physical members and
+reject foreign stopped-node bindings. No quorum or identity assertion changed.
+A new immutable live run is required; the old failure stays visible.
+
+The current flapping/MRF queue retains `38104306522` and `38104630791` as
+canonical FAIL with cleanup PASS. F03 drive-offline observation, MRF admission
+and source ownership checks failed. E03 offline PUT and marker branches passed,
+but its exact offline version DELETE had no committed durable MRF responsibility
+within the registered window. These partial results do not add canonical PASS.
+All six checks passed at `23953a96b7c565da3bee9f7c9442d803ba9ab7cc`; Linux run
+`38104493427` executed all 399 then-current tests without skips. The current
+A10/B08 local suite passes 419 tests with eight macOS skips. There are now 133
+automatic executors (74 non-Heal, 59 Heal), 25 remaining Heal executors and
+49/158 complete canonical live PASS results. All original 158 IDs/hashes and
+84/301/532 Heal recipes/branches/topology templates remain unchanged.
+
+
+All six A10/B08 checks passed at `7128b50818de3206f6fb368d4ea696e56da7206b`.
+Original Linux run `38105302558` executed all 419 tests with no skips. The
+A10/B08 live plan retains that exact revision and successful inspector attempt
+`38103474555`, under the original fleet lock and cleanup guard.
+
+`environment.inspect` is a separate read-only workflow operation, outside the
+158 canonical IDs. It records the current four Heal hosts, all eight possible
+mount endpoints on each, filesystem and physical-device identities, available
+space and kernel/command interfaces. A shared partition, stacked/virtual
+device, ordinary directory, duplicate filesystem identity or insufficient
+space cannot certify an independent endpoint. H3/H4 resource shapes remain
+separate from actual EC8+4 layout and fault proof. An installed cloud command
+cannot certify platform restart permission or a compatible release pair.
+No namespace is reserved, no service is stopped or installed, and no case is
+scored. Five regressions guard these boundaries; the updated complete local
+suite passes 424 tests with eight macOS skips.
+
+The six pending live plans now write their downloaded original evidence to
+`/Volumes/Developer/KAI/validation-migration-evidence`; their candidate, tool
+and harness identities are unchanged. This moves only future outputs because
+the system disk has about one GiB free. The currently active flapping/MRF plan
+and every existing artifact remain at their original locations. The local
+`pending-output-locations.json` records each prior/new destination.
+
+
+I04 `heal.queue-pressure-and-admission` now has all four source branches on
+H1/H2. Its existing private Heal services freeze a two-slot queue and one
+worker. The 769/129/129/129-object independent buckets have full ACK and
+physical manifests; Merged returns the same token, Dropped carries its exact
+structured overlap reason, and Full requires HTTP 503 SlowDown with
+`admission=full, reason=none`. A live private drive refusal creates three
+exact degraded-write ACKs. Actual Normal-priority MRF Full events must occur
+while all three original committed responsibilities remain active. Recovery
+keeps the original process, and retirement requires each successful exact
+source task followed by all-member physical checks and every peer's complete
+S3 manifest. Queue/source/priority counts, request latency, RSS, FD and thread
+curves remain bounded by frozen budgets. Twelve negative oracle tests reject
+false admission, wrong identities, premature retirement and resource drift.
+No I04 canonical live PASS is claimed. There are now 134 automatic executors
+(74 non-Heal, 60 Heal) and 24 remaining Heal executors.
+
+Read-only current-fleet inspection `38106333882` at helper
+`b75f4a7602dca463c5aa532133ddc022f132051f` found 16 independent eligible
+endpoints: four on each of the existing four Heal nodes. H1/H2/H3 resource
+shapes are available; H4's required 24 endpoints are unavailable. This does
+not prove actual EC layout or an H07 PASS. H6 restart authority and a
+compatible immutable release pair remain unverified. Original Linux contract
+run `38106020097` executed all 424 tests without skips.
+
+F04 `38105011828` remains FAIL with cleanup PASS: root canonical counters,
+MRF partial replay admission and replacement disk identity failed their
+independent gates. I02 `38106408335` remains FAIL with cleanup FAIL: bounded
+block exhaustion was not witnessed, inode repair admission failed, and the
+quota GET returned usage-unavailable before workload/Heal admission. Failure
+recovery incorrectly called the stopped service. Quota rescue now restarts
+only its owned service and independently checks config without claiming
+usage or product repair. The narrowly supported archived H1 quota failure
+can also restore both original missing parts against an unchanged complete
+scene and remove its private bucket/quota namespace without starting a
+service. Invoked/uncertain PUTs, changed candidate/run/versions, missing fault
+receipts or another dirty instance reject finalization. Original failure
+results remain immutable; any subsequent recovery has separate evidence.
+
+Recovery run `38107394081` rescued both exact original shards but failed
+before namespace removal because its remote finalization snapshot lacked the
+encryption attestation module. Both audited finalization actions now carry
+that dependency. An interrupted quota rescue may resume only when every
+remaining file and format matches the archived failed scene and each added
+file is one of the exact original missing shards. The original fault intents
+and backups are revalidated even for already restored bytes. Unexpected
+content, missing controls, duplicate files or another host still retain the
+namespace. Two regressions exercise remote dependency loading and every
+accepted/rejected retry scene. The fleet remains guarded until a separate
+successful recovery proves namespace removal; neither original FAIL changes.
+Linux contract run `38107374734` independently confirms all 441 tests passed
+with no skips at `e40fd9d74a902b9b9733c7bddfdc0b9d1b3f5c03`.
+
+Independent quota recovery `38107681061` at
+`6ef733d6d62e6a9fe6f9e0d6e8f4f01cdb947165` completed with the owned service
+stopped, both original shards restored and the complete archived scene hash
+`979e39a78712ed39bba8af43acbd1187d57c9e5edd730f474cad3c2f2936d2d4`.
+Its original finalization evidence proves private roots and bucket/quota
+namespace removal. Original I02 canonical FAIL and cleanup FAIL are retained.
+The serial queue resumed after independent successful-attempt API identity,
+artifact hashes and exact failure/guard binding were checked. Rejected
+short-SHA dispatch `38107655997` never entered recovery. Linux contract
+`38107650661` executed all 443 tests without skips; all six checks passed.
+
+`manager_native.py` adds a separately collected exact-source manager suite
+for F07/C11/J02 prerequisites. Fifteen original candidate tests cover corrupt,
+future and stale checkpoints, real MRF payload/manifest write failures,
+four deterministic migration publication interruptions, uncertain root
+terminal publication, incarnation replay/checkpoint guards and outcome truth.
+Hosted `build_manager_native` builds the unmodified `rustfs-heal` library with
+the immutable candidate tree, lock and eight reviewed source hashes. Every
+test runs by its complete name in a fresh process. Verification retains both
+inventories, exact commands, executable and collector digests, original logs
+and individual results. Seven negative evidence tests reject zero/ignored
+execution, scope inflation, wrong build attempts, filtered names and modified
+source or executable identities. This suite does not inject individual fsync
+hooks, demonstrate physical power loss or execute canonical fleet branches;
+it adds no automatic executor or live PASS count by itself. Existing C07
+native collector and pinned artifacts retain their exact contract.
+
+The F07 durable-record backend now reads only exact root/checkpoint task names
+or the six allowlisted MRF slot files, without enumerating system metadata.
+Injection requires the stopped owned recovery service, unchanged candidate,
+actual run/disk/mount identity and archived complete file digest. An Admin
+checkpoint must retain its original task, owned bucket incarnation and valid
+candidate integrity digest. MRF injection additionally proves an actual
+committed active responsibility on the selected slot. Controlled truncation,
+future schema and CRC corruption retain original backups and intents before
+mutation. Terminal reports, resume states, blocked fences, commit manifests
+and unrelated records are read-only. Rescue revalidates backups and exact
+injected/original bytes; advanced or removed product responsibility is never
+resurrected. Distinct injection/rescue temporaries preserve interrupted
+evidence. Ten file/remote-boundary regressions cover these guarantees. This
+backend remains an F07 prerequisite; the six canonical branches and their
+full live execution are still outstanding, with no count or PASS increase.
+
+Hosted manager run `38108127382` attempt 1 succeeded with driver
+`b8126e50f78e40dfd108c7bf32162c751d4a87dd` and collector
+`aba6f5c6a36121adcc5a67663b767d0de5667ba9`. Independent artifact verification
+confirmed the immutable source tree/lock/eight source hashes, both collector
+digests, executable SHA-256
+`3ec2d5cfb22db74ae44ddacb6548e0b30fbf0fc35359e0fa04bc4c88e83c431b`,
+the complete build/runtime listings and fifteen original one-test commands,
+logs and PASS results. The original four publication interruption hooks are
+distinct from individual fsync hooks and physical power loss. This adds no
+canonical fleet PASS.
+
+`checkpoint.py` locates the actual Admin erasure-set checkpoint selected by
+an empty root request with explicit pool/set selectors. Ordinary bucket and
+cluster scans cannot substitute for that record. Exact task/disk paths must
+contain one schema-8 checkpoint and one schema-7 Admin resume state on the
+same physical owner, plus one schema-3 root admission on its exact owner, with
+unchanged scope/options, valid candidate digest, owned bucket incarnation and
+truthful durable progress. Read-only live observation brackets two identical
+bounded record reads with the original service birth, candidate, command and
+environment. The bounded profile exposes root records only for read-only census
+and observation; root mutation and MRF mutation remain rejected. The
+freeze/crash boundary requires pidfd-bound SIGSTOP/SIGKILL and unchanged
+stopped record bytes before any fault; no platform power event is claimed.
+Duplicate JSON fields, non-finite values and duplicate dedup identities reject
+injection. Root admission, live replacement/process drift, missing/duplicate
+owners, foreign scope, typed options and fabricated progress are covered by
+negative tests. These are F07/C11 prerequisites; the remaining 24 complete
+executors are still outstanding.
+
+The Functional Chain consumer downloads the manager artifact from the exact
+successful tools run attempt before fleet mutation. `manager_fixture.py`
+independently rechecks all fifteen original results, then exposes only the
+ten F07, three C11 or two J02 prerequisite tests assigned to the selected case.
+A failed/partial attempt, foreign workflow, wrong candidate, changed result
+or unrelated case rejects the artifact. The same consumer independently
+verified the retained `38108127382` artifact for all three scopes. It exports
+no canonical fleet execution, individual fsync-hook, emitted barrier-log or
+physical power-loss claim. These required evidence inputs do not make the
+three incomplete canonical executors dispatchable.
+
+F07 stale-generation injection now consumes an immutable earlier checkpoint
+captured from the actual candidate process, rather than synthesizing a record.
+The capture brackets two stable exact-path reads with the original service
+birth and retains the original bytes and run/candidate/disk/device/mount/task
+binding in separate quarantine files. A later generation must prove strictly
+greater durable progress in the same incarnation, set and JSON-typed options,
+with truthful monotonic disposition counters and an original runnable cursor.
+The bounded orchestration waits for this actual advance before freezing the
+owner. Capture does not mutate product metadata and does not claim a live CAS
+barrier; the independently collected native stale-manager test retains that
+separate responsibility.
+
+Fault backups preserve the newer pre-fault generation. Rescue reinstalls that
+newer record, retains the injected older bytes and original capture, and
+refuses to overwrite any advanced or deleted product responsibility. Fleet
+cleanup now restores registered journal faults before namespace deletion;
+failed restoration marks cleanup FAIL and retains every private namespace.
+Eleven additional regressions exercise real-file capture/advance/inject/rescue,
+immutable evidence, strict JSON identities, process drift, missing or foreign
+capture proof, counter/cursor drift and retained failed cleanup. The complete
+local contract suite executes 498 tests with eight macOS platform skips. These
+are execution prerequisites: F07's six branches on both H1 and H2, including
+bounded write failure and full retained-responsibility diagnostics/rescan,
+remain required. Automatic coverage remains 134/158 with 24 Heal executors
+outstanding; no canonical fleet PASS is added by these changes.
+
+F07 also has a dedicated `journal-bounded` runtime using the existing
+fixed 128-MiB-per-drive filesystem backend. Only fixed block exhaustion is
+accepted for this profile; caller-supplied limits, arbitrary paths, inode
+faults, root admissions and MRF payload mutations are rejected. The candidate
+receives verified private service bindings on the same four original physical
+drives, with scanner/MRF/automatic repair disabled. Exact checkpoint reads use
+those actual bounded views, and capacity witnesses retain the original process
+birth, namespace, UID, device and precise ENOSPC result. Filler is released
+before registered journal rescue; failed release retains all namespaces.
+Six new boundary regressions pass. The complete local suite now executes
+498 tests with eight macOS skips. The current Linux contract run
+`38111929721` passes its catalog/matrix/shell/scanner gates and the Heal
+contract step executes 479 tests. The prior Linux run `38111242118`
+independently confirms 490 tests without skips at the preceding helper; no
+result is relabeled to the current helper. No bounded-journal live execution or
+complete F07 executor is claimed yet.
+
+Current reconciliation: helper `6f91210f224dc0a9e460b8240c4a4cf73d7e64c1` is pushed to auto-testing PR #113 and contains the A09/G01/G02/G03/G04/G05/G06 race executors. The current local contract suite runs 518 tests with eight macOS skips; the preceding Linux contract run `38109944413` ran 473 tests without skips. The independent canonical census remains the source of live PASS totals; native, read-only, recovery, duplicate and incomplete runs are excluded, and the new G04/G05 executors add no canonical live PASS claim while their native marker/MPU fences are BLOCKED. A09 and G01-G06 preserve their source branch IDs, ACK-bound foreground operations, H1/H2 physical evidence, peer convergence and run-owned cleanup; native commit/version/delete/marker/MPU/copy fences remain in the denominator until a reviewed candidate barrier exists. The matrix has 141 registered dispatch IDs (74 non-Heal and 67 Heal), with 17 Heal cases still lacking automatic dispatch. Registration includes executors whose required native branches remain BLOCKED; it does not certify complete canonical migration or live PASS.
+
+
+The environment-gated batch now registers F08/F09, G07/G08, H01-H05/H07, I01/I06 and J05. The default gate writes a per-case BLOCKED reason and environment evidence before any SSH/install/fault operation. Explicit opt-in still requires exact candidate/client/runtime provenance, every required topology and a digest-bound resource adapter. The enabled control flows reuse auto-testing Probe/Recorder and physical fault oracles, retain every source branch and actual 24h/72h soak duration, stop on dirty cleanup, and require source-assertion review instead of manufacturing PASS from adapter responses. No live run or current-fleet adapter deployment is claimed. Second-batch blockers are recorded accurately: C11/F07/J02 already have 3/10/2 candidate native prerequisites but still need full live orchestration/evidence; I05 lacks an exact reviewed lock-owner/abort fence. See `docs/validation-heal-environment-gates.md`.
+
+This batch passes all 36 test modules selected by Validation Catalog CI: 537 local tests with eight macOS platform skips, plus exact source-catalog hash comparison, the 84/301/532 matrix, shell syntax and scanner mock gates. The 19 new environment tests cover disabled dispatch across all 13 IDs without SSH, explicit runtime provenance, topology and counter negatives, complete enabled IO control flow, actual overlap, full-duration soak control, and recovery after a lost setup ACK. These are local contract checks; no live environment-gated result is claimed.
+
+
+The 2026-10-12 reconciliation rechecks both retained Pool tail results, every referenced original file digest, and the current canonical report gate. The derived census now contains 88 unique PASS cases; it preserves the preceding 86-case census and excludes cleanup/native/read-only runs. Of the 141 automatic executors, 53 still lack complete canonical PASS: 31 have retained live failures, seven (A09/G01-G06) require reviewed native barriers, and 15 have no complete independently audited live evidence. The 13 environment gates remain disabled and C11/F07/I05/J02 remain outside dispatch. Fresh F05/J06 verification must use a new immutable harness pin containing the durable bucket-event validator and the 120-minute performance lease, followed by the 15 evidence-gap cases on the same serialized current auto-testing fleet. Old failures and recoveries are retained separately.
+
+Fresh read-only inspections `38193642346` and `38194452552` confirm the same four current Heal nodes and 16 independent eligible endpoints. F05 run `38193742108` pins helper `0334abfefeba0d7ea85855da1fe9aa8a287d2940` and tools attempt `38103474555/1`; it retained FAIL/cleanup PASS. Inspection of the final pending-MPU consumer found a separate harness defect: `Probe.s3` streams and hashes GET bodies into the top-level `payload`, but F05 read `response.payload`. The consumer now verifies the actual streamed receipt on every original peer. A regression uses real Probe streaming receipts, proves the original nested-payload check rejects healthy bytes, and rejects corrupted bytes from the fourth peer. All 22 F05/J06 focused tests pass. The original local queue was stopped at its clean paused case boundary; its immutable plan and original FAIL remain intact.
+
+The replacement 17-case queue pins helper `ccef050df3b28416c51356951e78cb46dd74d10b`. F05 retry `38194557163` also retained FAIL/cleanup PASS: the original root token returned `stopped`, `aborted/untraversable`, partial coverage and zero processed objects. Its detail names three bucket failures and `Storage error: erasure write quorum`. The fleet starts peers concurrently and waits for readiness, but the recovered task runs during startup. Candidate `heal_bucket` wraps its typed storage error into `TaskExecutionFailed`; the message fallback does not classify this write-quorum string as recoverable, and cluster traversal records a permanent untraversable failure. This is a candidate recovery-path diagnosis, not a failure of the later MPU consumer and not a reason to issue a replacement task or accept an aborted result. J06 retry `38195131282` then started on the same immutable queue; its result remains pending.
+
+Original A10 `38120735805` and C10 `38120042167` negative receipts exposed another harness mismatch. The candidate returns `stopped/completed_with_errors` and one exact permanent not-found receipt for a nonrecursive absent prefix or protected metadata-less orphan. The old convergence helper required `finished`; A10 additionally demanded an empty receipt list and C10 demanded `finished` even for a failed traversal. Convergence now compares the original path/token, settings and complete matching terminal outcomes, including truthful failed terminals. The negative oracle requires exactly one original failed object identity, unchanged selectors, an unversioned/unbound absent identity, complete coverage, exact 1/0/0/0/1/0 counters, one visible failed attempt, zero overflow and the exact not-found detail. Normal repair branches still require successful execution and their full ACK manifests. The 199 focused tests pass (one macOS skip); read-only replay of both original C10 refusals passes the corrected oracle without changing either retained FAIL or adding canonical PASS. Fresh full A10/C10 runs are still required.
+
+B07/C07 negative task oracles now require `stopped` for their already-required failed or aborted canonical execution, matching the candidate's `legacy_execution_status` projection. They still reject successful, unknown, incomplete or wrongly attributed responsibility; the four original C07 task receipts from `38119830051` remain rejected because they are completed deferred existence checks with no incarnation, rather than exact failed/aborted conflict receipts. The 254 focused contract tests pass with two macOS platform skips, and the source matrix remains 84/301/532. These oracle repairs add no canonical PASS and require new immutable live runs.
+
+J06 retry `38195131282` completed all twelve fixed ABBA phases with 1,440 scheduled requests and zero request failures, then retained FAIL/cleanup PASS at the original maximum paired latency ratio of 3. The second group's PUT p99 means were 67.45 ms without Heal and 353.32 ms with Heal (5.24 times); the third group's were 84.04 ms and 292.13 ms (3.48 times). The other ten paired comparisons met the gate. No acceptance threshold changed and successful physical repair or completed phases cannot replace the failed performance gate. The same serialized queue then started A03 `38198122041` at helper `ccef050df3b28416c51356951e78cb46dd74d10b`.
+
+A03 run `38198122041` passes every required H2 cross-node branch at helper `ccef050df3b28416c51356951e78cb46dd74d10b` and driver `03fb3d78a0ffc4213583dd3bcb44c7ac38f42ead`. Independent verification of the original artifact members, canonical result gate, complete branch outcomes and immutable candidate provenance increases the unique canonical census to 89. The remaining serialized backlog continues on the same candidate; no prior failure is overwritten.
+
+J02 now has a non-dispatched orchestration prerequisite in `outcome.py`. It reuses the existing Fleet, Probe, Recorder and physical fault backends for H1/H2 and DB/DV: one mixed healthy/repaired/below-R task, a separately admitted dry-run task, and an exact metadata-dependency refusal. Complete ACK identities, the actual R-1 geometry, original incarnation, typed dispositions and exact canonical counters are reconciled before fixture rescue; the original token/outcome is checked again after rescue. The reviewed candidate's four failed bucket-object attempts remain visible, and legacy `objectsHealed` is never used as proof of repair. Unknown native evidence remains a prerequisite. Its new live executor first verifies an ACK-bound protected control and original incarnation, switches only the owned candidate write mode through a durable one-way transaction, writes real SDK legacy objects, and requires every physical replica to show absent integrity descriptors. The original Deep task must report exact Unknown identities/counters and zero repairs for those objects, preserve the protected control and all original physical bytes, and remain unchanged after subsequent GETs. A fresh candidate-bound inspector build and complete live evidence remain required; this implementation is not a canonical PASS. These control flows have local negative-contract coverage but no live execution claim; J02 remains outside dispatch and the original corrupt-metadata Unknown probe, version-specific grace Deferred source procedures remain outstanding. The source readonly failure/new-token compensation branch is now implemented and requires live verification. Unknown is counted within skipped, not as repair; even if every current diagnostic branch passes, this executor emits complete=false and canonicalCasePassClaimed=false until those source procedures are covered. The 84 cases, 301 branches and 532 topology templates are unchanged.
+
+
+The corrected live census now contains 99 unique complete canonical PASS cases: 85 on candidate source `072b57b37559fbe626aeda8abf61617fdcbdd926`, 12 on the older immutable source and two on source `16cf19817e0c81e24d0d787a916f734a34517ec5`. C04 `heal.truncated-and-zeroed-shard` run `38203454300` and `heal.completed-multipart-object` run `38204339623` were independently audited with clean finalization. The derived ledger records 35 retained live failures, 7 required native barriers, 4 orchestration/evidence blockers, and 13 environment scenarios disabled by default.
+
+D07 `heal.range-head-and-conditions` run `38205235080`, D09 `heal.object-lock-retention` run `38205854024`, D10 `heal.special-keys-and-pagination` run `38207381816`, and `heal.manual-normal-attribution` run `38207956101` passed every required source branch and were independently audited against the original canonical results, referenced member digests and immutable provenance. Both finalizations passed. A distinct validation-channel package was built successfully in run `38204362745/1` from source `16cf19817e0c81e24d0d787a916f734a34517ec5`, with package SHA-256 `531965f2c009a5a6ef1c02cc6aa1ba82a82b6fbe4aa441d89613afc8a3f91d40`. It contains the typed bucket-heal error and SSE-C response fixes. The D08 and manager native collectors explicitly bind this reviewed source and its actual source tree; the original candidate remains separately supported. Evidence for one candidate cannot certify the other. No new-candidate canonical PASS is claimed before complete fresh live execution.
+
+Manual-deep attribution run `38208251180` passed every required source branch and independent canonical audit with clean finalization. Hosted tools run `38207334642/1` also completed: the new candidate inspector and all fifteen exact manager regression results passed local independent artifact verification. Source tree `7edb1a2d02d60323cdfab4173485c33cb06b297a`, candidate lock, collector source digests, original logs and exact one-test invocations were checked. These native results are scoped as ten F07, three C11 and two J02 prerequisites and add no canonical PASS.
+
+F07 now has a non-dispatched partial orchestrator in `persistence.py` for actual checkpoint truncation and future schema on H1/H2 with DB/DV fixtures. It admits an explicit Admin erasure-set task, observes real durable progress, freezes and crashes the exact process, validates stopped root/checkpoint ownership, and mutates only that original checkpoint. Restart must preserve corrupt bytes and resume ownership, emit the blocked fence, and retain a durable original failure terminal/report. A distinct rescan is checked against complete ACK/physical/S3 manifests without reclassifying the original failed token. Original corrupt record and failure-report bytes must remain unchanged through rescan and business reads; cleanup restores only unchanged injected journal bytes. The MRF checksum, earlier-generation replay, bounded journal-write failure and exact persist-fence branches remain explicitly BLOCKED, and F07 stays outside automatic dispatch. No F07 live result is claimed. Nine new contract tests reject changed records, missing or substituted responsibility, silent success and invented counters; 61 focused persistence/native prerequisite tests pass, and the 84/301/532 matrix remains unchanged.
+
+D04 run `38204764944` has a digest-bound diagnostic comparing the stopped stale-authority scene with the pre-manual-admission witness: only the target member xl.meta changes on each of H1 and H2, adding the exact newly acknowledged marker. Every other file and disk format remains unchanged. The candidate keeps durable MRF replay active when best-effort ingress is disabled; exact source-task attribution still needs investigation. The manual attribution gate and original FAIL remain intact. B09 run `38202851374` likewise has a complete identity-difference diagnostic: no expected ACK identity is missing; vm003 retains deleted `objects/0006.bin` null data in DB and historical `objects/0001.bin` version `de87efd5-e1ef-42a2-a86a-a1e7a18fe46c` in DV on all four disks. External resurrection is not established.
+
+The seventeen-case corrected live queue completed with eight PASS and nine retained FAIL results; every finalization passed. J01 run `38209400619` did not witness the full raw/rc uninitialized startup window on H1, and H2 rejected rc text that erased status uncertainty during convergence. Both disabled branches passed. E03 run `38208556719` retained range-repair physical differences and missing fresh ReadRepair attribution in H2. These partial results do not change either canonical verdict.
+
+C10 `heal.dangling-and-orphan-safety` run `38209676009` subsequently passed complete canonical audit at helper `dbe74ec9eb3288e32923d8b8777c581c904ba78f`; cleanup passed. A10 run `38210630408` retained FAIL with clean finalization after its corrected negative prefix oracle passed: other branches still reported incomplete version/marker diagnostics or unknown/incomplete integrity. The followup completed and discharged its fleet guard. The separate F05/D08 validation-package plan on driver `16cf19817e0c81e24d0d787a916f734a34517ec5` also completed and discharged its guard. F05 `38227684435` and D08 `38229589497` passed independent complete canonical audit and clean finalization after read-only inspection `38227616934`. These are the first two PASS cases for this distinct candidate. D08 covered SSE-C, SSE-KMS, SSE-S3 and encrypted historical versions on H1/H2, including exact ciphertext/physical references, plaintext reads, classification headers and key-refusal checks. The downloaded validation DEB matches its full package SHA and contains RustFS binary SHA-256 `d0d0fce93845728ef00b3b9406210b5bbf728c9262c329e2522319b40bc61917`; rc is an independently bound runner binary and is not included in that DEB.
+
+E03's physical diagnostic for run `38208556719` proves that the Range bitrot target retained the exact injected damaged bytes on H1/H2. Its ReadRepair task used Normal scan mode and returned typed `metadata_healthy` with healed=0/unchanged=1; legacy progress counters cannot certify payload repair. The H2 missing/historical branches recovered bytes but lacked fresh admission evidence in the old bounded journal tail. The executor now collects the complete journal cursor window with pagination, preserving exact source/task attribution. This harness change passed 176 focused Heal/recovery checks and all six CI lanes at `7c3b114c0e611870b0844987b69e8dca059689ca`; E03 still requires a new complete live run on the product repair candidate. Its original FAIL remains retained.
+
+C11 now has partial H1/H2 DV orchestration in `incarnation.py` for `old-metadata-rejoin`. It verifies an original incarnation and complete DV ACKs, denies one actual member's reads/writes, deletes every data version and marker through S3, confirms successful bucket deletion and absence, and creates a same-name successor with distinct bytes/version IDs and explicit bucket configuration. Rejoin retains the original processes; successor responsibility must bind a different incarnation, preserve surviving new shards and contain no old version/marker references. Physical system records, full shard checksums, every peer's complete S3 inventory and config, old-version refusal and an independent control bucket remain separate evidence. Unknown S3 mutation state cannot become clean finalization. Eight new local tests cover refusal and partial-case gates; 52 focused tests, AST/YAML, actionlint, whitespace and the unchanged 84/301/532 matrix passed. This implementation has no live result. Old checkpoint/MRF replay and the exact storage commit barrier remain BLOCKED, and C11 stays outside dispatch.
+
+E03 followup `38235099954` uses package `38231599023/1` from exact source `e224eb48e5311cff022b04fd78c97a8572d5c270`. Five of six H1/H2 DB/DV branch instances passed, including both Range bitrot and historical-version instances with restored physical bytes and Deep typed repair receipts. H2 ordinary GET remains FAIL because no fresh source admission was observed; its original journal contains `Suppressed 13097 messages` for the exact test unit. The read-repair runtime now enables DEBUG only for `rustfs_ecstore` and `rustfs::heal`, keeping the default at INFO and other repair sources disabled. This prevents unrelated HTTP/HTTP2 debug traffic from consuming the journal budget. The 176 focused Heal/recovery checks passed. A complete fresh E03 run is required; the original FAIL and the 99-case canonical census remain unchanged.
+
+The complete E03 followup `38237270640` passed all six required H1/H2 DB/DV instances with clean finalization. Independent audit rechecked the original canonical result, every referenced member digest and exact package/binary/helper provenance. It uses the same source `e224eb48e5311cff022b04fd78c97a8572d5c270` and package `38231599023/1`, with helper `8ea38a9d8044e512de91ec788d79ecb7aa2f0fd8`; the preceding suppression failure remains retained. C08 `38235978152` also passed independent complete canonical audit. The derived ledger now records 101 complete PASS, 33 retained live failures, seven required native barriers, four orchestration/evidence blockers, and 13 disabled environment scenarios. B07 `38236702473` remains FAIL with clean finalization: H1's below-quorum S3 response and H2's negative terminal responsibility violate the frozen oracles.
+
+C11's native collector now binds reviewed source `1aa9be87d18cf941e5020f19e8622c26ba4b10ac`, its exact tree, Cargo.lock, six storage sources, helper digest and an optimized independent test executable. The nine-test suite preserves all six original native regressions and adds three real storage incarnation tests. The final-rename test pauses actual shard reconstruction before publication, aborts its caller while the lifecycle writer remains pending, drains and physically verifies the exact repaired version, and rejects obsolete admission after normal delete/recreate. Four original ordered milestones are mandatory; omitted, duplicated, reordered or invented rows cannot certify the barrier. Execution uses captured successful output and the candidate's bounded larger ecstore test stack. Local nextest passed 28 focused tests; the direct libtest check with captured output also passed. These local results are not hosted optimized native evidence. C11 independently verifies any hosted storage artifact before fleet mutation and records its scope as a native prerequisite with no fleet topology claim. Old checkpoint/MRF replay is still blocked, the metadata branch has no live result, and C11 remains outside dispatch.
+
+
+C11 original-record replay implementation (2026-10-12)
+
+C11 now implements both remaining H1/H2 DV replay flows through the existing Fleet, Probe, Recorder, checkpoint and read-only MRF observers. Stopped descriptor-relative transactions bind run, candidate binary, old incarnation and original task, durably archive all original bytes before detachment, and restore only those bytes. The MRF allowlist also includes both source-defined legacy fallback journals; whole-record CRC and exact original intent bindings prevent omitted or foreign replay sources. Checkpoint terminals and new product responsibility cannot be overwritten. Empty transition fallback files carry no responsibility and are recorded before removal. Phase changes verify archive and transaction identities and fail closed on interrupted configuration commits. Incomplete capture/install or unresolved S3 mutation keeps every private namespace and original archive.
+
+The old MRF fault gate is released while stopped after all original replay sources are detached and before normal S3 bucket deletion/recreation. Successor and control objects are written on a healthy fleet, independently verified, and frozen before original records are installed. Replayed checkpoints must reject the old generation while retaining original progress and durable terminal/report ownership. Replayed MRF must retain the exact responsibility ID, intent digest and source incarnation under a new process checkpoint owner with greater sequence, durably held as bucket_incarnation_changed without operator acceptance. Complete successor/control physical bytes, formats, system records, full shard integrity and every peer's S3/configuration remain independent evidence.
+
+Immutable package run 38240858236/1 and tool run 38244986993/1 succeeded for source 1aa9be87d18cf941e5020f19e8622c26ba4b10ac. Independent Git-object checks verified the exact source tree, lock and all source contracts; all seven MRF sources match the reviewed reference byte-for-byte. The complete 68,625,642-byte DEB digest is b99dd915376b51f0896fc346ca4642b7d26bf3352135bb5c6f60f8179d865761 and its contained binary digest is 0103ca46386396ed15f27b9efbe4196fe62bd9e93033671a88f3b3e944602cf4. Inspector, nine native storage regressions and fifteen manager regressions passed independent artifact consumers for harness d5a41739a7a544d93ba71f51583d45ad4e6161eb. A new immutable tool build is required for the updated replay harness; older harness provenance is not accepted. The BLOCKED writer preserves unverified migration status, original provenance and no PASS assertions. A phase identity in the process environment detects interrupted checkpoint transitions even when product configuration knobs are identical. Independent stopped censuses are retained before capture, after detachment and after installation to compare each original byte and absence witness. Read-only environment run 38251226817 confirms 16 distinct eligible endpoints on the four current Heal hosts for H1/H2; H4 remains unavailable.
+
+The final full Heal contract suite passed 593 tests with eight platform skips; all 30 focused incarnation tests, the unchanged 84/301/532 matrix, AST, bash syntax, actionlint and whitespace passed on the final code. Two sequential final reviews covered correctness, simplicity, test coverage, path/artifact security and persistence/cancellation. Explicit diagnostic execution cannot claim canonical PASS; clean finalization does not hide a failed branch. C11 remains outside automatic dispatch and complete canonical PASS remains 101/158 pending live execution and independent audit.
+
+F07 now reuses its independently verified ten-test candidate-bound native scope as PREREQUISITE_PASS without a fleet topology or DB/DV claim. The remaining three live flows and full review of the required fsync/commit cuts remain blocked; existing publication interruptions are not relabelled as individual fsync or power-loss proof. The exact 1aa9be87 candidate is accepted only after all six checkpoint route/source contracts were independently proven byte-identical to the reviewed reference. Thirteen focused persistence/artifact tests passed. Canonical PASS remains 101/158.
+
+F07 committed MRF corruption now has a non-dispatched H1/H2 DB/DV executor in persistence_mrf.py. It creates real member permission denial and ACK-bound original durable obligations, freezes complete committed payload/manifest/lifecycle bytes, corrupts only each actual payload CRC through the existing stopped journal adapter, and requires a source-matched corruption diagnostic on every mutated process. All unknown bytes and original commitment/lifecycle responsibility must survive startup, independent Admin rescan, complete shard checks and all-peer business verification. Rescan repair never retires or relabels the original corrupted MRF responsibility. Lost injection acknowledgements register recovery before mutation and retain dirty namespaces. Sixty-three focused contracts and the final 599-test Heal suite passed with eight platform skips; AST, actionlint, whitespace and 84/301/532 passed. Two sequential reviews found no unresolved correctness, simplicity, security or persistence/cancellation issues after the focused failure probes. This flow has not run live; F07 still lacks stale-generation, journal-write-failure and complete native cut review. C11 live execution uses the separately frozen 891f264f39ffb608f1898fef151b273e205321b5 harness and its own rebuilt tools. Canonical PASS remains 101/158.
+
+C11 first live diagnostic and retained-scene recovery (2026-10-12)
+
+Tools 38253155969/1 passed inspector, nine storage regressions and fifteen manager regressions; exact source/tree/lock, collector bytes, binary digests and original results were independently verified against harness 891f264f39ffb608f1898fef151b273e205321b5. Diagnostic 38257435494/1 stopped in H1 old-metadata-rejoin: all eleven original versions/markers had explicit deletion ACKs, but DeleteBucket returned 500/InternalError with File access denied while one member was isolated. Its FAIL and retained namespace are preserved; no later branch ran and canonical PASS stays 101/158. Recovery accepts only this exact known refusal, a complete digest-bound request/ACK ledger, a restored member gate and the unchanged stopped candidate scene. It exports the retained system records and rechecks the physical manifest immediately before finalization. Unknown calls, missing ACKs, changed provenance, other bound namespaces, replay transactions and scene drift are rejected. Recovery never converts the product failure into PASS.
+
+C11 recovery 38259016213/1 completed with clean finalization. Independent audit compared both stopped physical censuses with the original failed artifact, checked the candidate binary, restored gate, retained system-record integrity and credential boundary, and verified the cleanup receipt. The driver's new merge commit preserves the Functional Chain and validation-tool workflow bytes exactly. The original DeleteBucket FAIL remains unchanged.
+
+F07 original stale-generation replay now captures an actual earlier product checkpoint before repair, proves later durable progress in the same task/incarnation/physical owner, crashes the exact owner and installs only the saved earlier bytes through the existing journal adapter. An independently stopped census verifies the injected checkpoint and unchanged resume companion. A valid record may replay safely; the oracle requires complete ACK accounting, original durable terminal/report, full physical repair and shard integrity before business reads, an independent unchanged rescan and every peer's original task/S3 view. Explicit rejection instead follows the retained-record failure/rescan path. Neither path claims the stale in-memory manager CAS or individual fsync barriers. Unknown fixture/admission acknowledgements retain the namespace. Cleanup never resurrects a completed checkpoint; original-byte evidence and complete repair proof precede dropping rescue registrations. F07 has an explicit diagnostic route, while its canonical matrix remains evidence-blocked because journal write-failure and complete native cut coverage are incomplete. A clean recovery cannot hide an implemented branch's FAIL.
+
+The final stale-generation change passed 608 Heal contracts with eight platform skips, 91 focused persistence/checkpoint/journal/manager/environment/migration checks, the unchanged 84/301/532 matrix, Python AST, shell syntax and whitespace. Two sequential final reviews retained exact bytes, task and owner bindings, rejected unknown calls and delayed rescue removal until complete evidence export. No unresolved finding remains for this executor diff; no live F07 PASS is claimed.
+
+F07 original checkpoint write failure now has a diagnostic executor using the existing four-node `journal-bounded` profile. It fills only the four fixed private journal volumes while the candidate is stopped, then proves a candidate UID and mount namespace witness for the real `ENOSPC` write. The original checkpoint, resume bytes, root owner, exact failed task outcome and source-matched `Heal task failed` event remain durable. The executor pauses and records every owner before releasing only its private filler, performs a distinct Admin rescan with a new token, verifies complete physical, shard and peer S3 evidence, and preserves the first failed attempt separately from any later same-task retry. Cleanup keeps rescue state when a fill ACK, release, evidence export or reconciliation step is incomplete. The branch is implemented but remains outside canonical dispatch until a live run and the remaining reviewed native persistence cuts complete. The final full Heal suite passes 615 tests with eight platform skips; 21 focused persistence contracts, the catalog, 84/301/532 matrix, Python AST, shell syntax and whitespace pass. Two fresh sequential reviews cover correctness, simplicity, test coverage, path security and durability. Frozen process-bound censuses and decreasing root timeout budgets retain their original identity checks. Canonical PASS remains 101/158.
+
+F07 diagnostic 38261190390/1 retained FAIL with cleanup FAIL. Checkpoint observation rejected the candidate's actual ordered JSON digest; the MRF windows failed on H1, and H2's first CRC injection was rejected because its actual committed payload also contains system metadata responsibility. No live branch PASS is claimed. The rejected injection left all original MRF bytes and object files unchanged and created no intent or backup. Recovery now accepts only that exact first H2 DB refusal, seven continuous digest-bound create/PUT ACKs, unchanged original MRF and physical censuses, and a cleared member gate. Every node is audited before finalization; each node rechecks its stopped candidate, empty private quarantine, exact MRF digest and full physical scene immediately before removal. Unknown calls, missing ACKs, changed scene, any injection evidence or backup retain the namespace. The original FAIL is never rewritten.
+
+F07 recovery 38266595027/1 passed independent audit: all four stopped physical scenes and original MRF bytes matched, every node passed its no-injection audit before finalization, and every unit/root cleanup receipt passed. The original failed result is byte-identical and remains FAIL. The checkpoint observer now verifies both source-defined serde_json map representations, including the full server's unified preserve_order feature, while sorting the source-defined ledger arrays before SHA-256. A pinned unmodified candidate fixture rejects counter, cursor, incarnation and digest tampering. The MRF adapter now accepts the exact known internal metadata companions alongside active original business work, rejects foreign business or IAM responsibility, and corrupts one latest committed replica per process with every untouched replica retained as a control. Stopped original bindings replace earlier live sequence observations. The final 621-test Heal suite passes with eight platform skips; 55 focused persistence/MRF contracts, AST, catalog, 84/301/532 matrix and whitespace pass. Two sequential final reviews found no unresolved issue in these ownership, digest, injection and retention changes. H1's unobserved durable MRF window and all original live failures remain retained pending a fresh run; canonical PASS remains 101/158.
+
+The F07 future-schema fault now recomputes the embedded integrity digest after changing schema 8 to 9, using the exact map representation verified in the original candidate checkpoint and the source-defined sorted ledger arrays. Original task, incarnation, options, cursor, counters and ledger order remain unchanged; an invalid original cannot be re-signed. Both the unmodified full-server fixture and sorted-map fixture independently verify the future record's digest. Diagnostic 38268715777/1 pins the preceding harness 6db679f and cannot validate this correction. Its future-schema result must not be credited as valid unknown-schema coverage even if its retained-record checks pass. Canonical PASS remains 101/158.
+
+The fixed `1aa9` candidate already contains six deterministic directory-fsync failure regressions in `disk::local::test`, and the verified original ecstore binary inventory includes all six exact names. `persistence_native.py` now executes them as an additive F07 prerequisite using that verified full candidate binary and its original nine-test suite evidence. The collector retains original one-test logs, exact commands, candidate/tree/lock/binary/run/attempt/helper identity, its own digest and strict scope limitations; source, command, artifact, inventory and log substitutions are refused. The hosted tools lane archives the supplemental evidence separately. These tests cover actual post-rename or post-delete directory-fsync errors, manifest `StorageFull`, recovery-anchor preservation and the relaxed-mode system metadata durability rule. They do not cover individual file-fsync interruptions, physical power loss, all checkpoint/commit cuts or canonical fleet branches. Hosted execution and independent original artifact audit remain pending; `requiredNativeCoverageComplete` stays false.
+
+Mixed MRF payload faults select only an owned business record's CRC byte. Record boundaries are independently matched to each decoded original intent digest; internal metadata companions remain byte-identical even when they appear at the end of the payload. UTF-8 keys and variable-length marker purge records have explicit framing coverage. Retention verification recomputes the same owned target from the frozen bucket binding. The final 621 Heal tests pass with eight platform skips after this scope restriction; both final reviews retain exact original bytes, incarnation and recovery boundaries.
+
+
+F07 diagnostic `38268715777/1` completed with FAIL and clean finalization. Independent audit verified all twenty live branch instances, fifty node cleanup receipts and 240 embedded original-byte witnesses; no diagnostic branch or canonical PASS is credited. The checkpoint task can finish in roughly three seconds: an extra live root census missed its actual checkpoint window. The observer now freezes the exact checkpoint process before reading and validating the runnable root, and rechecks both original records before crash/mutation. Live read/capture races retain a structured `NOT_OBSERVED` refusal across SSH within the existing deadline; stopped operations, changed process identity and path/ownership failures remain fatal. MRF corruption restart now re-arms its same member permission gate while stopped and reactivates it after readiness, avoiding disk initialization with an already active chmod denial. Original corruption, physical scene and responsibility checks still gate all later steps. Seventy-four focused contracts and 631 Heal contracts pass with eight platform skips; two fresh sequential reviews found no unresolved findings. A new immutable diagnostic run is required; H1's missing committed MRF window remains unverified.
+
+Hosted tools `38272891095/1` passed the original nine exact storage regressions and all six supplemental directory-fsync tests. Independent consumers verified candidate `1aa9`, source tree/lock and source-file Git objects, executable/collector identity, exact one-test commands and original logs. These are scoped native prerequisites; individual file fsync, physical power loss, complete required native cuts and canonical PASS remain unclaimed. Isolated package publication `38271092352/1` also passed: fresh hosted publisher bytes equal the opaque build artifact, the complete public DEB digest matches schema 3, and the existing candidate resolver accepts its source/run/attempt binding. Current fleet candidate `38240858236/1` remains fixed.
+
+B07 fixed-candidate replay `38275626918/1` remains FAIL with clean finalization. Independent audit binds all four H1/H2 DB/DV instances to `1aa9` source, package/binary and helper `e62c3cc`. H1 GET still returns `500/InternalError`: reader setup reports one available shard against two required shards and reduces missing local files to a generic I/O error. H2 DB GET returns `503/SlowDownRead`, but Admin completion records `deferred/dangling_delete_grace`, zero failed objects and completed execution instead of the frozen failed responsibility. H2 DV startup misses readiness with system metadata read quorum unavailable. Original failures remain retained. Canonical PASS stays 101/158.
+
+
+F07 diagnostic `38277312274/1` pins helper `c0d69a3` and the unchanged `1aa9` candidate. It retains FAIL/cleanup FAIL after twelve live branch instances and two native prerequisites; later H2 instances were not executed. Independent audit verified 156 embedded original-byte witnesses. H1 checkpoint truncation and future-schema injection now reach actual stopped original records and durable failed terminals, but report reconciliation still fails; this is not canonical PASS. H2 DB MRF restart advances its committed records and repairs nine missing target files through its untouched companion replica; its exact corruption diagnostic is not observed. Cleanup correctly refuses old-record resurrection. Recovery now audits the complete acknowledged fixture, exact original corruption, current stopped scene and all private backup/intent/applied bytes on all four nodes before finalization, and repeats those checks before removing each dedicated namespace. It exports evidence without restoring advanced product records, preserves the original FAIL and rejects unknown writes or other mutations. The 634 Heal contracts pass with eight platform skips; two fresh sequential final reviews found no unresolved correctness, simplicity, coverage, security, durability or compatibility finding. Live retained-scene recovery remains pending.
+
+B07 read-quorum error propagation now has a real external-part regression in the companion RustFS PR. Reverting the dominant-I/O return reproduces `Io(NotFound)` at R-1; the fix preserves object-bound `InsufficientReadQuorum` in both legacy and streaming setup. Exactly R and healthy reads return every original payload byte. All 123 reader tests pass on Rust 1.99. This is local product regression evidence; fixed-candidate live run `38275626918/1`, H2 deferred responsibility and startup failures remain retained, and canonical PASS stays 101/158.
+
+F07 terminal reconciliation now accepts only the source-defined scheduler transition from an explicit null admission timeout to exactly 300 seconds with zero nanoseconds. The two reviewed candidate commits share the exact scheduler/config sources, and the dedicated fleet verifies `RUSTFS_HEAL_TASK_TIMEOUT_SECS=300` in the actual process environment, including a fresh witness after fault restart. Every other option remains exact; explicit or checkpoint-consumed budgets cannot be refunded, and terminal/report JSON types must agree. The original H1 DB/DV truncation and future-schema records from `38277312274/1` reproduce all four prior reconciliation refusals and pass this derived retained-record check after the fix. Their original FAIL results remain unchanged; complete rescan, physical/S3 reconciliation and new live execution are still required. The 639 Heal contracts pass with eight platform skips. Two fresh sequential final reviews cover the exact normalization, source/runtime boundary, budget preservation and negative regression inputs without unresolved findings. Retained-scene recovery `38281391802/1` remains queued behind the existing shared fleet chain; no later fleet replay is started before clean recovery. Canonical PASS stays 101/158.
+
+The original H1 DB/DV MRF logs show repeated committed-checkpoint publication failures and durable-admission fallback under whole-member DAC denial. The denial also hid system metadata before any readable committed anchor existed; extending the observation deadline cannot manufacture an original durable MRF responsibility. F07 now prepares its partial-write fixture with a permission gate on only the exact run-owned business bucket in the dedicated `mrf-crash` runtime. Actual candidate-namespace probes must show business read/write EACCES and successful system-format read plus exclusive system probe creation/removal under the same UID/GID and removed DAC capabilities, both before ACKs and after restart. Scope/path/owner/link checks and original device/inode/permissions fence activation and recovery; the existing whole-drive gate remains available to its other cases. Real committed ACK-bound responsibility, CRC injection, retained original records, diagnostics and complete physical/S3 reconciliation remain required. The 644 Heal contracts pass with eight platform skips; two fresh sequential final reviews have no unresolved correctness, simplicity, coverage, security, durability or compatibility finding. No new live result or canonical PASS is claimed before clean recovery and serialized replay.
+
+Independent examination of the original F07 H2 DB stopped records in `38277312274/1` establishes eight valid committed replay sources on vm000, across four disks and both sequence 10 and 11 slots. The old single-replica fault corrupts only disk 0 / slot 0; seven sources remain complete with the same six responsibilities. The fixed candidate intentionally selects a healthy committed replica even when another is damaged, explaining the missing inspection-failure diagnostic. F07 now validates every original replay source on every bound responsibility owner before applying any CRC fault, including readable older slots. Foreign or empty responsibilities refuse construction; every apply is registered for rescue before invocation, and a missing or substituted receipt prevents restart. Only owned business-record CRC bytes change, while manifests, lifecycle companions and other records remain exact retention controls. The original run remains FAIL/cleanup FAIL. The orchestration regression fails with the old single-replica loop; 108 focused contracts and the 648-test Heal suite pass with eight platform skips. Complete live execution, all native barriers and canonical PASS remain unclaimed.
+
+The F07 node adapter now validates each exact disk/slot against its frozen committed owner, sequence, payload digest and active bucket incarnation. The all-source plan is validated before the first apply; the selected target still needs its original valid manifest/lifecycle, unchanged before-bytes and stopped candidate. Previously, the adapter decoded the whole cohort on every apply, so the first intended CRC fault prevented the second injection. A real filesystem regression fails at that second call with global revalidation restored, then passes all four disk/slot mutations and byte-exact original rescue after the fix. Global MRF observation remains strict and rejects the same damaged cohort; replica-scoped observations are explicitly marked and cannot certify complete responsibility. Held/accepted/unbound/foreign-incarnation older sources refuse preflight. All 112 focused contracts, 652 Heal tests (eight platform skips), ten migration tests, AST and whitespace checks pass. No new live or canonical PASS is claimed.
+
+Recovery diagnostic `38704914473/1` ran on helper `9c01cb8` after the independent Fault-Tolerance workflow released the shared fleet lock. It refused before the first physical snapshot: vm000 disk 0 no longer contained its owner marker. Its original namespace records show nine absent run roots (disks 0-2 on vm000-vm002), seven surviving roots and all four state directories; every service is stopped. No MRF export, restoration or finalization occurred. This is externally drifted/incomplete evidence, not clean recovery or a case PASS. Namespace inspection now emits bounded regular-file owner fingerprints (raw/canonical digests and UID/GID/mode/inode/device), without marker contents or credentials, so surviving state can be compared with the original reserved receipts. Tables installation previously recursively changed ownership of the entire mount; its chown is now restricted to its own mnmd subtree. That known interference is not asserted to explain the nine missing roots. Original failures, surviving roots and native/canonical gates remain retained pending forensic inspection.
+
+
+The 2026-10-15 source reconciliation restores the omitted J02 `readonly-terminal-failure` branch from frozen source script `524eb5b32c3236497fdd8a9271df0494fa3d9974`. Heal retains 84 canonical cases; its operation inventory is now 302 branches and 534 applicable topology templates. The current-token missing-shard fixture uses an actual private service-namespace read-only gate and proves EROFS before admission. Its original task must retain one permanent, unbound key-level failure and exactly one attempt; release must leave the shard missing. A distinct compensating token must repair the original current ACK version at the protected-control incarnation, with physical reconstruction and bitrot evidence captured before GETs. Full public versions/tags and the original failed token are checked again after compensation. Lost gate acknowledgements retain the gate registration for cleanup. This is local implementation evidence, not a live PASS; original corrupt-metadata Unknown and grace Deferred/new-token source procedures remain incomplete, so all current diagnostics passing still cannot claim full J02 completion.
+
+### I03 fixture completeness follow-up (2026-10-19)
+
+The frozen source is `524eb5b32c3236497fdd8a9271df0494fa3d9974`.
+Its I03 fixture contract includes DB, DV and DL. Run `39232916911/1`
+(helper `4d43a6e96f8b8cfb2e45e1aebc99772d83ae23dc`, candidate
+`1c7fc818dd858247c45b39542abd8291b1e6e31b`) reported five passing
+branches. Independent archive, request, ACK/task/physical/public and cleanup
+checks accept that replay's four-key, two-version scope. It does not complete
+the frozen fixture contract. The accepted migration total remains **104/158**.
+Original failures and original reports remain unchanged.
+
+| Fixture | Implementation and evidence | Remaining acceptance work |
+| --- | --- | --- |
+| DV | Reuses auto-test `seed(DV)` and `suspend_fixture`: four keys with three distinct non-null data versions each; real suspended overwrites, null marker creation/removal ACKs; retained live/null marker identities; current and historical markers. Versioning returns to Enabled before partition. The fault targets a retained null data version. Every selected key version participates in GET, negative Heal and complete recursive recovery. All 15 retained data versions receive full physical shard checks; markers receive exact S3 refusal/header checks. HEAD preserves version identity, length, ETag, Last-Modified and metadata. | This expanded fixture still needs a new live run and independent evidence acceptance. It does not claim a partition while the bucket remains Suspended. |
+| DB | Adds a separate never-versioned run-owned bucket with the default 128 controls. The exact candidate size contract and actual EC:8 process/object geometry calibrate zero/one, 4 KiB, inline, EC block and source-bound 5-MiB MPU minimum minus/exact/plus sizes. All 128 controls require decoded full-set shape witnesses; every distinct size has partition GET, physical checksum/no-part and Range evidence. Original/fault/rejoin/repaired snapshots explicitly include both primary and DB buckets. All peers must preserve complete inventories and stable original HEADs. | Fresh live execution and independent acceptance remain required. The MPU-size controls are single PUT objects; they do not claim completed multipart protocol coverage, which remains part of DL. |
+| DL | `network_load.py` supplies three distinct control versions per endpoint, response-bound MPU fixtures and four independent worker journals for its concurrent 28-call matrix. `network_load_session.py` registers seed/workers before SDK invocation, reconciles original versions and pending MPU effects after rejoin, and withholds cleanup until a stopped physical MPU census proves no files remain. Original request and ACK byte hashes remain frozen. | The complete adapter now invokes the session, physical version recovery, MPU closure and unresolved-scene isolation for the fixed `1c7fc818dd858247c45b39542abd8291b1e6e31b` candidate. Fresh same-candidate live execution and independent full-fixture acceptance remain required. Modeled matrix/reconciliation tests are not DL live or canonical acceptance. |
+
+The expanded DV fixture stays within the existing 128-identity/512-MiB
+inventory limits. Network expiry remains 1,200 seconds; partition task deadlines,
+request retry policy, quorum error mapping and complete-version receipt checks
+are preserved. Source fixture completeness must be checked separately from a
+raw five-branch PASS before accepting I03 as a migrated canonical PASS.
+
+
+I03 DB implementation detail (2026-10-19): the existing `boundary_sizes`
+helper accepts the explicit EC:8 calibration only when every live process
+confirms the exact storage class and the decoded object has eight data and
+eight parity members. Default callers keep their original environment guard.
+The reviewed multipart minimum is bound to the fixed candidate's full
+`set_disk/mod.rs` digest. Current candidate/prior physical calibration yields
+15 distinct sizes and 200,127,457 payload bytes for 128 controls, within the
+existing 512-MiB budget. This read-only preparation is not fresh DB live evidence.
+The existing node-side snapshot bucket guard continues to require the primary
+bucket and allows only its owned suffixes; no broader host path is introduced.
+
+
+I03 DL request layer (2026-10-19): `LoadJournal` reuses the auto-test
+`Recorder` and serializes one worker's calls; readers and writers never share
+mutable sequence state. Two endpoint pairs execute eight mutations and six
+reads each, within a frozen 180-second admission budget and the existing SDK
+request limits. The controller alone writes Fleet evidence. Every prepared
+request stays bound to its original ACK SHA-256; missing ACKs and unknown
+results remain unresolved, and a failed worker admits no subsequent calls.
+
+The fixed candidate maps write-quorum/lock errors to HTTP 503
+`ServiceUnavailable`; that refusal does not prove rollback, especially for
+MPU Abort. Partition LIST responses are bounded observations, not global
+completeness. A finite HTTP-status tuple (at most three explicit codes) extends
+the code-level Recorder API for these LIST observations; existing integer and
+default callers retain their checks, and CLI traces still require one integer.
+Operation-specific code, payload, version and selector validators remain
+mandatory. HEAD properties use the same datetime representation as the durable
+ACK JSON. Only actual invoked SDK intervals in one clock domain can prove
+client overlap; collector setup windows cannot establish it, and no server
+commit fence is claimed.
+
+`LoadSession` now durably registers the seed Recorder and all four worker
+journals before invocation. Seed failures retain their original unknown
+request even when no complete fixture returns. Recovery first verifies all
+original business versions on every peer, binds discovered pending upload IDs
+only to original prepared Create requests, and records distinct recovery Abort
+ACKs. No PUT/Complete retry or fabricated original ACK is allowed. Public
+absence alone cannot permit cleanup: a stopped four-node census must also
+find no remaining MPU files. The original stopped business snapshot is frozen
+before load; the final stopped snapshot must preserve all original files and
+formats, with only the existing physical repair oracle's exact acknowledged
+version exceptions. Hidden business files also block cleanup. Both original
+request and ACK byte hashes must remain unchanged. Unexpected positive mutation ACKs, changed/missing
+business versions and hidden MPU orphans retain the namespace.
+
+`mpu_scene.py` hashes all files under eight exact run-owned bucket/key SHA-256
+prefixes on the original sixteen drives, using the immutable candidate's
+multipart layout. It neither decodes opaque UploadIds nor reads credential or
+other system-key contents. It requires stopped original services, matching
+candidate/owner/mount identities and bounded stable regular files. The census
+does not claim payload integrity or EC/bitrot correctness.
+
+The live adapter now preserves original object and system MPU failure scenes,
+checks partial refused deletes without dropping version assertions, and routes
+unresolved effects into the strict isolation/recovery protocol. The earlier
+807-check session delivery remains scoped evidence; current full-adapter
+regressions and fresh live acceptance are recorded separately below.
+
+I03 unresolved-scene isolation (2026-10-19): `network_retention.py` now freezes
+the original four reservation owners and stopped business/MPU scene digests.
+All four read-only preflights precede any move. The existing durable isolation
+backend supports `lock-quorum` with private per-run archive parents and
+non-overwriting same-filesystem renames. It hashes the primary and DB fixture,
+original DL MPU key prefixes and original I03 shard-injection backups while
+retaining credential files by inode/metadata without reading their contents.
+Interrupted moves resume against the same durable census; missing roots,
+changed archived bytes, active gates, owner/mount drift and destination
+collisions block progress. Every original file and inode is retained; no shard
+restoration, S3 Abort or namespace deletion is performed by isolation.
+
+The recovery entry point accepts this path only for one archived I03
+FAIL/cleanup-FAIL branch with an exact candidate/package/binary/helper/run
+binding and four matching original reservations. It reports `ISOLATED`, never
+`PASS` or `alreadyClean`, including retries after all active paths are absent.
+Historical expiry-only recovery keeps its existing strict boundary. The live
+I03 adapter now emits this intent when a seed or original DL effect remains
+unresolved. Local filesystem/controller tests do not certify live isolation
+or increase the canonical count; full-fixture live acceptance remains pending.
+
+
+I03 complete DL adapter (2026-10-19): the fixed `1c7fc818...` candidate now
+executes all DB/DV/DL stages in one original branch. The session exists before
+any DL SDK call. Its six acknowledged data versions join the primary baseline,
+all original HEADs, recursive version responsibility and all sixteen-drive
+streaming checks. Four workers issue the 28-call matrix inside the actual
+partition, between independent network witnesses. DL stable reads use their
+own intact data and actual shared-lock quorum, independently of the separate
+missing DV shard. The 128-object DB fixture remains included in every complete
+business snapshot.
+
+`network_delete_records.py` reads only bounded `delete-prepared` JSON files
+under the two original stopped DL control keys. It exports selectors and
+hashes, never raw records or nested metadata. Exact source hashes bind the
+version-1 journal, FileInfo serialization, delete rollback and Heal settlement
+behavior. Duplicate JSON fields, unsafe files, wrong runtime/owner, unbound
+versions/data directories/metadata hashes/target bytes and changed
+incarnations fail closed. The exact original refused historical-version
+request can explain only its reachable writable slots. Other versions, DB/DV
+controls, readonly or disconnected roots, new business commits and altered
+part bytes keep their original strict assertions.
+
+A complete prepared original backup is a product settlement obligation, not
+proof of commit or rollback. After rejoin, independent recursive product tasks
+must report all three original versions per DL key. Unbacked version loss
+requires an actual repaired receipt; source-bound product delete settlement
+may precede a healthy shard receipt only for that exact original generation
+and incarnation. All final physical files/formats, original shard bytes and
+public versions/metadata/tags still must match before cleanup. MPU retirement
+uses separate recovery ACKs and stopped final MPU/business checks. These occur
+before the recovered unique PUT. An incomplete seed/load or unresolved effect
+instead stops services, captures the original scene, releases owned gates and
+isolates unchanged inodes; the case and cleanup remain FAIL until separately audited.
+
+Failure capture order is explicit: stop services, save original stopped
+business/MPU bytes and logs while fault intents still exist, release only
+owned network/private-write gates, and recapture the stopped scene. Byte
+hashes must remain identical across gate recovery before isolation can begin.
+
+The full adapter intentionally requires the exact fixed candidate above.
+Historical `072b57b...` scoped source/contracts are retained, but its different
+delete protocol does not certify this expanded adapter. No product source,
+compiled collector, inspector pin, native barrier, request retry or time budget
+changes in this delivery. Local modeled orchestration and filesystem tests
+remain distinct from new full-fixture live evidence. Canonical acceptance
+remains **104/158** until that evidence is independently verified.
+
+
+I03 full-fixture live run [39254429889/1](https://github.com/rustfs/rustfs/actions/runs/39254429889)
+used helper `3b064c90bc24a11003c1721a964a9c30b831751a`, fixed candidate
+`1c7fc818dd858247c45b39542abd8291b1e6e31b`, package `38288322026/1`
+and inspector `39214576404/1`. Its first minority-partition branch failed:
+19 DL requests were actually invoked and fully drained, with zero unknown
+results. The node1 historical DELETE and node4 GET returned definite
+`500/InternalError`. Original request IDs, exact version selectors, seed
+payloads and current server PIDs were independently correlated. The remaining
+four branches did not run. These responses retain their original FAIL;
+no expected-status relaxation or product root-cause claim is made.
+
+The original stopped business/MPU/log scene was captured before owned-gate
+removal, with unchanged business and MPU hashes afterward. All original
+root/state trees on the four nodes were isolated by inode-preserving same-filesystem moves into
+private per-run archives. Separate recovery re-audit
+[39257565469/1](https://github.com/rustfs/rustfs/actions/runs/39257565469)
+verified all four preflight, no-op isolation and post-audit receipts. Independent
+audit checked the official ZIP digests, all 723 unchanged original evidence
+members, all 20 original root/state content and inode censuses, unchanged
+private archive parents and the exact executed stopped-service/binary/mount
+guards. Two sequential reviews passed before fleet ownership was released.
+Recovery reports `ISOLATED`; it performs no S3 requests, namespace deletion,
+old-shard restoration or original verdict rewrite. Original case SHA-256 is
+`13d06d16e8a05d544ec5dd6df5ae8bdabcd6c204c85f8ca4f411196569807099`.
+The complete I03 case remains unaccepted; the canonical total stays **104/158**.
+
+I03 diagnostic candidate `85c706cac09d53d0649487b8f1865f18beb53225`
+adds bounded GET/DELETE error classification at the existing S3 handler return
+boundary. It preserves the ingress request ID and original response. The
+classifier uses `S3Error`'s inherent source accessor, including nested wrappers;
+its standard error trait does not expose that payload. Two actual development
+regressions caught empty source/RPC classifications before this correction.
+The final three classification checks and one actual JSON-event check pass,
+including nested/cyclic errors, numeric errno, unchanged response properties
+and sensitive-message/RPC-metadata exclusion. Two fresh final-diff source
+reviews passed. This diagnoses errors without changing HTTP mapping, storage,
+RPC/authentication, retries, timeouts or the old live result.
+
+The adapter explicitly admits this additional full commit after checking all
+20 referenced product source files against committed Git objects. Eighteen
+original contract files remain byte-identical; the error source hash is
+updated and the diagnostic S3 boundary is additionally bound. Independent
+extractor checks cover 21 distinct files in total and retain the original
+`503/SlowDownRead` and size rules. Original `1c7fc818...` contracts are unchanged;
+unknown neighboring SHAs and the historical incompatible delete protocol stay
+rejected. The 825 Heal checks pass with nine platform skips.
+
+Separate hosted package-only run
+[39267250075/1](https://github.com/rustfs/rustfs/actions/runs/39267250075)
+is building this exact source. The complete published package, opaque build
+bytes and a newly built matching inspector still require independent audits
+before a new full-fixture live run. No previous tool artifact is treated as a
+new-candidate build, and no original `500` or canonical count is rewritten.
+
+
+C02/C03 DB boundary completion (2026-10-19): the shard executor retains all
+128 original DB controls and every frozen first/middle/last corruption branch,
+including C02's completed multipart-data branch. Before any fault, the exact
+candidate size contract, actual default-class process environment and decoded
+full-set calibration object now derive inline and EC block minus/exact/plus
+sizes. Supplemental public PUT controls also cover zero/one, 4-KiB and the
+source-bound 5-MiB minimum-part-size boundary. These single-PUT size controls do
+not claim completed multipart protocol coverage or a Cartesian C03/DM matrix.
+The original count, bytes and MPU controls remain in the full baseline; the
+combined fixture must fit the existing 256-object and 512-MiB budgets. Every
+supplemental control requires a decoded full-set shape and original physical
+checksum/no-part proof, repeated after each repair, plus independent full GET
+and Range verification. The runtime integration regression rejects the previous
+uncalibrated C02 and C03 callers. Six focused tests and the 831-test Heal suite
+pass (nine platform skips). Fresh candidate-bound H1/H2 execution and independent
+acceptance remain required; the original C02/C03 failures and canonical
+104/158 count are unchanged. I03's inspector and replay remain pinned to the
+separately reviewed `536a0a13b7a428a557dbe20d03faf72808da4abd` helper.
+
+
+I04 physical-scope correction (2026-10-19): original run `38119569421/1`
+recorded successful 769/129/129/129-object queue manifests on both H1 and H2,
+but its physical baselines contain zero business files on every node. The exact
+original helper calls `Fleet.snapshot` without a bucket list, selecting only the
+unseeded primary owner bucket instead of its four queue buckets and durable
+control bucket. All six baseline, fault, attribution, pre-GET, post-GET and
+failure snapshots now explicitly include the primary prefix and those five
+owned buckets through the existing frozen fleet binding and bounded scope validator. No queue admission,
+resource limit, MRF retention/retirement, fairness, physical comparison, candidate
+source or cleanup oracle changes. Runtime regression tests execute the actual
+H1/H2 caller and `Fleet.snapshot` adapter; the previous caller fails. All 13
+focused checks and the 832-test Heal suite pass (nine platform skips). The
+original FAIL remains unchanged; later pressure behavior and complete fresh
+candidate-bound H1/H2 execution remain unproven. Canonical acceptance stays
+104/158, and I03 still uses its separate frozen helper.
+
+
+I05 manual timeout deployment (2026-10-22): `lock_timeout_runner.py` now joins
+the existing four-argument dispatcher and Functional Chain manual case input.
+It validates the exact 757b candidate, immutable helper, inspector and current
+vm000-vm003 binding before package bootstrap, then runs independent H1 and H2
+timeout diagnostics with original versioned ACKs, full peer content/metadata/tag
+verification, physical comparisons and original-process FD closure. Dirty
+H1, unknown writes or failed finalization stop the sequence and retain the
+original stopped namespace/log scene. SIGTERM follows that retention path.
+The wrapper's pre-existing log directory is accepted; prior topology or summary
+files are refused before any new deployment. A clean diagnostic returns
+canonical BLOCKED, never PASS; the automatic matrix remains unchanged.
+
+Final focused verification ran 106 tests (104 passed, two Linux-only skips);
+31 existing shell/outcome/nolock compatibility tests also passed. All 14 new
+runner tests passed, including the actual CLI, real SIGTERM, retained receipts,
+credential redaction and executed shell verdict block. The first focused run
+had an earlier receive-buffer prerequisite failure in the empty-send-queue
+negative test; the single test and complete focused rerun passed. A separate
+300-connection macOS probe observed one kernel buffer increase after connect,
+so this local transport limitation remains explicit; the bounded guard is not
+relaxed and actual Linux component coverage remains in hosted CI. Catalog
+158 and matrix 84/302/534 checks, shell syntax, actionlint and whitespace pass.
+These checks do not establish live or canonical I05 acceptance.
+
+Read-only environment run [39710469108/1](https://github.com/rustfs/rustfs/actions/runs/39710469108)
+completed against candidate package 39364893106/1 and helper fa79b52. It
+observed sixteen independent eligible endpoints, H1/H2 mounts and no port-9000
+listeners. vm001-vm003 retain failed units from cancelled F07 38715204273/1;
+its separately recorded successful recovery is [38726570148/1](https://github.com/rustfs/rustfs/actions/runs/38726570148).
+The inspection performs no mutation and certifies neither historical-scene
+recovery nor global workflow idleness. Fresh ownership/cleanup and immutable
+artifact checks still precede any new fleet diagnostic. H4's twenty-four
+endpoints and H6 platform restart authority remain unavailable.
+
+
+I05 first manual live diagnostic [39711742431/1](https://github.com/rustfs/rustfs/actions/runs/39711742431)
+used helper `704bd606071676dded130dca91d3d5278455bcaa`, candidate
+`757b751482bb970bc321e92059690a7698554358`, package build 39364893106/1 and
+inspector build 39566504743/1. H1 retained FAIL/cleanup FAIL after all nine seed
+requests received successful bound ACKs and the original full S3 baseline
+passed. The single historical GET was written and its client socket closed;
+no contender PUT was invoked and H2 did not start. Official artifact
+12397696964 has digest `e9ab49593f0423f0649066aa71325d59fadc0e7e6d0328071e2f06f45790325f`.
+
+The observer incorrectly required an absent diagnostic version field. It now
+accepts an absent field or the exact unique HTTP version selector and rejects
+any other version. The three original production events are regression
+fixtures: they correlate to the exact GET but already show `guard_dropped`
+within approximately 0.36 ms, so they cannot prove a held timeout window. The
+reviewed source passes no version to this diagnostic while this log carries
+one; that difference is not yet explained. Neither a wrong package nor a
+source mix is inferred from it. Do not replay the same warmed baseline premise
+or claim that the field correction establishes lock contention.
+
+`timeout_retention.py` recognizes three exact I05 failure boundaries: the first
+helper's H1 failure before a contender, the reviewed `d100abeb` H1 failure
+after one contender PUT received a known 503, and current helper `1b88cad` H2
+failure after H1 completed and cleaned successfully. The H1 paths require the
+exact candidate/package/binary/inspector and owner, all nine successful seed
+request/ACK pairs, deterministic bytes, public metadata/tags/versions, closed
+read-only historical GET and unchanged stopped four-drive physical scene. The
+H2 path also requires all four owners and stopped physical scenes, inspector
+uploads, preflight and deployment proof, per-node resource baselines, lock
+checkpoints, three complete lock journal windows, and service logs. Every
+contender boundary checks its only durable request/ACK, request digest and
+clock ordering, structured `ServiceUnavailable` response, request-correlated
+bucket-incarnation read-lock lifecycle, and exact failed `put_object_commit`
+write-lock lifecycle. Unknown or successful writes, changed physical evidence,
+missing peers, an unclean H1, another case or extra actions refuse recovery.
+
+Recovery reuses retained-scene isolation with same-filesystem non-overwriting
+renames, durable intent, preflight, interrupted-move retries and five private
+archive parents. It makes no S3 request and never restores backups, deletes the
+namespace or rewrites the original FAIL. The first failure recovery
+[39715499193/1](https://github.com/rustfs/rustfs/actions/runs/39715499193)
+completed `ISOLATED`; all 58 original artifact files matched byte-for-byte and
+the five scene roots moved into private archive parents. The second diagnostic
+[39733064688/1](https://github.com/rustfs/rustfs/actions/runs/39733064688)
+also remains FAIL/cleanup FAIL. Its server returned the expected 503 after a
+1001 ms target write-lock timeout while the GET read lock was held for 4.148 s
+and its service socket retained an 8.17 MB send queue. The validator rejected
+the event sequence because it grouped the PUT's bucket-incarnation read lock
+with its target write lock. The before/after stopped four-drive physical
+census matched; the complete S3 inventory was captured before the contender,
+and the server returned a known 503 with no successful VersionId. A
+post-contender S3 inventory was not captured. The new recovery planner accepts
+the exact archived H1 boundary for preservation; it does not make a canonical
+PASS claim or authorize replay. Read-only environment inspection
+[39738071753/1](https://github.com/rustfs/rustfs/actions/runs/39738071753)
+completed with stable observations of all four current Heal nodes and no
+RustFS service, validation unit or port-9000 listener on the target vm000.
+Evidence-based isolation [39738426191/1](https://github.com/rustfs/rustfs/actions/runs/39738426191)
+completed `ISOLATED` for the exact vm000 scene: all 65 original artifact files
+match byte-for-byte; preflight, move and post-audit physical digests match;
+four data roots and private state remain in five root-owned mode-0700
+same-device archive parents. No S3 request, backup restoration, namespace
+deletion or original verdict rewrite occurred. The inspection also observed
+previous failed F07 units on vm001-vm003, which were outside this plan. The
+original run remains FAIL/cleanup FAIL. Canonical acceptance and its 104/158
+count remain unchanged.
+
+
+I05 lock-window diagnosis (2026-10-22): exact candidate source
+`757b751482bb970bc321e92059690a7698554358` defaults
+`RUSTFS_OBJECT_LOCK_OPTIMIZATION_ENABLE` to `true`. Its `get_object` path
+acquires the ECStore read guard, marks downstream options `no_lock`, then
+returns the reader without attaching that guard when optimization is enabled.
+The original isolated-service attestation does not set this variable, so the
+candidate default applies. The archived GET's exact `acquired` and
+`guard_dropped` events share request `1ba2503b-78ea-40cd-b2ff-003f54662c9c` and
+lock attempt `3a86c2cc-ada5-4e8a-9e0d-dbe3bb7c1489`; they are 0.367104 ms apart.
+This explains the missing backpressured lock window without attributing it to
+the caller socket or optional version field.
+
+The `lock-timeout` runtime profile now explicitly sets
+`RUSTFS_OBJECT_LOCK_OPTIMIZATION_ENABLE=false`, causing the streaming GET to
+retain its read guard until EOF or reader drop. The candidate binary and other
+runtime profiles are unchanged. The existing remote process observer checks
+every configured setting, so it will reject a deployment that omits or changes
+this value. Focused verification ran 50 observer/timeout/runner tests: 48
+passed and two existing Linux-only tests were skipped on macOS; `git diff
+--check` passed. The previous FAIL and its isolated scene remain retained; this
+local runtime correction is not live or canonical acceptance.
+
+I05 H2 retained-scene recovery (2026-10-22): `timeout_retention.py` now accepts
+the exact `1b88cad` H2 contender failure only after H1 completed with cleanup
+PASS. The H2 plan binds all four reserved owners, inspector uploads, preflight
+mounts, service identities/settings, journal checkpoints and balanced peer
+sampling rounds, plus the four stopped physical scenes and failure logs. It
+still requires the exact nine successful seed ACKs, complete public baseline,
+closed historical GET and known contender 503/lock lifecycle. Unknown,
+successful, foreign-peer, partial or physically changed evidence refuses
+before creating a Fleet or moving any root. H2 isolation therefore keeps the
+four peers together and never changes the original FAIL. An H2 archive fixture
+and negative evidence cases were added. The matching local CI group ran 117
+tests successfully with two existing Linux-only tests skipped on macOS; the
+previous single timeout FD test also passed when rerun in isolation. No new
+live diagnostic or canonical acceptance is established by these checks.
+
+Post-isolation read-only inspection [39745040693/1](https://github.com/rustfs/rustfs/actions/runs/39745040693)
+used helper `22589bc00836b5d47f382429313e967c1dfc38a9` and candidate package
+39364893106/1. It observed 16 independent eligible endpoints and available H1/H2
+mount shapes, with stable runtime observations and no port-9000 listeners.
+vm000 had no loaded validation unit; vm001-vm003 still reported the failed H2
+unit from cancelled F07 run 38715204273/1. The inspection recorded
+`mutated=false`, `cleanStateCertified=false` and
+`globalWorkflowIdleCertified=false`; it neither changed that scene nor adds
+canonical acceptance.
+
+I05 drain-budget failure [39746002648/1](https://github.com/rustfs/rustfs/actions/runs/39746002648)
+is now recognized by the retention planner under helper `0442d4d`. The plan
+accepts only its H1 `NOT_OBSERVED` boundary: one closed, non-retried historical
+GET with an admitted lock owner, one known 503 contender PUT whose target write
+lock reached timeout without acquisition, the exact caller marker and lock
+lifecycle, the two backpressure socket samples, all repeated read-only drain
+socket/resource/journal samples, unchanged four-drive scene and the original
+nine seed ACK pairs. Unknown or successful writes, extra requests, missing
+samples, changed process identity or physical drift refuse before Fleet
+creation. The archived failure remains `FAIL/cleanup FAIL`; no canonical PASS,
+backup restoration, namespace deletion or verdict rewrite is authorized.
+
+I05 drain-budget scene isolation [39750591531/1](https://github.com/rustfs/rustfs/actions/runs/39750591531)
+completed `ISOLATED` using helper `b56b848`. Independent audit compared all 101
+original artifact files byte-for-byte, rechecked the unchanged original
+`FAIL/cleanup FAIL`, and verified identical preflight/move/post-audit tree,
+owner, device and inode baselines. The original business scene digest remains
+`c438f5bf295f56ddd168e9e840b41281863c3da06c0973eb9abead95edd63e89`.
+Four data roots and private state remain in five root-owned mode-0700
+same-device archive parents. No S3 request, backup restoration, namespace
+deletion or original verdict rewrite occurred. This preservation result adds
+no canonical acceptance; the documented count remains 104/158.
+
+The retained run proves the caller socket disappeared and its diagnostic read
+guard dropped, while the process FD count remained 23 against baseline 22.
+It does not identify the extra FD. I05 resource and socket samples now include
+bounded descriptor kinds and target fingerprints plus TCP rows joined to the
+original process's owned socket inodes. File contents and raw descriptor paths
+are excluded. The same original process birth and network namespace must
+survive collection; snapshots still make no global no-leak claim. The FD
+threshold and thirty-second drain budget are unchanged. A real local HTTP/1.1
+503 integration confirms the actual `Probe.s3` SDK path closes its keepalive
+socket without retry, but does not identify the original production FD. Fresh
+live descriptor evidence is required before attributing or correcting it.
+
+Verification: all 70 focused observer/timeout/transport/performance checks
+passed in Linux, including actual `/proc` descriptors and network-namespace
+refusal. The nine direct socket/census components also passed without network
+access in Linux. All 63 retention/runner/migration checks passed on macOS;
+catalog 158, matrix 84/302/534 and whitespace checks passed. The macOS focused
+suite encountered the previously recorded receive-buffer prerequisite in one
+physical-change negative test; that test passed independently and in the full
+Linux suite. The prerequisite and failure oracle were not relaxed.
+
+I05 descriptor diagnosis `39754893467/1` retains FAIL/cleanup FAIL under exact
+helper `dd7e54e6672cab76b5a6245ab3b4fbd051ddd39d`; H2 was not started. Complete
+FD snapshots attribute the 22-to-23 increase to an external TCP connection
+from `168.63.129.16` while the intentional GET socket disappears and its read
+lock drops. The concrete platform probe configuration remains unverified.
+The preservation planner now validates this helper's complete census and
+original bounded refusal, caller, journal and physical evidence. The real
+101-file fixture and corrupt-evidence regressions exercise preservation
+without S3 access, restoration, verdict change or canonical acceptance.
diff --git a/rustfs-kms-test.sh b/rustfs-kms-test.sh
index 59b20825..7391bf7e 100755
--- a/rustfs-kms-test.sh
+++ b/rustfs-kms-test.sh
@@ -18,6 +18,7 @@
# ./rustfs-kms-test.sh --topology single-single --backends local -y
#
set -Eeuo pipefail
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Per-run temp dir (shared runners may leave stale /tmp files owned by other users)
TEST_TMP="$(mktemp -d "${TMPDIR:-/tmp}/rustfs-test.XXXXXX")"
@@ -125,6 +126,12 @@ ENDPOINT=""
VOLUMES=""
KMS_BACKEND="local"
KMS_EXTRA_ENV="${KMS_EXTRA_ENV:-}"
+VALIDATION_CASE=""
+VALIDATION_ARTIFACTS_DIR=""
+VALIDATION_CANDIDATE_MANIFEST=""
+VALIDATION_TESTING_COMMIT=""
+VALIDATION_BINARY_SHA256=""
+VALIDATION_OBJECT_CLEANUP_STATUS=PASS
# ==================== Helpers ====================
@@ -710,6 +717,36 @@ run_test() {
fi
}
+validation_write_evidence() {
+ local evidence_dir="${VALIDATION_ARTIFACTS_DIR}/evidence/${VALIDATION_CASE}" item
+ mkdir -p "${evidence_dir}"
+ cp "${TEST_TMP}/test.out" "${evidence_dir}/test.log"
+ for item in validation-put.json validation-head.json validation-get.json validation-failure.txt validation-missing.txt validation-fixture.json; do
+ if [ -f "${TEST_TMP}/${item}" ]; then cp "${TEST_TMP}/${item}" "${evidence_dir}/${item}"; fi
+ done
+}
+
+validation_write_result() {
+ local verdict="$1" reason="$2" cleanup_status="$3" item
+ local args=("${SCRIPT_DIR}/scripts/validation_result.py"
+ --case-id "${VALIDATION_CASE}" --profile single-1disk
+ --result-file "${VALIDATION_ARTIFACTS_DIR}/cases/${VALIDATION_CASE}.json"
+ --verdict "${verdict}" --cleanup-status "${cleanup_status}")
+ if [ -n "${reason}" ]; then args+=(--reason "${reason}"); fi
+ if [ "${verdict}" != RUNNING ]; then
+ args+=(--candidate-manifest "${VALIDATION_CANDIDATE_MANIFEST}"
+ --rc-bin "$(command -v rc)" --testing-commit "${VALIDATION_TESTING_COMMIT}"
+ --binary-sha256 "${VALIDATION_BINARY_SHA256}"
+ --evidence "evidence/${VALIDATION_CASE}/test.log")
+ fi
+ if [ "${verdict}" = PASS ]; then
+ for item in validation-put.json validation-head.json validation-get.json validation-failure.txt validation-missing.txt validation-fixture.json; do
+ args+=(--evidence "evidence/${VALIDATION_CASE}/${item}")
+ done
+ fi
+ python3 "${args[@]}"
+}
+
rc_run() {
RC_OUT="$(rc "$@" 2>&1)"; RC_CODE=$?
return "${RC_CODE}"
@@ -1218,6 +1255,72 @@ test_kms301() {
rm_bucket kms301-b1
}
+test_validation_kms_sse_kms_round_trip() {
+ local bucket="val-kms-${RANDOM}-$$" key="encrypted" rejected="rejected"
+ local source="${TEST_TMP}/validation-kms-source" downloaded="${TEST_TMP}/validation-kms-downloaded"
+ local marker="validation-kms-plaintext-${RANDOM}-$$" kid="" expected_sha case_rc=0 cleanup_rc=0
+ VALIDATION_OBJECT_CLEANUP_STATUS=PASS
+ kms_create_key "val-kms-${RANDOM}-$$" "validation=roundtrip" || return 1
+ kid="${KMS_KEY_ID}"
+ printf '%s' "${marker}" > "${source}"
+ head -c 4096 /dev/urandom >> "${source}"
+ expected_sha="$(sha256sum "${source}" | awk '{print $1}')"
+ jq -n --arg bucket "${bucket}" --arg key "${key}" --arg key_id "${kid}" \
+ --arg sha256 "${expected_sha}" --argjson bytes "$(wc -c < "${source}")" \
+ '{bucket:$bucket,key:$key,key_id:$key_id,sha256:$sha256,bytes:$bytes,metadata:{validation:"kms-roundtrip"}}' \
+ > "${TEST_TMP}/validation-fixture.json"
+ new_bucket "${bucket}" || return 1
+ (
+ aws_run s3api put-object --bucket "${bucket}" --key "${key}" --body "${source}" \
+ --server-side-encryption aws:kms --ssekms-key-id "${kid}" --metadata validation=kms-roundtrip
+ assert_aws_ok || exit 1
+ printf '%s\n' "${AWS_OUT}" > "${TEST_TMP}/validation-put.json"
+ printf '%s' "${AWS_OUT}" | jq -e --arg kid "${kid}" \
+ '.ServerSideEncryption == "aws:kms" and (.SSEKMSKeyId | strings | contains($kid))' >/dev/null \
+ || { echo "PUT lacks expected SSE-KMS key"; exit 1; }
+
+ aws_run s3api head-object --bucket "${bucket}" --key "${key}"
+ assert_aws_ok || exit 1
+ printf '%s\n' "${AWS_OUT}" > "${TEST_TMP}/validation-head.json"
+ jq -e --arg kid "${kid}" --argjson bytes "$(wc -c < "${source}")" \
+ '.ServerSideEncryption == "aws:kms" and (.SSEKMSKeyId | strings | contains($kid)) and .ContentLength == $bytes and .Metadata.validation == "kms-roundtrip"' \
+ "${TEST_TMP}/validation-head.json" >/dev/null || { echo "HEAD encryption or metadata mismatch"; exit 1; }
+
+ aws_run s3api get-object --bucket "${bucket}" --key "${key}" "${downloaded}"
+ assert_aws_ok || exit 1
+ printf '%s\n' "${AWS_OUT}" > "${TEST_TMP}/validation-get.json"
+ [ "$(sha256sum "${downloaded}" | awk '{print $1}')" = "${expected_sha}" ] \
+ || { echo "SSE-KMS payload mismatch"; exit 1; }
+
+ aws_run s3api put-object --bucket "${bucket}" --key "${rejected}" --body "${source}" \
+ --server-side-encryption aws:kms --ssekms-key-id "missing-${RANDOM}-$$"
+ assert_aws_fails || { echo "invalid KMS key unexpectedly accepted"; exit 1; }
+ printf '%s\n' "${AWS_OUT}" > "${TEST_TMP}/validation-failure.txt"
+ for secret in "${marker}" "${KMS_LOCAL_MASTER_KEY}" "${KMS_BACKUP_KEK}" "${VAULT_TOKEN}"; do
+ [ -n "${secret}" ] || continue
+ if [[ "${AWS_OUT}" == *"${secret}"* ]]; then echo "expected failure leaked plaintext or key material"; exit 1; fi
+ done
+ aws_run s3api head-object --bucket "${bucket}" --key "${rejected}"
+ assert_aws_fails || { echo "failed encrypted PUT persisted an object"; exit 1; }
+ printf '%s\n' "${AWS_OUT}" > "${TEST_TMP}/validation-missing.txt"
+ if [[ "${AWS_OUT}" == *"${marker}"* ]] || [[ "${AWS_OUT}" == *"${KMS_LOCAL_MASTER_KEY}"* ]]; then
+ echo "missing-object failure leaked plaintext or key material"; exit 1
+ fi
+ ) || case_rc=$?
+
+ for key in "${key}" "${rejected}"; do
+ aws_run s3api delete-object --bucket "${bucket}" --key "${key}"
+ assert_aws_ok || cleanup_rc=1
+ done
+ aws_run s3api delete-bucket --bucket "${bucket}"
+ assert_aws_ok || cleanup_rc=1
+ if [ "${cleanup_rc}" -ne 0 ]; then
+ VALIDATION_OBJECT_CLEANUP_STATUS=FAIL
+ echo "validation bucket cleanup failed"
+ fi
+ [ "${case_rc}" -eq 0 ] && [ "${cleanup_rc}" -eq 0 ]
+}
+
test_kms302() {
kms_create_key "k302" "env=ci" || return 1
local kid="${KMS_KEY_ID}"
@@ -2139,6 +2242,10 @@ Usage:
--ssh-user SSH user for the test nodes (default: azureuser)
--log-file Tee output to a log file
--dry-run Preview commands without executing them
+ --validation-case ID Run only kms.sse-kms-round-trip (local, single-single)
+ --validation-artifacts-dir DIR Write canonical result and evidence
+ --candidate-manifest FILE Immutable candidate manifest
+ --testing-commit SHA auto-testing commit used for validation results
-y, --yes Skip confirmation prompts
-h, --help Show this help
USAGE
@@ -2161,6 +2268,10 @@ main() {
--ssh-user) SSH_USER="$1"; shift ;;
--log-file) LOG_FILE="$1"; shift ;;
--dry-run) DRY_RUN=1 ;;
+ --validation-case) VALIDATION_CASE="$1"; shift ;;
+ --validation-artifacts-dir) VALIDATION_ARTIFACTS_DIR="$1"; shift ;;
+ --candidate-manifest) VALIDATION_CANDIDATE_MANIFEST="$1"; shift ;;
+ --testing-commit) VALIDATION_TESTING_COMMIT="$1"; shift ;;
-y|--yes) ASSUME_YES=1 ;;
-h|--help) usage; exit 0 ;;
*) die "unknown option: ${opt} (see --help)" ;;
@@ -2177,6 +2288,28 @@ main() {
if [ "${all}" -eq 1 ]; then
SELECTED_TOPOLOGIES=("${TOPO_SINGLE_SINGLE}" "${TOPO_SINGLE_MULTI}" "${TOPO_MULTI_MULTI}")
fi
+ if [ -n "${VALIDATION_CASE}" ]; then
+ [ "${VALIDATION_CASE}" = "kms.sse-kms-round-trip" ] || die "unknown validation case: ${VALIDATION_CASE}"
+ [ "${all}" -eq 0 ] || die "--validation-case cannot be combined with --all-topologies"
+ [ "${#SELECTED_TOPOLOGIES[@]}" -eq 0 ] || die "--validation-case chooses its own topology"
+ [ "${KEEP_ENV}" -eq 0 ] || die "--validation-case requires environment teardown"
+ SELECTED_TOPOLOGIES=("${TOPO_SINGLE_SINGLE}")
+ backends_arg=local
+ if [ "${DRY_RUN}" -eq 0 ]; then
+ [ -n "${VALIDATION_ARTIFACTS_DIR}" ] || die "--validation-case requires --validation-artifacts-dir"
+ [ -n "${VALIDATION_CANDIDATE_MANIFEST}" ] || die "--validation-case requires --candidate-manifest"
+ [ -n "${VALIDATION_TESTING_COMMIT}" ] || die "--validation-case requires --testing-commit"
+ [ ! -e "${VALIDATION_ARTIFACTS_DIR}" ] || die "validation artifacts directory already exists"
+ PACKAGE_URL="$(jq -er '.package_url' "${VALIDATION_CANDIDATE_MANIFEST}")" || die "candidate URL missing"
+ PACKAGE_SHA256="$(jq -er '.package_sha256' "${VALIDATION_CANDIDATE_MANIFEST}")" || die "candidate package SHA missing"
+ [[ "${PACKAGE_SHA256}" =~ ^[0-9a-f]{64}$ ]] || die "invalid candidate package SHA"
+ jq -e '.source_sha | test("^[0-9a-f]{40}$")' "${VALIDATION_CANDIDATE_MANIFEST}" >/dev/null \
+ || die "candidate source commit missing"
+ [ "${VALIDATION_TESTING_COMMIT}" = "$(git -C "${SCRIPT_DIR}" rev-parse HEAD)" ] \
+ || die "testing commit does not match this checkout"
+ mkdir -p "${VALIDATION_ARTIFACTS_DIR}"
+ fi
+ fi
[ "${#SELECTED_TOPOLOGIES[@]}" -gt 0 ] || die "no topology selected (--all-topologies / --topology)"
IFS=',' read -r -a BACKENDS <<<"${backends_arg}"
[ "${#BACKENDS[@]}" -gt 0 ] || die "no backends selected (--backends local,vault-kv2)"
@@ -2192,6 +2325,7 @@ main() {
need_cmd jq "JSON processor"
need_cmd curl "HTTP client"
need_cmd openssl "OpenSSL (SigV4)"
+ if [ -n "${VALIDATION_CASE}" ]; then need_cmd python3 "validation result writer"; fi
log "nodes: ${NODES[*]} package: $(resolve_package_url)"
log "backends: ${BACKENDS[*]} topologies: ${SELECTED_TOPOLOGIES[*]}"
@@ -2205,17 +2339,41 @@ main() {
vault_stop
vault_start
fi
+ if [ -n "${VALIDATION_CASE}" ] && [ "${DRY_RUN}" -eq 0 ]; then
+ validation_write_result RUNNING "validation case started" PENDING
+ fi
env_setup "${topo}"
+ if [ -n "${VALIDATION_CASE}" ] && [ "${DRY_RUN}" -eq 0 ]; then
+ VALIDATION_BINARY_SHA256="$(printf 'sha256sum /usr/bin/rustfs\n' | run_remote "${NODES[0]}" | awk '{print $1}')"
+ [[ "${VALIDATION_BINARY_SHA256}" =~ ^[0-9a-f]{64}$ ]] || die "installed RustFS binary SHA-256 unavailable"
+ fi
PASS=0; FAIL=0; UNSUPPORTED=0; FAILED_TESTS=()
KMS_KEY_ID=""
- run_all_tests
+ if [ -n "${VALIDATION_CASE}" ]; then
+ run_test "${VALIDATION_CASE}" "SseKmsRoundTrip" test_validation_kms_sse_kms_round_trip
+ else
+ run_all_tests
+ fi
results_summary
record_topology_result "${topo}/${KMS_BACKEND}"
+ local cleanup_status=PASS
if [ "${KEEP_ENV}" -eq 0 ]; then
- env_teardown "${topo}"
+ env_teardown "${topo}" || cleanup_status=FAIL
if [ "${KMS_BACKEND}" = "vault-kv2" ]; then vault_stop; fi
fi
+ if [ "${VALIDATION_OBJECT_CLEANUP_STATUS}" != PASS ]; then cleanup_status=FAIL; fi
+ if [ -n "${VALIDATION_CASE}" ] && [ "${DRY_RUN}" -eq 0 ]; then
+ validation_write_evidence
+ if [ "${FAIL}" -eq 0 ] && [ "${UNSUPPORTED}" -eq 0 ] && [ "${cleanup_status}" = PASS ]; then
+ validation_write_result PASS "" PASS
+ elif [ "${FAIL}" -eq 0 ] && [ "${UNSUPPORTED}" -eq 0 ]; then
+ validation_write_result INFRA_ERROR "environment teardown failed" FAIL
+ else
+ validation_write_result FAIL "case failed or unsupported; inspect evidence" "${cleanup_status}"
+ fi
+ fi
if [ "${FAIL}" -gt 0 ]; then overall_fail=1; fi
+ if [ "${UNSUPPORTED}" -gt 0 ] || [ "${cleanup_status}" != PASS ]; then overall_fail=1; fi
done
done
final_summary
@@ -2223,4 +2381,6 @@ main() {
log "all done"
}
-main "$@"
+if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
+ main "$@"
+fi
diff --git a/rustfs-replication-test.sh b/rustfs-replication-test.sh
index 885aae31..036a182e 100755
--- a/rustfs-replication-test.sh
+++ b/rustfs-replication-test.sh
@@ -1384,4 +1384,6 @@ main() {
log "all done"
}
-main "$@"
+if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
+ main "$@"
+fi
diff --git a/rustfs-s3-compat-test.sh b/rustfs-s3-compat-test.sh
index 43b09d15..4c4f5c82 100755
--- a/rustfs-s3-compat-test.sh
+++ b/rustfs-s3-compat-test.sh
@@ -20,6 +20,7 @@
# ./rustfs-s3-compat-test.sh --all-topologies --dry-run
#
set -Eeuo pipefail
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Per-run temp dir (shared runners may leave stale /tmp files owned by other users)
TEST_TMP="$(mktemp -d "${TMPDIR:-/tmp}/rustfs-test.XXXXXX")"
@@ -82,6 +83,12 @@ ASSUME_YES=0
KEEP_ENV=0
LOG_FILE=""
SELECTED_TOPOLOGIES=()
+VALIDATION_CASE=""
+VALIDATION_ARTIFACTS_DIR=""
+VALIDATION_CANDIDATE_MANIFEST=""
+VALIDATION_TESTING_COMMIT=""
+VALIDATION_OBJECT_CLEANUP_STATUS=PASS
+VALIDATION_BINARY_SHA256=""
ENDPOINT=""
VOLUMES=""
@@ -356,10 +363,11 @@ EOF
}
env_teardown() {
- local topo="$1" nodes_str nodes=()
+ local topo="$1" nodes_str nodes=() failed=0 node
read -r -a nodes <<<"$(topology_nodes "${topo}")"
log "environment teardown: ${topo}"
- for node in "${NODES[@]}"; do cleanup_node "${node}"; done
+ for node in "${NODES[@]}"; do cleanup_node "${node}" || failed=1; done
+ return "${failed}"
}
# ==================== Server-domain / virtual-hosted-style helpers ====================
@@ -475,6 +483,48 @@ run_test() {
fi
}
+validation_write_evidence() {
+ local evidence_dir="${VALIDATION_ARTIFACTS_DIR}/evidence/${VALIDATION_CASE}"
+ local item
+ mkdir -p "${evidence_dir}"
+ cp "${TEST_TMP}/test.out" "${evidence_dir}/test.log"
+ for item in validation-put.json validation-head.json validation-list.json validation-missing.txt; do
+ if [ -f "${TEST_TMP}/${item}" ]; then cp "${TEST_TMP}/${item}" "${evidence_dir}/${item}"; fi
+ done
+}
+
+validation_write_result() {
+ local verdict="$1" reason="$2" cleanup_status="$3"
+ local result_file="${VALIDATION_ARTIFACTS_DIR}/cases/${VALIDATION_CASE}.json"
+ local args=(
+ "${SCRIPT_DIR}/scripts/validation_result.py"
+ --case-id "${VALIDATION_CASE}"
+ --profile "single-1disk"
+ --result-file "${result_file}"
+ --verdict "${verdict}"
+ --cleanup-status "${cleanup_status}"
+ )
+ if [ -n "${reason}" ]; then args+=(--reason "${reason}"); fi
+ if [ "${verdict}" != RUNNING ]; then
+ [ -n "${VALIDATION_CANDIDATE_MANIFEST}" ] || die "validation result requires --candidate-manifest"
+ [ -n "${VALIDATION_TESTING_COMMIT}" ] || die "validation result requires --testing-commit"
+ args+=(
+ --candidate-manifest "${VALIDATION_CANDIDATE_MANIFEST}"
+ --rc-bin "$(command -v rc)"
+ --testing-commit "${VALIDATION_TESTING_COMMIT}"
+ --binary-sha256 "${VALIDATION_BINARY_SHA256}"
+ )
+ args+=(--evidence "evidence/${VALIDATION_CASE}/test.log")
+ fi
+ if [ "${verdict}" = PASS ]; then
+ local item
+ for item in validation-put.json validation-head.json validation-list.json validation-missing.txt; do
+ args+=(--evidence "evidence/${VALIDATION_CASE}/${item}")
+ done
+ fi
+ python3 "${args[@]}"
+}
+
# rc helper: capture output + exit code. rc exit 7 = gated/unsupported.
rc_run() {
# usage: rc_run ; sets RC_OUT, RC_CODE
@@ -911,6 +961,64 @@ run_all_tests() {
run_test SNW-114 "ResponseAndChecksum" test_snw114
}
+# Keep the validation fixture together so CRUD, ETag, metadata, and errors
+# describe the same object rather than separate S3C-* fixtures.
+test_validation_s3_basic_object_round_trip() {
+ local bucket="val-basic-${RANDOM}-$$" key="object" source="${TEST_TMP}/validation-basic-source"
+ local downloaded="${TEST_TMP}/validation-basic-downloaded" expected_sha expected_etag put_etag
+ local case_rc=0 cleanup_rc=0
+ VALIDATION_OBJECT_CLEANUP_STATUS=PASS
+ make_test_file "${source}" 4096
+ expected_sha="$(sha256sum "${source}" | awk '{print $1}')"
+ expected_etag="\"$(md5_of_file "${source}")\""
+
+ new_bucket "${bucket}" || return 1
+ (
+ aws_run s3api put-object --bucket "${bucket}" --key "${key}" --body "${source}" \
+ --metadata validation=roundtrip --content-type application/octet-stream
+ assert_aws_ok || exit 1
+ printf '%s\n' "${AWS_OUT}" > "${TEST_TMP}/validation-put.json"
+ put_etag="$(printf '%s' "${AWS_OUT}" | jq -r '.ETag // empty')"
+ [ "${put_etag}" = "${expected_etag}" ] || { echo "PUT ETag mismatch: ${put_etag}"; exit 1; }
+
+ aws_run s3api head-object --bucket "${bucket}" --key "${key}"
+ assert_aws_ok || exit 1
+ printf '%s\n' "${AWS_OUT}" > "${TEST_TMP}/validation-head.json"
+ printf '%s' "${AWS_OUT}" | jq -e --arg etag "${expected_etag}" \
+ '.ContentLength == 4096 and .ETag == $etag and .Metadata.validation == "roundtrip" and .ContentType == "application/octet-stream"' >/dev/null \
+ || { echo "HEAD fields mismatch: ${AWS_OUT}"; exit 1; }
+
+ aws_run s3api get-object --bucket "${bucket}" --key "${key}" "${downloaded}"
+ assert_aws_ok || exit 1
+ [ "$(sha256sum "${downloaded}" | awk '{print $1}')" = "${expected_sha}" ] \
+ || { echo "GET payload mismatch"; exit 1; }
+
+ aws_run s3api list-objects-v2 --bucket "${bucket}"
+ assert_aws_ok || exit 1
+ printf '%s\n' "${AWS_OUT}" > "${TEST_TMP}/validation-list.json"
+ printf '%s' "${AWS_OUT}" | jq -e --arg key "${key}" \
+ 'any(.Contents[]?; .Key == $key and .Size == 4096)' >/dev/null \
+ || { echo "LIST omitted the object: ${AWS_OUT}"; exit 1; }
+
+ aws_run s3api delete-object --bucket "${bucket}" --key "${key}"
+ assert_aws_ok || exit 1
+ aws_run s3api get-object --bucket "${bucket}" --key "${key}" "${downloaded}"
+ assert_aws_fails_with "NoSuchKey" || exit 1
+ printf '%s\n' "${AWS_OUT}" > "${TEST_TMP}/validation-missing.txt"
+ ) || case_rc=$?
+
+ # Finalization is explicit because the old rm_bucket helper ignores errors.
+ aws_run s3api delete-object --bucket "${bucket}" --key "${key}"
+ if [ "${AWS_CODE}" -ne 0 ] && ! printf '%s' "${AWS_OUT}" | grep -qiE 'NoSuchKey|not exist'; then cleanup_rc=1; fi
+ aws_run s3api delete-bucket --bucket "${bucket}"
+ assert_aws_ok || cleanup_rc=1
+ if [ "${cleanup_rc}" -ne 0 ]; then
+ VALIDATION_OBJECT_CLEANUP_STATUS=FAIL
+ echo "validation bucket cleanup failed"
+ fi
+ [ "${case_rc}" -eq 0 ] && [ "${cleanup_rc}" -eq 0 ]
+}
+
# ---------- 1. Bucket basics ----------
test_s3c101() {
@@ -2776,6 +2884,10 @@ Options:
--ssh-user USER SSH user for the nodes (default azureuser)
--ssh-port PORT SSH port for the nodes (default 22)
--log-file FILE Append all output to FILE
+ --validation-case ID Run only s3.basic-object-round-trip (single-single)
+ --validation-artifacts-dir DIR Write canonical validation result and evidence
+ --candidate-manifest FILE Immutable candidate manifest for validation results
+ --testing-commit SHA auto-testing commit used for validation results
--dry-run Preview commands without executing them
-y, --yes Skip all confirmation prompts
-h, --help Show this help
@@ -2797,6 +2909,10 @@ main() {
--ssh-user) SSH_USER="$1"; shift ;;
--ssh-port) SSH_PORT="$1"; shift ;;
--log-file) LOG_FILE="$1"; shift ;;
+ --validation-case) VALIDATION_CASE="$1"; shift ;;
+ --validation-artifacts-dir) VALIDATION_ARTIFACTS_DIR="$1"; shift ;;
+ --candidate-manifest) VALIDATION_CANDIDATE_MANIFEST="$1"; shift ;;
+ --testing-commit) VALIDATION_TESTING_COMMIT="$1"; shift ;;
--dry-run) DRY_RUN=1 ;;
-y|--yes) ASSUME_YES=1 ;;
-h|--help) usage; exit 0 ;;
@@ -2814,6 +2930,27 @@ main() {
if [ "${all}" -eq 1 ]; then
SELECTED_TOPOLOGIES=("${TOPO_SINGLE_SINGLE}" "${TOPO_SINGLE_MULTI}" "${TOPO_MULTI_MULTI}")
fi
+ if [ -n "${VALIDATION_CASE}" ]; then
+ [ "${VALIDATION_CASE}" = "s3.basic-object-round-trip" ] || die "unknown validation case: ${VALIDATION_CASE}"
+ [ "${all}" -eq 0 ] || die "--validation-case cannot be combined with --all-topologies"
+ [ "${#SELECTED_TOPOLOGIES[@]}" -eq 0 ] || die "--validation-case chooses its own topology"
+ SELECTED_TOPOLOGIES=("${TOPO_SINGLE_SINGLE}")
+ [ "${KEEP_ENV}" -eq 0 ] || die "--validation-case requires environment teardown"
+ if [ "${DRY_RUN}" -eq 0 ]; then
+ [ -n "${VALIDATION_ARTIFACTS_DIR}" ] || die "--validation-case requires --validation-artifacts-dir"
+ [ -n "${VALIDATION_CANDIDATE_MANIFEST}" ] || die "--validation-case requires --candidate-manifest"
+ [ -n "${VALIDATION_TESTING_COMMIT}" ] || die "--validation-case requires --testing-commit"
+ [ ! -e "${VALIDATION_ARTIFACTS_DIR}" ] || die "validation artifacts directory already exists"
+ PACKAGE_URL="$(jq -er '.package_url' "${VALIDATION_CANDIDATE_MANIFEST}")" || die "candidate URL missing"
+ PACKAGE_SHA256="$(jq -er '.package_sha256' "${VALIDATION_CANDIDATE_MANIFEST}")" || die "candidate package SHA missing"
+ [[ "${PACKAGE_SHA256}" =~ ^[0-9a-f]{64}$ ]] || die "invalid candidate package SHA"
+ jq -e '.source_sha | test("^[0-9a-f]{40}$")' "${VALIDATION_CANDIDATE_MANIFEST}" >/dev/null \
+ || die "candidate source commit missing"
+ [ "${VALIDATION_TESTING_COMMIT}" = "$(git -C "${SCRIPT_DIR}" rev-parse HEAD)" ] \
+ || die "testing commit does not match this checkout"
+ mkdir -p "${VALIDATION_ARTIFACTS_DIR}"
+ fi
+ fi
[ "${#SELECTED_TOPOLOGIES[@]}" -gt 0 ] || die "no topology selected (--all-topologies / --topology)"
# aws may be installed under a user-local bin not present in the default PATH.
local _p
@@ -2834,21 +2971,47 @@ main() {
for topo in "${SELECTED_TOPOLOGIES[@]}"; do
log "== topology: ${topo} =="
CURRENT_TOPOLOGY="${topo}"
+ if [ -n "${VALIDATION_CASE}" ] && [ "${DRY_RUN}" -eq 0 ]; then
+ validation_write_result RUNNING "validation case started" PENDING
+ fi
env_setup "${topo}"
+ if [ -n "${VALIDATION_CASE}" ] && [ "${DRY_RUN}" -eq 0 ]; then
+ VALIDATION_BINARY_SHA256="$(printf 'sha256sum /usr/bin/rustfs\n' | run_remote "${NODES[0]}" | awk '{print $1}')"
+ [[ "${VALIDATION_BINARY_SHA256}" =~ ^[0-9a-f]{64}$ ]] || die "installed RustFS binary SHA-256 unavailable"
+ fi
PASS=0; FAIL=0; UNSUPPORTED=0; FAILED_TESTS=()
- run_all_tests
+ if [ -n "${VALIDATION_CASE}" ]; then
+ run_test "${VALIDATION_CASE}" "BasicObjectRoundTrip" test_validation_s3_basic_object_round_trip
+ else
+ run_all_tests
+ fi
results_summary
record_topology_result "${topo}"
+ local cleanup_status=PASS
if [ "${KEEP_ENV}" -eq 0 ] || [ "${topo}" != "${SELECTED_TOPOLOGIES[${#SELECTED_TOPOLOGIES[@]}-1]}" ]; then
- env_teardown "${topo}"
+ env_teardown "${topo}" || cleanup_status=FAIL
+ fi
+ if [ "${VALIDATION_OBJECT_CLEANUP_STATUS}" != PASS ]; then cleanup_status=FAIL; fi
+ if [ -n "${VALIDATION_CASE}" ] && [ "${DRY_RUN}" -eq 0 ]; then
+ validation_write_evidence
+ if [ "${FAIL}" -eq 0 ] && [ "${UNSUPPORTED}" -eq 0 ] && [ "${cleanup_status}" = PASS ]; then
+ validation_write_result PASS "" PASS
+ elif [ "${FAIL}" -eq 0 ] && [ "${UNSUPPORTED}" -eq 0 ]; then
+ validation_write_result INFRA_ERROR "environment teardown failed" FAIL
+ else
+ validation_write_result FAIL "case failed or unsupported; inspect evidence" "${cleanup_status}"
+ fi
fi
if [ "${FAIL}" -gt 0 ]; then
overall_fail=1
fi
+ if [ "${UNSUPPORTED}" -gt 0 ] || [ "${cleanup_status}" != PASS ]; then overall_fail=1; fi
done
final_summary
[ "${overall_fail}" -eq 0 ] || exit 1
log "all done"
}
-main "$@"
+if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
+ main "$@"
+fi
diff --git a/rustfs-security-test.sh b/rustfs-security-test.sh
index 4476e538..217dc89e 100644
--- a/rustfs-security-test.sh
+++ b/rustfs-security-test.sh
@@ -116,6 +116,13 @@ OIDC_RUN=0
SELECTED_TOPOLOGIES=()
ENDPOINT=""
VOLUMES=""
+VALIDATION_CASE=""
+VALIDATION_ARTIFACTS_DIR=""
+VALIDATION_CANDIDATE_MANIFEST=""
+VALIDATION_TESTING_COMMIT=""
+VALIDATION_BINARY_SHA256=""
+VALIDATION_OBJECT_CLEANUP_STATUS=PASS
+RUN_TOKEN=""
# ==================== Helpers ====================
@@ -244,6 +251,18 @@ RUSTFS_CONSOLE_ENABLE=${RUSTFS_CONSOLE_ENABLE}
RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL}
RUSTFS_OBS_LOG_DIRECTORY="${RUSTFS_OBS_LOG_DIRECTORY}"
EOF
+ if [ "${VALIDATION_CASE}" = oidc.login-and-session ]; then
+ cat </dev/null
}
+validation_iam_policy_body() {
+ local user="$1" secret="$2" bucket="$3" policy_name="$4" policy="$5" out
+ out="$(admin_user_add "${user}" "${secret}")"; assert_admin_ok 200 "add validation user" || return 1
+ out="$(admin_policy_add "${policy_name}" "${policy}")"; assert_admin_ok 200 "add validation policy" || return 1
+ out="$(admin_policy_attach "${user}" "${policy_name}")"; assert_admin_ok 200 "attach validation policy" || return 1
+ printf 'validation policy attached to %s\n' "${user}"
+ printf '%s\n' "${policy}" > "${TEST_TMP}/validation-iam-policy.json"
+ printf 'validation-payload-%s\n' "${RUN_TOKEN}" > "${TEST_TMP}/validation-iam-source.bin"
+ aws_run_as "${user}" "${secret}" -- s3api put-object --bucket "${bucket}" --key object.bin --body "${TEST_TMP}/validation-iam-source.bin"
+ assert_aws_ok "allowed PUT" || return 1
+ aws_run_as "${user}" "${secret}" -- s3api get-object --bucket "${bucket}" --key object.bin "${TEST_TMP}/validation-iam-read.bin"
+ assert_aws_ok "allowed GET" || return 1
+ cmp "${TEST_TMP}/validation-iam-source.bin" "${TEST_TMP}/validation-iam-read.bin" || return 1
+ aws_run_as "${user}" "${secret}" -- s3api delete-object --bucket "${bucket}" --key object.bin
+ assert_aws_fails "explicit deny DELETE" || return 1
+ [[ "${AWS_OUT}" == *AccessDenied* ]] || { echo "DELETE failure was not AccessDenied"; return 1; }
+ aws_run s3api get-object --bucket "${bucket}" --key object.bin "${TEST_TMP}/validation-iam-root-read.bin"
+ assert_aws_ok "denied DELETE preserved object" || return 1
+ cmp "${TEST_TMP}/validation-iam-source.bin" "${TEST_TMP}/validation-iam-root-read.bin" || return 1
+ jq -n --arg user "${user}" --arg bucket "${bucket}" \
+ --arg sha256 "$(sha256sum "${TEST_TMP}/validation-iam-source.bin" | awk '{print $1}')" \
+ '{principal:$user,bucket:$bucket,allowed:["PutObject","GetObject"],explicitly_denied:"DeleteObject",object_preserved:true,payload_sha256:$sha256}' \
+ > "${TEST_TMP}/validation-iam-observations.json"
+}
+
+test_validation_iam_policy_enforcement() {
+ local user="valiam-${RUN_TOKEN//-/}" secret
+ secret="$(openssl rand -hex 24)" || return 1
+ local bucket="val-iam-${RUN_TOKEN}" policy_name="valiam-${RUN_TOKEN//-/}" result=0 out
+ local policy
+ policy="$(jq -nc --arg bucket "${bucket}" '{Version:"2012-10-17",Statement:[
+ {Effect:"Allow",Action:["s3:PutObject","s3:GetObject"],Resource:["arn:aws:s3:::"+$bucket+"/*"]},
+ {Effect:"Deny",Action:["s3:DeleteObject"],Resource:["arn:aws:s3:::"+$bucket+"/*"]}
+ ]}')"
+ new_bucket "${bucket}" || return 1
+ validation_iam_policy_body "${user}" "${secret}" "${bucket}" "${policy_name}" "${policy}" || result=$?
+ if [ -f "${TEST_TMP}/validation-iam-source.bin" ]; then
+ aws_run s3api delete-object --bucket "${bucket}" --key object.bin
+ [ "${AWS_CODE}" -eq 0 ] || VALIDATION_OBJECT_CLEANUP_STATUS=FAIL
+ fi
+ out="$(admin_user_remove "${user}")"
+ assert_admin_ok 200 "remove validation user" || VALIDATION_OBJECT_CLEANUP_STATUS=FAIL
+ out="$(admin_api DELETE "/rustfs/admin/v3/remove-canned-policy?name=${policy_name}")"
+ assert_admin_ok 200 "remove validation policy" || VALIDATION_OBJECT_CLEANUP_STATUS=FAIL
+ aws_run s3api delete-bucket --bucket "${bucket}"
+ [ "${AWS_CODE}" -eq 0 ] || VALIDATION_OBJECT_CLEANUP_STATUS=FAIL
+ [ "${VALIDATION_OBJECT_CLEANUP_STATUS}" = PASS ] || result=1
+ return "${result}"
+}
+
case_iam_113_access_key_list_hides_secret() {
local user="sec113u" secret="sec113secret-xyz" out
out="$(admin_user_add "${user}" "${secret}")"; assert_admin_ok 200 "add-user"
@@ -944,6 +1042,10 @@ parse_args() {
--package-url) [ "$#" -ge 2 ] || die "--package-url requires a value"; PACKAGE_URL="$2"; shift ;;
--version) [ "$#" -ge 2 ] || die "--version requires a value"; RUSTFS_VERSION="$2"; shift ;;
--oidc-live) OIDC_LIVE=1 ;;
+ --validation-case) [ "$#" -ge 2 ] || die "--validation-case requires a value"; VALIDATION_CASE="$2"; shift ;;
+ --validation-artifacts-dir) [ "$#" -ge 2 ] || die "--validation-artifacts-dir requires a value"; VALIDATION_ARTIFACTS_DIR="$2"; shift ;;
+ --candidate-manifest) [ "$#" -ge 2 ] || die "--candidate-manifest requires a value"; VALIDATION_CANDIDATE_MANIFEST="$2"; shift ;;
+ --testing-commit) [ "$#" -ge 2 ] || die "--testing-commit requires a value"; VALIDATION_TESTING_COMMIT="$2"; shift ;;
--keep-env) KEEP_ENV=1 ;;
-y) ASSUME_YES=1 ;;
--dry-run) DRY_RUN=1 ;;
@@ -951,6 +1053,25 @@ parse_args() {
esac
shift
done
+ if [ -n "${VALIDATION_CASE}" ]; then
+ [ "${VALIDATION_CASE}" = iam.policy-enforcement ] || die "unknown validation case: ${VALIDATION_CASE}"
+ RUN_TOKEN="$(date -u +%y%m%d%H%M%S)-$$"
+ [ "${#SELECTED_TOPOLOGIES[@]}" -eq 0 ] || die "--validation-case chooses its topology"
+ [ "${KEEP_ENV}" -eq 0 ] || die "--validation-case requires environment teardown"
+ SELECTED_TOPOLOGIES=("${TOPO_SINGLE_SINGLE}")
+ if [ "${DRY_RUN}" -eq 0 ]; then
+ [ -n "${VALIDATION_ARTIFACTS_DIR}" ] || die "--validation-artifacts-dir required"
+ [ -n "${VALIDATION_CANDIDATE_MANIFEST}" ] || die "--candidate-manifest required"
+ [ -n "${VALIDATION_TESTING_COMMIT}" ] || die "--testing-commit required"
+ [ ! -e "${VALIDATION_ARTIFACTS_DIR}" ] || die "validation artifacts directory already exists"
+ PACKAGE_URL="$(jq -er '.package_url' "${VALIDATION_CANDIDATE_MANIFEST}")" || die "candidate URL missing"
+ PACKAGE_SHA256="$(jq -er '.package_sha256' "${VALIDATION_CANDIDATE_MANIFEST}")" || die "candidate package SHA missing"
+ [[ "${PACKAGE_SHA256}" =~ ^[0-9a-f]{64}$ ]] || die "invalid candidate package SHA"
+ jq -e '.source_sha | test("^[0-9a-f]{40}$")' "${VALIDATION_CANDIDATE_MANIFEST}" >/dev/null || die "candidate source commit missing"
+ [ "${VALIDATION_TESTING_COMMIT}" = "$(git -C "${SCRIPT_DIR}" rev-parse HEAD)" ] || die "testing commit does not match checkout"
+ mkdir -p "${VALIDATION_ARTIFACTS_DIR}"
+ fi
+ fi
[ "${#SELECTED_TOPOLOGIES[@]}" -gt 0 ] || die "select a topology (--topology ) or --all-topologies"
}
@@ -991,6 +1112,7 @@ main() {
need_cmd aws "aws cli"
need_cmd jq "jq"
need_cmd python3 "python3"
+ if [ -n "${VALIDATION_CASE}" ]; then need_cmd rc "rc client provenance"; fi
TEST_SCRIPT_REVISION="$(git -C "${SCRIPT_DIR}" rev-parse HEAD 2>/dev/null || printf unknown)"
log "test scripts commit: ${TEST_SCRIPT_REVISION}"
if [ "${DRY_RUN}" -eq 0 ]; then
@@ -1004,15 +1126,36 @@ main() {
for topo in "${SELECTED_TOPOLOGIES[@]}"; do
CURRENT_TOPOLOGY="${topo}"
log "===== topology ${topo} ====="
+ if [ -n "${VALIDATION_CASE}" ] && [ "${DRY_RUN}" -eq 0 ]; then
+ validation_write_result RUNNING "validation case started" PENDING
+ fi
env_setup "${topo}"
+ if [ -n "${VALIDATION_CASE}" ] && [ "${DRY_RUN}" -eq 0 ]; then
+ VALIDATION_BINARY_SHA256="$(printf 'sha256sum /usr/bin/rustfs\n' | run_remote "${NODES[0]}" | awk '{print $1}')"
+ [[ "${VALIDATION_BINARY_SHA256}" =~ ^[0-9a-f]{64}$ ]] || die "installed RustFS binary SHA-256 unavailable"
+ fi
if [ -z "${RUSTFS_VERSION_INFO}" ]; then
RUSTFS_VERSION_INFO="$(detect_rustfs_version)"
fi
CASE_RESULTS=()
- run_security_cases
+ if [ -n "${VALIDATION_CASE}" ]; then
+ run_test "${VALIDATION_CASE}" "PolicyEnforcement" test_validation_iam_policy_enforcement
+ else
+ run_security_cases
+ fi
record_topology_results "${topo}"
- if [ "${KEEP_ENV}" -eq 0 ]; then
- env_teardown "${topo}"
+ local cleanup_status=PASS
+ if [ "${KEEP_ENV}" -eq 0 ]; then env_teardown "${topo}" || cleanup_status=FAIL; fi
+ if [ "${VALIDATION_OBJECT_CLEANUP_STATUS}" != PASS ]; then cleanup_status=FAIL; fi
+ if [ -n "${VALIDATION_CASE}" ] && [ "${DRY_RUN}" -eq 0 ]; then
+ validation_write_evidence
+ if [ "${FAIL}" -eq 0 ] && [ "${cleanup_status}" = PASS ]; then
+ validation_write_result PASS "" PASS
+ elif [ "${FAIL}" -eq 0 ]; then
+ validation_write_result INFRA_ERROR "environment teardown failed" FAIL
+ else
+ validation_write_result FAIL "IAM assertion failed; inspect evidence" "${cleanup_status}"
+ fi
fi
done
@@ -1026,4 +1169,6 @@ main() {
echo "security suite: ${PASS} passed, ${FAIL} failed, ${SKIP} skipped"
}
-main "$@"
+if [ "${RUSTFS_SECURITY_SCRIPT_SOURCE_ONLY:-0}" != 1 ]; then
+ main "$@"
+fi
diff --git a/rustfs-storage-test.sh b/rustfs-storage-test.sh
index 057e6cd1..e111b7c4 100755
--- a/rustfs-storage-test.sh
+++ b/rustfs-storage-test.sh
@@ -1,7 +1,3 @@
- # Both keys carry a compressible extension: disk compression is decided
- # from the request key/Content-Type, and aws s3api sends no Content-Type on
- # put-object or copy-object, so an extension-less key is never compressed
- # (same include filter as MinIO) and the assertion below would be vacuous.
#!/usr/bin/env bash
#
# rustfs-storage-test.sh
@@ -32,6 +28,7 @@
# reported as UNSUPPORTED, not FAIL.
#
set -Eeuo pipefail
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# ==================== Configuration ====================
@@ -76,6 +73,7 @@ RUSTFS_OBS_LOG_DIRECTORY="/var/log/rustfs/"
TOPO_SINGLE_SINGLE="single-single"
TOPO_SINGLE_MULTI="single-multi"
TOPO_MULTI_MULTI="multi-multi"
+TOPO_ILM_2X4="ilm-2x4"
ENDPOINT_SINGLE_SINGLE="http://rustfs-node1:9000"
ENDPOINT_SINGLE_MULTI="http://rustfs-node1:9000"
@@ -84,6 +82,8 @@ ENDPOINT_MULTI_MULTI="http://rustfs-node1:9000"
VOLUMES_SINGLE_SINGLE="/data/rustfs1/mnmd"
VOLUMES_SINGLE_MULTI="/data/rustfs{1...4}/mnmd"
VOLUMES_MULTI_MULTI="http://rustfs-node{1...4}:9000/data/rustfs{1...4}/mnmd"
+VOLUMES_ILM_2X4="http://rustfs-node{1...2}:9000/data/rustfs{1...4}/mnmd"
+ILM_TIER_ENDPOINT="http://rustfs-node3:9000"
SERVICE_TIMEOUT=300
@@ -96,6 +96,12 @@ SELECTED_TOPOLOGIES=()
CURRENT_TOPOLOGY=""
ENDPOINT=""
VOLUMES=""
+VALIDATION_CASE=""
+VALIDATION_ARTIFACTS_DIR=""
+VALIDATION_CANDIDATE_MANIFEST=""
+VALIDATION_TESTING_COMMIT=""
+VALIDATION_BINARY_SHA256=""
+VALIDATION_OBJECT_CLEANUP_STATUS=PASS
TEST_TMP="$(mktemp -d "${TMPDIR:-/tmp}/rustfs-storage.XXXXXX")"
trap 'rm -rf "${TEST_TMP}"; rm -f "${ADMIN_API_CODE_FILE}"' EXIT
@@ -187,6 +193,14 @@ RUSTFS_CONSOLE_ENABLE=${RUSTFS_CONSOLE_ENABLE}
RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL}
RUSTFS_OBS_LOG_DIRECTORY="${RUSTFS_OBS_LOG_DIRECTORY}"
EOF
+ if [ "${VALIDATION_CASE}" = "scanner.usage-convergence" ] || [ "${VALIDATION_CASE}" = "scanner.status-readiness" ]; then
+ printf 'RUSTFS_SCANNER_ENABLED=true\nRUSTFS_SCANNER_CYCLE=5\nRUSTFS_SCANNER_START_DELAY_SECS=0\n'
+ elif [[ "${VALIDATION_CASE}" == ilm.* ]]; then
+ printf 'RUSTFS_SCANNER_ENABLED=true\nRUSTFS_SCANNER_CYCLE=2\nRUSTFS_SCANNER_START_DELAY_SECS=0\n'
+ printf 'RUSTFS_DATA_USAGE_UPDATE_DIR_CYCLES=1\nRUSTFS_ILM_DEBUG_DAY_SECS=10\nRUSTFS_ILM_PROCESS_TIME=1\n'
+ printf 'RUSTFS_API_STALE_UPLOADS_CLEANUP_INTERVAL=2s\n'
+ printf 'HTTP_PROXY=\nHTTPS_PROXY=\nALL_PROXY=\nhttp_proxy=\nhttps_proxy=\nall_proxy=\n'
+ fi
}
write_rustfs_config() {
@@ -272,6 +286,7 @@ resolve_topology() {
"${TOPO_SINGLE_SINGLE}") ENDPOINT="${ENDPOINT_SINGLE_SINGLE}"; VOLUMES="${VOLUMES_SINGLE_SINGLE}"; return 0 ;;
"${TOPO_SINGLE_MULTI}") ENDPOINT="${ENDPOINT_SINGLE_MULTI}"; VOLUMES="${VOLUMES_SINGLE_MULTI}"; return 0 ;;
"${TOPO_MULTI_MULTI}") ENDPOINT="${ENDPOINT_MULTI_MULTI}"; VOLUMES="${VOLUMES_MULTI_MULTI}"; return 0 ;;
+ "${TOPO_ILM_2X4}") ENDPOINT="${ENDPOINT_SINGLE_MULTI}"; VOLUMES="${VOLUMES_ILM_2X4}"; return 0 ;;
*) die "unknown topology: $1 (single-single|single-multi|multi-multi)" ;;
esac
}
@@ -279,6 +294,12 @@ resolve_topology() {
topology_nodes() {
case "$1" in
"${TOPO_SINGLE_SINGLE}"|"${TOPO_SINGLE_MULTI}") printf '%s' "${NODES[0]}" ;;
+ "${TOPO_ILM_2X4}")
+ if [ "${VALIDATION_CASE}" = ilm.transition-and-restore ]; then
+ printf '%s %s %s' "${NODES[0]}" "${NODES[1]}" "${NODES[2]}"
+ else
+ printf '%s %s' "${NODES[0]}" "${NODES[1]}"
+ fi ;;
*) printf '%s' "${NODES[*]}" ;;
esac
}
@@ -318,8 +339,13 @@ EOF
for node in "${nodes[@]}"; do printf '%s\n' "${script}" | run_remote "${node}"; done
for node in "${nodes[@]}"; do
- write_rustfs_config "${node}" "${VOLUMES}"
- ensure_volume_dirs "${node}" "${VOLUMES}"
+ if [ "${VALIDATION_CASE}" = ilm.transition-and-restore ] && [ "${node}" = "${NODES[2]}" ]; then
+ VALIDATION_CASE="" write_rustfs_config "${node}" "${VOLUMES_SINGLE_SINGLE}"
+ ensure_volume_dirs "${node}" "${VOLUMES_SINGLE_SINGLE}"
+ else
+ write_rustfs_config "${node}" "${VOLUMES}"
+ ensure_volume_dirs "${node}" "${VOLUMES}"
+ fi
done
start_and_wait_nodes "${nodes[@]}"
@@ -333,6 +359,16 @@ EOF
sleep 5; waited=$((waited+5))
done
[ "${waited}" -lt "${SERVICE_TIMEOUT}" ] || die "S3 endpoint ${ENDPOINT} not ready"
+ if [ "${VALIDATION_CASE}" = ilm.transition-and-restore ]; then
+ waited=0
+ while [ "${waited}" -lt "${SERVICE_TIMEOUT}" ]; do
+ if curl --noproxy '*' -fsS --max-time 5 "${ILM_TIER_ENDPOINT}/minio/health/live" >/dev/null 2>&1; then
+ log "ILM warm tier ready: ${ILM_TIER_ENDPOINT}"; break
+ fi
+ sleep 5; waited=$((waited+5))
+ done
+ [ "${waited}" -lt "${SERVICE_TIMEOUT}" ] || die "ILM warm tier ${ILM_TIER_ENDPOINT} not ready"
+ fi
rc alias set local "${ENDPOINT}" "${ACCESS_KEY}" "${SECRET_KEY}" >/dev/null 2>&1 \
|| die "rc alias set failed for ${ENDPOINT}"
@@ -498,6 +534,86 @@ run_test() {
fi
}
+validation_write_evidence() {
+ local evidence_dir="${VALIDATION_ARTIFACTS_DIR}/evidence/${VALIDATION_CASE}" item
+ mkdir -p "${evidence_dir}"
+ cp "${TEST_TMP}/test.out" "${evidence_dir}/test.log"
+ for item in $(validation_evidence_items); do
+ if [ -f "${TEST_TMP}/${item}" ]; then cp "${TEST_TMP}/${item}" "${evidence_dir}/${item}"; fi
+ done
+ if [[ "${VALIDATION_CASE}" == ilm.* ]] && [ -d "${TEST_TMP}/ilm-evidence" ]; then
+ cp -R "${TEST_TMP}/ilm-evidence" "${evidence_dir}/ilm"
+ fi
+}
+
+validation_evidence_items() {
+ case "${VALIDATION_CASE}" in
+ scanner.usage-convergence)
+ printf '%s\n' validation-fixture.json validation-list.json validation-baseline-usage.json validation-scanner.json validation-usage.json ;;
+ scanner.status-readiness)
+ printf '%s\n' validation-scanner-node1.json validation-scanner-node2.json validation-scanner-contract.json ;;
+ esac
+}
+
+validation_write_result() {
+ local verdict="$1" reason="$2" cleanup_status="$3" item
+ local profile=single-4disk
+ if [ "${VALIDATION_CASE}" = scanner.status-readiness ] || [[ "${VALIDATION_CASE}" == ilm.* ]]; then
+ profile=ilm-2x4
+ fi
+ local args=("${SCRIPT_DIR}/scripts/validation_result.py"
+ --case-id "${VALIDATION_CASE}" --profile "${profile}"
+ --result-file "${VALIDATION_ARTIFACTS_DIR}/cases/${VALIDATION_CASE}.json"
+ --verdict "${verdict}" --cleanup-status "${cleanup_status}")
+ if [ -n "${reason}" ]; then args+=(--reason "${reason}"); fi
+ if [ "${verdict}" != RUNNING ]; then
+ args+=(--candidate-manifest "${VALIDATION_CANDIDATE_MANIFEST}"
+ --rc-bin "$(command -v rc)" --testing-commit "${VALIDATION_TESTING_COMMIT}"
+ --binary-sha256 "${VALIDATION_BINARY_SHA256}"
+ --evidence "evidence/${VALIDATION_CASE}/test.log")
+ fi
+ if [ "${verdict}" = PASS ]; then
+ for item in $(validation_evidence_items); do
+ args+=(--evidence "evidence/${VALIDATION_CASE}/${item}")
+ done
+ if [[ "${VALIDATION_CASE}" == ilm.* ]]; then
+ local evidence_root="${VALIDATION_ARTIFACTS_DIR}/evidence/${VALIDATION_CASE}"
+ local evidence_file
+ while IFS= read -r -d '' evidence_file; do
+ args+=(--evidence "${evidence_file#"${VALIDATION_ARTIFACTS_DIR}/"}")
+ done < <(find "${evidence_root}/ilm" -type f -print0 | sort -z)
+ fi
+ fi
+ python3 "${args[@]}"
+}
+
+test_validation_ilm_case() {
+ local harness_dir="${TEST_TMP}/ilm-evidence" harness_rc=0
+ local -a ilm_args=()
+ if [ "${VALIDATION_CASE}" = ilm.restart-persistence ]; then
+ ilm_args+=(--restart-hook "$(python3 -c 'import json,sys; print(json.dumps(sys.argv[1:]))' \
+ bash "${SCRIPT_DIR}/scripts/validation_ilm/restart_cluster.sh" "${NODES[0]}" "${NODES[1]}")")
+ fi
+ if [ "${VALIDATION_CASE}" = ilm.transition-and-restore ]; then
+ ilm_args+=(--tier-endpoint "${ILM_TIER_ENDPOINT}"
+ --tier-access-key "${ACCESS_KEY}" --tier-secret-key "${SECRET_KEY}")
+ fi
+ python3 -B "${SCRIPT_DIR}/scripts/validation_ilm/ilm_behavior.py" \
+ --case "${VALIDATION_CASE}" \
+ --endpoint "${ENDPOINT}" \
+ --scanner-endpoint "http://rustfs-node1:9000" \
+ --scanner-endpoint "http://rustfs-node2:9000" \
+ --access-key "${ACCESS_KEY}" --secret-key "${SECRET_KEY}" \
+ --run-id "${VALIDATION_CASE}-$(date +%s)-$$" \
+ --evidence-dir "${harness_dir}" \
+ --rc-bin "$(command -v rc)" \
+ "${ilm_args[@]}" \
+ --cleanup-on-failure || harness_rc=$?
+ python3 -B "${SCRIPT_DIR}/scripts/validation_ilm/verify_result.py" \
+ "${VALIDATION_CASE}" "${harness_dir}" || return 1
+ [ "${harness_rc}" -eq 0 ]
+}
+
rc_run() {
RC_OUT="$(rc "$@" 2>&1)"; RC_CODE=$?
}
@@ -686,6 +802,125 @@ test_stor134() { # admin usage APIs must respond promptly and converge to actual
return 1
}
+validation_scanner_usage_matches() {
+ local scanner_file="$1" usage_file="$2" bucket="$3" baseline="$4" bytes="$5" uploaded="$6"
+ jq -e --argjson uploaded "${uploaded}" \
+ '.enabled == true and .freshness.state == "fresh" and .metrics.last_cycle_result == "success" and .metrics.leader_lock_held_by_this_process == true and .cycle_schedule.execution_role == "leader" and .freshness.last_cycle_end_unix_secs >= $uploaded' \
+ "${scanner_file}" >/dev/null 2>&1 && \
+ jq -e --arg bucket "${bucket}" --argjson baseline "${baseline}" --argjson bytes "${bytes}" \
+ '.usage_snapshot_complete == true and .usage_snapshot_converged != false and .scanner_cycle > $baseline and .buckets_usage[$bucket].objects_count == 20 and .buckets_usage[$bucket].size == $bytes' \
+ "${usage_file}" >/dev/null 2>&1
+}
+
+test_validation_scanner_usage_convergence() {
+ local bucket="val-usage-${RANDOM}-$$" i key size sha expected_size=0 baseline_cycle=0
+ local upload_end deadline case_rc=0 cleanup_rc=0
+ local fixture_tsv="${TEST_TMP}/validation-fixture.tsv"
+ VALIDATION_OBJECT_CLEANUP_STATUS=PASS
+ : > "${fixture_tsv}"
+ new_bucket "${bucket}" || return 1
+ (
+ admin_api GET "/rustfs/admin/v3/datausageinfo" "" "" > "${TEST_TMP}/validation-baseline-usage.json"
+ [ "$(admin_api_code)" = 200 ] || { echo "baseline usage API failed"; exit 1; }
+ baseline_cycle="$(jq -er '.scanner_cycle // 0 | numbers' "${TEST_TMP}/validation-baseline-usage.json")" || exit 1
+ for i in $(seq 1 20); do
+ key="obj-${i}"
+ size=$((1024 + i))
+ make_test_file "${TEST_TMP}/${key}" "${size}"
+ sha="$(sha256sum "${TEST_TMP}/${key}" | awk '{print $1}')"
+ printf '%s\t%s\t%s\n' "${key}" "${size}" "${sha}" >> "${fixture_tsv}"
+ expected_size=$((expected_size + size))
+ aws_run s3api put-object --bucket "${bucket}" --key "${key}" --body "${TEST_TMP}/${key}"
+ assert_aws_ok || exit 1
+ done
+ jq -Rn --arg bucket "${bucket}" '[inputs | split("\t") | {key:.[0], size:(.[1]|tonumber), sha256:.[2]}] | {bucket:$bucket, objects:., expected_count:length, expected_size:(map(.size)|add)}' \
+ < "${fixture_tsv}" > "${TEST_TMP}/validation-fixture.json"
+ upload_end="$(date -u +%s)"
+ aws_run s3api list-objects-v2 --bucket "${bucket}"
+ assert_aws_ok || exit 1
+ printf '%s\n' "${AWS_OUT}" > "${TEST_TMP}/validation-list.json"
+ jq -e --slurpfile fixture "${TEST_TMP}/validation-fixture.json" \
+ '(.Contents // [] | map({key:.Key,size:.Size}) | sort_by(.key)) == ($fixture[0].objects | map({key,size}) | sort_by(.key))' \
+ "${TEST_TMP}/validation-list.json" >/dev/null || { echo "S3 fixture list differs from uploaded manifest"; exit 1; }
+
+ deadline=$((SECONDS + 300))
+ while [ "${SECONDS}" -lt "${deadline}" ]; do
+ admin_api GET "/rustfs/admin/v3/scanner/status" "" "" > "${TEST_TMP}/validation-scanner.json"
+ local scanner_code usage_code
+ scanner_code="$(admin_api_code)"
+ admin_api GET "/rustfs/admin/v3/datausageinfo" "" "" > "${TEST_TMP}/validation-usage.json"
+ usage_code="$(admin_api_code)"
+ if [ "${scanner_code}" = 200 ] && [ "${usage_code}" = 200 ] && \
+ validation_scanner_usage_matches "${TEST_TMP}/validation-scanner.json" "${TEST_TMP}/validation-usage.json" \
+ "${bucket}" "${baseline_cycle}" "${expected_size}" "${upload_end}"; then
+ echo "scanner completed after upload; usage converged to 20 objects and ${expected_size} bytes"
+ exit 0
+ fi
+ sleep 5
+ done
+ echo "scanner or usage did not converge within 300 seconds"
+ exit 1
+ ) || case_rc=$?
+
+ for i in $(seq 1 20); do
+ aws_run s3api delete-object --bucket "${bucket}" --key "obj-${i}"
+ assert_aws_ok || cleanup_rc=1
+ done
+ aws_run s3api delete-bucket --bucket "${bucket}"
+ assert_aws_ok || cleanup_rc=1
+ if [ "${cleanup_rc}" -ne 0 ]; then
+ VALIDATION_OBJECT_CLEANUP_STATUS=FAIL
+ echo "validation bucket cleanup failed"
+ fi
+ [ "${case_rc}" -eq 0 ] && [ "${cleanup_rc}" -eq 0 ]
+}
+
+validation_scanner_status_ready() {
+ local node1="$1" node2="$2" started_at="$3"
+ jq -se --argjson started "${started_at}" '
+ length == 2 and
+ all(.[]; .enabled == true and
+ .runtime_config.cycle_interval_seconds == {value:5,source:"env"} and
+ .runtime_config.start_delay_seconds == {value:0,source:"env"}) and
+ ([.[] | select(.metrics.leader_lock_held_by_this_process == true)] | length) == 1 and
+ any(.[]; .metrics.leader_lock_held_by_this_process == true and
+ .metrics.leader_lock_state == "acquired" and
+ .metrics.last_cycle_result == "success" and
+ .metrics.last_cycle_end_unix_secs >= $started and
+ .freshness.state == "fresh" and
+ .cycle_schedule.execution_role == "leader")' \
+ "${node1}" "${node2}" >/dev/null
+}
+
+test_validation_scanner_status_readiness() {
+ local started_at deadline status_code node endpoint index
+ started_at="$(date -u +%s)"
+ deadline=$((SECONDS + 120))
+ while [ "${SECONDS}" -lt "${deadline}" ]; do
+ status_code=0
+ for index in 1 2; do
+ node="${NODES[index-1]}"
+ endpoint="http://rustfs-node${index}:9000"
+ (API_ENDPOINT="${endpoint}"; admin_api GET /rustfs/admin/v3/scanner/status "" "" > "${TEST_TMP}/validation-scanner-node${index}.json"
+ [ "$(admin_api_code)" = 200 ]) || status_code=1
+ [ "${status_code}" -eq 0 ] || { echo "scanner status unavailable on ${node}"; break; }
+ done
+ if [ "${status_code}" -eq 0 ] && validation_scanner_status_ready \
+ "${TEST_TMP}/validation-scanner-node1.json" "${TEST_TMP}/validation-scanner-node2.json" "${started_at}"; then
+ jq -n --argjson started "${started_at}" --slurpfile node1 "${TEST_TMP}/validation-scanner-node1.json" \
+ --slurpfile node2 "${TEST_TMP}/validation-scanner-node2.json" \
+ '{started_at_unix:$started,cycle_interval_seconds:5,start_delay_seconds:0,
+ leader:([($node1[0],$node2[0]) | select(.metrics.leader_lock_held_by_this_process == true) |
+ {last_cycle_end_unix_secs:.metrics.last_cycle_end_unix_secs,last_cycle_result:.metrics.last_cycle_result}] | first)}' \
+ > "${TEST_TMP}/validation-scanner-contract.json"
+ return 0
+ fi
+ sleep 5
+ done
+ echo "two-node scanner runtime or successful new leader cycle did not meet the 120-second deadline"
+ return 1
+}
+
test_stor101() {
new_bucket "stor101-b1" || return 1
make_compressible_file "${TEST_TMP}/stor101-v" 1048576
@@ -1080,6 +1315,10 @@ Options:
--ssh-user SSH user for the test nodes
--ssh-port SSH port for the test nodes
--log-file append all output to a log file
+ --validation-case ID run a canonical scanner case on its required topology
+ --validation-artifacts-dir DIR write canonical result and evidence
+ --candidate-manifest FILE immutable candidate manifest
+ --testing-commit SHA auto-testing commit used for validation results
--dry-run preview only
-y, --yes skip confirmation
-h, --help show this help
@@ -1101,6 +1340,10 @@ main() {
--ssh-user) SSH_USER="$1"; shift ;;
--ssh-port) SSH_PORT="$1"; shift ;;
--log-file) LOG_FILE="$1"; shift ;;
+ --validation-case) VALIDATION_CASE="$1"; shift ;;
+ --validation-artifacts-dir) VALIDATION_ARTIFACTS_DIR="$1"; shift ;;
+ --candidate-manifest) VALIDATION_CANDIDATE_MANIFEST="$1"; shift ;;
+ --testing-commit) VALIDATION_TESTING_COMMIT="$1"; shift ;;
--dry-run) DRY_RUN=1 ;;
-y|--yes) ASSUME_YES=1 ;;
-h|--help) usage; exit 0 ;;
@@ -1118,6 +1361,39 @@ main() {
if [ "${all}" -eq 1 ]; then
SELECTED_TOPOLOGIES=("${TOPO_SINGLE_SINGLE}" "${TOPO_SINGLE_MULTI}" "${TOPO_MULTI_MULTI}")
fi
+ if [ -n "${VALIDATION_CASE}" ]; then
+ case "${VALIDATION_CASE}" in
+ scanner.usage-convergence|scanner.status-readiness) ;;
+ ilm.configuration-roundtrip|ilm.invalid-configuration|ilm.current-expiration|ilm.tag-filter-expiration|ilm.disabled-rule|ilm.rule-removal|ilm.versioned-current-expiration|ilm.noncurrent-expiration|ilm.newer-noncurrent-retention|ilm.delete-all-versions|ilm.delete-marker-expiration|ilm.multipart-expiration|ilm.restart-persistence|ilm.transition-and-restore) ;;
+ *) die "unknown validation case: ${VALIDATION_CASE}" ;;
+ esac
+ [ "${all}" -eq 0 ] || die "--validation-case cannot be combined with --all-topologies"
+ [ "${#SELECTED_TOPOLOGIES[@]}" -eq 0 ] || die "--validation-case chooses its own topology"
+ [ "${KEEP_ENV}" -eq 0 ] || die "--validation-case requires environment teardown"
+ if [ "${VALIDATION_CASE}" = scanner.status-readiness ] || [[ "${VALIDATION_CASE}" == ilm.* ]]; then
+ SELECTED_TOPOLOGIES=("${TOPO_ILM_2X4}")
+ [ "${#NODES[@]}" -ge 2 ] || die "scanner.status-readiness needs two test nodes"
+ if [ "${VALIDATION_CASE}" = ilm.transition-and-restore ]; then
+ [ "${#NODES[@]}" -ge 3 ] || die "ilm.transition-and-restore needs three test nodes"
+ fi
+ else
+ SELECTED_TOPOLOGIES=("${TOPO_SINGLE_MULTI}")
+ fi
+ if [ "${DRY_RUN}" -eq 0 ]; then
+ [ -n "${VALIDATION_ARTIFACTS_DIR}" ] || die "--validation-case requires --validation-artifacts-dir"
+ [ -n "${VALIDATION_CANDIDATE_MANIFEST}" ] || die "--validation-case requires --candidate-manifest"
+ [ -n "${VALIDATION_TESTING_COMMIT}" ] || die "--validation-case requires --testing-commit"
+ [ ! -e "${VALIDATION_ARTIFACTS_DIR}" ] || die "validation artifacts directory already exists"
+ PACKAGE_URL="$(jq -er '.package_url' "${VALIDATION_CANDIDATE_MANIFEST}")" || die "candidate URL missing"
+ PACKAGE_SHA256="$(jq -er '.package_sha256' "${VALIDATION_CANDIDATE_MANIFEST}")" || die "candidate package SHA missing"
+ [[ "${PACKAGE_SHA256}" =~ ^[0-9a-f]{64}$ ]] || die "invalid candidate package SHA"
+ jq -e '.source_sha | test("^[0-9a-f]{40}$")' "${VALIDATION_CANDIDATE_MANIFEST}" >/dev/null \
+ || die "candidate source commit missing"
+ [ "${VALIDATION_TESTING_COMMIT}" = "$(git -C "${SCRIPT_DIR}" rev-parse HEAD)" ] \
+ || die "testing commit does not match this checkout"
+ mkdir -p "${VALIDATION_ARTIFACTS_DIR}"
+ fi
+ fi
[ "${#SELECTED_TOPOLOGIES[@]}" -gt 0 ] || die "no topology selected (--all-topologies / --topology)"
local _p
for _p in /root/.local/bin /home/*/.local/bin /usr/local/bin; do
@@ -1130,26 +1406,63 @@ main() {
need_cmd jq "JSON processor"
need_cmd curl "HTTP client"
need_cmd openssl "OpenSSL (SigV4 signing)"
+ if [ -n "${VALIDATION_CASE}" ]; then need_cmd python3 "validation result writer"; fi
+ if [[ "${VALIDATION_CASE}" == ilm.* ]] && [ "${DRY_RUN}" -eq 0 ]; then
+ python3 -c 'import boto3, botocore' || die "ILM validation requires boto3 and botocore"
+ fi
log "nodes: ${NODES[*]} package: $(resolve_package_url)"
local overall_fail=0 topo
for topo in "${SELECTED_TOPOLOGIES[@]}"; do
log "== topology: ${topo} =="
+ if [ -n "${VALIDATION_CASE}" ] && [ "${DRY_RUN}" -eq 0 ]; then
+ validation_write_result RUNNING "validation case started" PENDING
+ fi
env_setup "${topo}"
CURRENT_TOPOLOGY="${topo}"
+ if [ -n "${VALIDATION_CASE}" ] && [ "${DRY_RUN}" -eq 0 ]; then
+ VALIDATION_BINARY_SHA256="$(printf 'sha256sum /usr/bin/rustfs\n' | run_remote "${NODES[0]}" | awk '{print $1}')"
+ [[ "${VALIDATION_BINARY_SHA256}" =~ ^[0-9a-f]{64}$ ]] || die "installed RustFS binary SHA-256 unavailable"
+ fi
PASS=0; FAIL=0; UNSUPPORTED=0; FAILED_TESTS=()
- run_all_tests
+ if [ -n "${VALIDATION_CASE}" ]; then
+ case "${VALIDATION_CASE}" in
+ scanner.usage-convergence) run_test "${VALIDATION_CASE}" "UsageConvergence" test_validation_scanner_usage_convergence ;;
+ scanner.status-readiness) run_test "${VALIDATION_CASE}" "StatusReadiness" test_validation_scanner_status_readiness ;;
+ ilm.*) run_test "${VALIDATION_CASE}" "ILMBehavior" test_validation_ilm_case ;;
+ esac
+ else
+ run_all_tests
+ fi
results_summary
record_topology_result "${topo}"
+ local cleanup_status=PASS
if [ "${KEEP_ENV}" -eq 0 ] || [ "${topo}" != "${SELECTED_TOPOLOGIES[${#SELECTED_TOPOLOGIES[@]}-1]}" ]; then
- env_teardown "${topo}"
+ env_teardown "${topo}" || cleanup_status=FAIL
+ fi
+ if [ "${VALIDATION_OBJECT_CLEANUP_STATUS}" != PASS ]; then cleanup_status=FAIL; fi
+ if [ -n "${VALIDATION_CASE}" ] && [ "${DRY_RUN}" -eq 0 ]; then
+ validation_write_evidence
+ if [ "${FAIL}" -eq 0 ] && [ "${UNSUPPORTED}" -eq 0 ] && [ "${cleanup_status}" = PASS ]; then
+ validation_write_result PASS "" PASS
+ elif [ "${FAIL}" -eq 0 ] && [ "${UNSUPPORTED}" -eq 0 ]; then
+ validation_write_result INFRA_ERROR "environment teardown failed" FAIL
+ elif [[ "${VALIDATION_CASE}" == ilm.* ]] && [ -f "${TEST_TMP}/ilm-evidence/result.json" ] && \
+ jq -e '.status.state == "infra_error"' "${TEST_TMP}/ilm-evidence/result.json" >/dev/null; then
+ validation_write_result INFRA_ERROR "ILM harness preflight or execution infrastructure failed; inspect evidence" "${cleanup_status}"
+ else
+ validation_write_result FAIL "case failed or unsupported; inspect evidence" "${cleanup_status}"
+ fi
fi
if [ "${FAIL}" -gt 0 ]; then
overall_fail=1
fi
+ if [ "${UNSUPPORTED}" -gt 0 ] || [ "${cleanup_status}" != PASS ]; then overall_fail=1; fi
done
final_summary
[ "${overall_fail}" -eq 0 ] || exit 1
log "all done"
}
-main "$@"
+if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
+ main "$@"
+fi
diff --git a/rustfs-table-test.sh b/rustfs-table-test.sh
index 55259385..1e15747b 100755
--- a/rustfs-table-test.sh
+++ b/rustfs-table-test.sh
@@ -150,7 +150,7 @@ remote() { ssh -o BatchMode=yes -o ConnectTimeout=10 -p "${SSH_PORT}" "${SSH_USE
setup_environment() {
local v
for v in "${NODES[@]}"; do
- remote "${v}" "curl -fsSL --retry 3 -o /tmp/rustfs-table.deb '${PACKAGE_URL}' && ${SUDO[*]} dpkg -i --force-confold /tmp/rustfs-table.deb >/dev/null 2>&1 && ${SUDO[*]} mkdir -p /data/rustfs{1..4}/mnmd && ${SUDO[*]} chown -R rustfs:rustfs /data/rustfs{1..4}" || die "install failed on ${v}"
+ remote "${v}" "curl -fsSL --retry 3 -o /tmp/rustfs-table.deb '${PACKAGE_URL}' && ${SUDO[*]} dpkg -i --force-confold /tmp/rustfs-table.deb >/dev/null 2>&1 && ${SUDO[*]} mkdir -p /data/rustfs{1..4}/mnmd && ${SUDO[*]} chown -R rustfs:rustfs /data/rustfs{1..4}/mnmd" || die "install failed on ${v}"
done
write_node_config() {
remote "$1" "${SUDO[*]} tee /etc/default/rustfs >/dev/null <<'CFG'
diff --git a/rustfs-tier-test.sh b/rustfs-tier-test.sh
index 0d17bdbc..a7769b24 100755
--- a/rustfs-tier-test.sh
+++ b/rustfs-tier-test.sh
@@ -20,6 +20,7 @@
# ./rustfs-tier-test.sh --topology single-single -y --version 1.0.0-rc.4-preview.1
#
set -Eeuo pipefail
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Per-run temp dir (shared runners may leave stale /tmp files owned by other users).
# Tests may inject an isolated directory while sourcing this file.
@@ -237,6 +238,10 @@ suite_exit() {
if [ "${CASE_ACTIVE:-0}" -eq 1 ] && declare -F case_cleanup >/dev/null 2>&1; then
case_cleanup "signal-or-exit" >> "${TEST_TMP}/test.out" 2>&1 || true
fi
+ if [ "${VALIDATION_ENV_ACTIVE:-0}" -eq 1 ]; then
+ VALIDATION_ENV_ACTIVE=0
+ env_teardown "${CURRENT_TOPOLOGY}" || exit_code=1
+ fi
if finish_log_capture "${exit_code}"; then
exit_code=0
else
@@ -365,6 +370,12 @@ RESULTS_DIR=""
RESULT_LOG_DIR=""
PROVENANCE_FILE=""
RESIDUE_FILE=""
+VALIDATION_CASE=""
+VALIDATION_ARTIFACTS_DIR=""
+VALIDATION_CANDIDATE_MANIFEST=""
+VALIDATION_TESTING_COMMIT=""
+VALIDATION_BINARY_SHA256=""
+VALIDATION_ENV_ACTIVE=0
# ==================== Helpers ====================
@@ -673,6 +684,10 @@ wait_service_active() {
rustfs_config_body() {
local volumes="$1"
+ local mqtt_enabled=true
+ if [ "${VALIDATION_CASE}" = events.delivery ] || [ "${VALIDATION_CASE}" = audit.request-correlation ]; then
+ mqtt_enabled=false
+ fi
cat < HTTP ${mcode}: ${mbody}"; die "failed to enable notify/audit modules"; }
- wait_targets_ready
+ if [ "${VALIDATION_CASE}" != tiering.transition-and-restore ]; then
+ receiver_start
+ if [ "${VALIDATION_CASE}" != events.delivery ] && [ "${VALIDATION_CASE}" != audit.request-correlation ]; then
+ mqtt_start
+ fi
+ # EVT and AUD use the API-managed module switches.
+ local mbody mcode
+ local switches='{"notify_enabled":true,"audit_enabled":true}'
+ [ "${VALIDATION_CASE}" = events.delivery ] && switches='{"notify_enabled":true,"audit_enabled":false}'
+ [ "${VALIDATION_CASE}" = audit.request-correlation ] && switches='{"notify_enabled":false,"audit_enabled":true}'
+ mbody="$(admin_api PUT /rustfs/admin/v3/module-switches "" "${switches}")"; mcode="$(admin_api_code)"
+ [ "${mcode}" = "200" ] || { echo "enable modules -> HTTP ${mcode}: ${mbody}"; die "failed to enable notify/audit modules"; }
+ [ "${VALIDATION_CASE}" = audit.request-correlation ] || wait_targets_ready
+ fi
log "rc aliases local/target; warm bucket ${TIER_BUCKET} ready"
fi
}
@@ -1511,6 +1532,48 @@ run_test() {
write_case_result "${status}" "${rc}" "$([ "${cleanup_rc}" -eq 0 ] && printf PASS || printf FAIL)" "$((SECONDS-started))" "${cleanup_detail}"
}
+validation_write_evidence() {
+ local evidence_dir="${VALIDATION_ARTIFACTS_DIR}/evidence/${VALIDATION_CASE}" item
+ mkdir -p "${evidence_dir}"
+ cp "${TEST_TMP}/test.out" "${evidence_dir}/test.log"
+ cp "${RESULTS_DIR}/${CURRENT_TOPOLOGY}--${VALIDATION_CASE}.json" "${evidence_dir}/suite-result.json"
+ for item in $(validation_evidence_items); do
+ if [ -f "${TEST_TMP}/${item}" ]; then cp "${TEST_TMP}/${item}" "${evidence_dir}/${item}"; fi
+ done
+}
+
+validation_evidence_items() {
+ case "${VALIDATION_CASE}" in
+ tiering.transition-and-restore)
+ printf '%s\n' validation-transition.json validation-head.json validation-restore-head.json validation-object.json ;;
+ events.delivery) printf '%s\n' validation-events.json ;;
+ audit.request-correlation) printf '%s\n' validation-audit.json ;;
+ esac
+}
+
+validation_write_result() {
+ local verdict="$1" reason="$2" cleanup_status="$3"
+ local args=("${SCRIPT_DIR}/scripts/validation_result.py"
+ --case-id "${VALIDATION_CASE}" --profile lifecycle-b
+ --result-file "${VALIDATION_ARTIFACTS_DIR}/cases/${VALIDATION_CASE}.json"
+ --verdict "${verdict}" --cleanup-status "${cleanup_status}")
+ if [ -n "${reason}" ]; then args+=(--reason "${reason}"); fi
+ if [ "${verdict}" != RUNNING ]; then
+ args+=(--candidate-manifest "${VALIDATION_CANDIDATE_MANIFEST}"
+ --rc-bin "${RC_BIN}" --testing-commit "${VALIDATION_TESTING_COMMIT}"
+ --binary-sha256 "${VALIDATION_BINARY_SHA256}"
+ --evidence "evidence/${VALIDATION_CASE}/test.log"
+ --evidence "evidence/${VALIDATION_CASE}/suite-result.json")
+ fi
+ if [ "${verdict}" = PASS ]; then
+ local item
+ for item in $(validation_evidence_items); do
+ args+=(--evidence "evidence/${VALIDATION_CASE}/${item}")
+ done
+ fi
+ python3 "${args[@]}"
+}
+
rc_run() {
if RC_OUT="$(run_bounded "${COMMAND_TIMEOUT}" "${RC_BIN}" "$@" 2>&1)"; then RC_CODE=0; else RC_CODE=$?; fi
return "${RC_CODE}"
@@ -1956,6 +2019,95 @@ final_summary() {
# ==================== Tier / Event / Audit test cases ====================
+test_validation_tiering_transition_and_restore() {
+ local bucket="val-tier-${RUN_TOKEN}" source="${TEST_TMP}/validation-source.bin"
+ local downloaded="${TEST_TMP}/validation-downloaded.bin" key=round-trip.bin
+ local transition job waited=0 source_sha downloaded_sha transitioned_sha
+ new_bucket "${bucket}" || return 1
+ add_tier rustfs "${TIER_NAME}" "${TARGET_ENDPOINT}" "${TIER_BUCKET}" "${ACCESS_KEY}" "${SECRET_KEY}"
+ assert_rc_ok || return 1
+ rc_run ilm rule add "local/${bucket}" --storage-class "${TIER_NAME}" --transition-days 0
+ assert_rc_ok || return 1
+ make_test_file "${source}" 1048576
+ source_sha="$(sha256_file "${source}")"
+ [[ "${source_sha}" =~ ^[0-9a-f]{64}$ ]] || { echo "source checksum unavailable"; return 1; }
+ aws_run s3api put-object --bucket "${bucket}" --key "${key}" --body "${source}"
+ assert_aws_ok || return 1
+ transition="$(run_transition "${bucket}")" || return 1
+ printf '%s\n' "${transition}" > "${TEST_TMP}/validation-transition.json"
+ job="$(printf '%s' "${transition}" | jq -r '.job_id // empty')"
+ if [ -n "${job}" ]; then wait_transition_job "${job}" >/dev/null || return 1; fi
+ while [ "${waited}" -lt 120 ]; do
+ aws_run s3api head-object --bucket "${bucket}" --key "${key}"
+ if [ "${AWS_CODE}" -eq 0 ] && printf '%s' "${AWS_OUT}" | jq -e --arg tier "${TIER_NAME}" \
+ '.StorageClass == $tier' >/dev/null 2>&1; then break; fi
+ sleep 3; waited=$((waited+3))
+ done
+ [ "${waited}" -lt 120 ] || { echo "object did not reach warm tier: ${AWS_OUT}"; return 1; }
+ printf '%s\n' "${AWS_OUT}" > "${TEST_TMP}/validation-head.json"
+ aws_run s3api get-object --bucket "${bucket}" --key "${key}" "${downloaded}"
+ assert_aws_ok || return 1
+ downloaded_sha="$(sha256_file "${downloaded}")"
+ [ "${downloaded_sha}" = "${source_sha}" ] || { echo "transitioned object checksum mismatch"; return 1; }
+ transitioned_sha="${downloaded_sha}"
+ aws_run s3api restore-object --bucket "${bucket}" --key "${key}" --restore-request '{"Days":1}'
+ assert_aws_ok || return 1
+ waited=0
+ while [ "${waited}" -lt 90 ]; do
+ aws_run s3api head-object --bucket "${bucket}" --key "${key}"
+ if [ "${AWS_CODE}" -eq 0 ] && printf '%s' "${AWS_OUT}" | jq -e \
+ '(.Restore // ."x-amz-restore" // "") | startswith("ongoing-request=\"false\"")' \
+ >/dev/null 2>&1; then break; fi
+ sleep 3; waited=$((waited+3))
+ done
+ [ "${waited}" -lt 90 ] || { echo "restore state did not appear: ${AWS_OUT}"; return 1; }
+ printf '%s\n' "${AWS_OUT}" > "${TEST_TMP}/validation-restore-head.json"
+ aws_run s3api get-object --bucket "${bucket}" --key "${key}" "${downloaded}"
+ assert_aws_ok || return 1
+ downloaded_sha="$(sha256_file "${downloaded}")"
+ [ "${downloaded_sha}" = "${source_sha}" ] || { echo "restored object checksum mismatch"; return 1; }
+ jq -nc --arg bucket "${bucket}" --arg key "${key}" --arg source_sha256 "${source_sha}" \
+ --arg transitioned_sha256 "${transitioned_sha}" --arg restored_sha256 "${downloaded_sha}" \
+ --arg tier "${TIER_NAME}" \
+ '{bucket:$bucket,key:$key,source_sha256:$source_sha256,transitioned_sha256:$transitioned_sha256,
+ restored_sha256:$restored_sha256,tier:$tier,restore_complete:true}' \
+ > "${TEST_TMP}/validation-object.json"
+}
+
+test_validation_events_delivery() {
+ local bucket="val-events-${RUN_TOKEN}" key="round-trip.bin" arn
+ new_bucket "${bucket}" || return 1
+ arn="$(get_first_arn)"
+ [ -n "${arn}" ] || { echo "notification target ARN missing"; return 1; }
+ rc_run event add "local/${bucket}" "${arn}" --event s3:ObjectCreated:Put
+ assert_rc_ok || return 1
+ events_reset
+ make_test_file "${TEST_TMP}/validation-event-source.bin" 65536
+ aws_run s3api put-object --bucket "${bucket}" --key "${key}" --body "${TEST_TMP}/validation-event-source.bin"
+ assert_aws_ok || return 1
+ wait_notify 1 "${bucket}" || return 1
+ sleep 15
+ python3 "${SCRIPT_DIR}/scripts/validation_webhooks.py" events \
+ --input "${NOTIFY_EVENTS}" --bucket "${bucket}" --key "${key}" \
+ --output "${TEST_TMP}/validation-events.json"
+}
+
+test_validation_audit_request_correlation() {
+ local bucket="val-audit-${RUN_TOKEN}" key="round-trip.bin"
+ new_bucket "${bucket}" || return 1
+ events_reset
+ make_test_file "${TEST_TMP}/validation-audit-source.bin" 65536
+ aws_run s3api put-object --bucket "${bucket}" --key "${key}" --body "${TEST_TMP}/validation-audit-source.bin"
+ assert_aws_ok || return 1
+ aws_run s3api get-object --bucket "${bucket}" --key "${key}" "${TEST_TMP}/validation-audit-read.bin"
+ assert_aws_ok || return 1
+ cmp "${TEST_TMP}/validation-audit-source.bin" "${TEST_TMP}/validation-audit-read.bin" || return 1
+ VALIDATION_AUDIT_TOKEN="${AUDIT_TOKEN}" VALIDATION_NOTIFY_TOKEN="${NOTIFY_TOKEN}" \
+ python3 "${SCRIPT_DIR}/scripts/validation_webhooks.py" audit \
+ --input "${AUDIT_EVENTS}" --bucket "${bucket}" --key "${key}" \
+ --output "${TEST_TMP}/validation-audit.json" --timeout 300
+}
+
run_all_tests() {
run_test TIER-101 "TierAddValidation" test_tier101
run_test TIER-102 "TierListInfo" test_tier102
@@ -2786,6 +2938,10 @@ Usage:
--rc-bin Exact rc binary to execute (default: resolve rc from PATH once)
--expected-rc-sha256 Fail before setup unless the rc binary matches this SHA-256
--artifacts-dir Persist structured case results, provenance, logs, and residue
+ --validation-case Run one canonical validation case on the existing Tier topology
+ --validation-artifacts-dir Unique canonical result and evidence directory
+ --candidate-manifest Immutable candidate URL, package SHA-256 and source SHA
+ --testing-commit Exact auto-testing checkout commit
--keep-env Do not tear down the environment after the last run
--ssh-user SSH user for the test nodes (default: azureuser)
--log-file Tee output to a log file
@@ -2809,6 +2965,10 @@ main() {
--rc-bin) RC_BIN_REQUEST="$1"; shift ;;
--expected-rc-sha256) RUSTFS_EXPECTED_RC_SHA256="$1"; shift ;;
--artifacts-dir) ARTIFACTS_DIR="$1"; shift ;;
+ --validation-case) VALIDATION_CASE="$1"; shift ;;
+ --validation-artifacts-dir) VALIDATION_ARTIFACTS_DIR="$1"; shift ;;
+ --candidate-manifest) VALIDATION_CANDIDATE_MANIFEST="$1"; shift ;;
+ --testing-commit) VALIDATION_TESTING_COMMIT="$1"; shift ;;
--keep-env) KEEP_ENV=1 ;;
--ssh-user) SSH_USER="$1"; shift ;;
--log-file) LOG_FILE="$1"; shift ;;
@@ -2822,6 +2982,30 @@ main() {
if [ "${all}" -eq 1 ]; then
SELECTED_TOPOLOGIES=("${TOPO_SINGLE_SINGLE}" "${TOPO_SINGLE_MULTI}")
fi
+ if [ -n "${VALIDATION_CASE}" ]; then
+ case "${VALIDATION_CASE}" in
+ tiering.transition-and-restore|events.delivery|audit.request-correlation) ;;
+ *) die "unknown validation case: ${VALIDATION_CASE}" ;;
+ esac
+ [ "${all}" -eq 0 ] || die "--validation-case cannot run all topologies"
+ [ "${#SELECTED_TOPOLOGIES[@]}" -eq 1 ] || die "--validation-case requires one topology"
+ [ "${SELECTED_TOPOLOGIES[0]}" = "${TOPO_SINGLE_SINGLE}" ] || die "validation case requires single-single topology"
+ [ "${KEEP_ENV}" -eq 0 ] || die "--validation-case requires environment teardown"
+ if [ "${DRY_RUN}" -eq 0 ]; then
+ [ -n "${VALIDATION_ARTIFACTS_DIR}" ] || die "--validation-case requires --validation-artifacts-dir"
+ [ -n "${VALIDATION_CANDIDATE_MANIFEST}" ] || die "--validation-case requires --candidate-manifest"
+ [ -n "${VALIDATION_TESTING_COMMIT}" ] || die "--validation-case requires --testing-commit"
+ [ ! -e "${VALIDATION_ARTIFACTS_DIR}" ] || die "validation artifacts directory already exists"
+ PACKAGE_URL="$(jq -er '.package_url' "${VALIDATION_CANDIDATE_MANIFEST}")" || die "candidate URL missing"
+ PACKAGE_SHA256="$(jq -er '.package_sha256' "${VALIDATION_CANDIDATE_MANIFEST}")" || die "candidate package SHA missing"
+ [[ "${PACKAGE_SHA256}" =~ ^[0-9a-f]{64}$ ]] || die "invalid candidate package SHA"
+ jq -e '.source_sha | test("^[0-9a-f]{40}$")' "${VALIDATION_CANDIDATE_MANIFEST}" >/dev/null \
+ || die "candidate source commit missing"
+ [ "${VALIDATION_TESTING_COMMIT}" = "$(git -C "${SCRIPT_DIR}" rev-parse HEAD)" ] \
+ || die "testing commit does not match this checkout"
+ mkdir -p "${VALIDATION_ARTIFACTS_DIR}"
+ fi
+ fi
[ "${#SELECTED_TOPOLOGIES[@]}" -gt 0 ] || die "no topology selected (--all-topologies / --topology)"
local _p
@@ -2835,8 +3019,14 @@ main() {
need_cmd python3 "Python 3 (path canonicalization and receiver)"
need_cmd curl "HTTP client"
need_cmd openssl "OpenSSL (SigV4)"
- need_cmd mosquitto_sub "mosquitto clients"
+ if [ "${VALIDATION_CASE}" != events.delivery ] && [ "${VALIDATION_CASE}" != audit.request-correlation ]; then
+ need_cmd mosquitto_sub "mosquitto clients"
+ fi
need_cmd timeout "GNU timeout (bounded CLI/network operations)"
+ if [ "${VALIDATION_CASE}" = audit.request-correlation ] && [ "${DRY_RUN}" -eq 0 ]; then
+ SECRET_KEY="$(openssl rand -hex 32)" || die "cannot generate distinct audit validation secret"
+ export RUSTFS_SECRET_KEY="${SECRET_KEY}"
+ fi
init_artifacts
resolve_rc_binary
write_provenance
@@ -2849,15 +3039,44 @@ main() {
for topo in "${SELECTED_TOPOLOGIES[@]}"; do
CURRENT_TOPOLOGY="${topo}"
log "== topology: ${topo} =="
+ if [ -n "${VALIDATION_CASE}" ] && [ "${DRY_RUN}" -eq 0 ]; then
+ validation_write_result RUNNING "validation case started" PENDING
+ VALIDATION_ENV_ACTIVE=1
+ fi
env_setup "${topo}"
+ if [ -n "${VALIDATION_CASE}" ] && [ "${DRY_RUN}" -eq 0 ]; then
+ VALIDATION_BINARY_SHA256="$(printf 'sha256sum /usr/bin/rustfs\n' | run_remote "${SOURCE_NODE}" | awk '{print $1}')"
+ [[ "${VALIDATION_BINARY_SHA256}" =~ ^[0-9a-f]{64}$ ]] || die "installed RustFS binary SHA-256 unavailable"
+ fi
PASS=0; FAIL=0; UNSUPPORTED=0; FAILED_TESTS=()
- run_all_tests
+ if [ -n "${VALIDATION_CASE}" ]; then
+ case "${VALIDATION_CASE}" in
+ tiering.transition-and-restore) run_test "${VALIDATION_CASE}" "TransitionAndRestore" test_validation_tiering_transition_and_restore ;;
+ events.delivery) run_test "${VALIDATION_CASE}" "ObjectEventDelivery" test_validation_events_delivery ;;
+ audit.request-correlation) run_test "${VALIDATION_CASE}" "AuditRequestCorrelation" test_validation_audit_request_correlation ;;
+ esac
+ else
+ run_all_tests
+ fi
results_summary
record_topology_result "${topo}"
+ local cleanup_status=PASS
if [ "${KEEP_ENV}" -eq 0 ]; then
- env_teardown "${topo}"
+ VALIDATION_ENV_ACTIVE=0
+ env_teardown "${topo}" || cleanup_status=FAIL
+ fi
+ if [ -n "${VALIDATION_CASE}" ] && [ "${DRY_RUN}" -eq 0 ]; then
+ validation_write_evidence
+ if [ "${FAIL}" -eq 0 ] && [ "${UNSUPPORTED}" -eq 0 ] && [ "${cleanup_status}" = PASS ]; then
+ validation_write_result PASS "" PASS
+ elif [ "${FAIL}" -eq 0 ] && [ "${UNSUPPORTED}" -eq 0 ]; then
+ validation_write_result INFRA_ERROR "environment teardown failed" FAIL
+ else
+ validation_write_result FAIL "case failed or unsupported; inspect evidence" "${cleanup_status}"
+ fi
fi
if [ "${FAIL}" -gt 0 ]; then overall_fail=1; fi
+ if [ "${UNSUPPORTED}" -gt 0 ] || [ "${cleanup_status}" != PASS ]; then overall_fail=1; fi
done
final_summary
[ "${overall_fail}" -eq 0 ] || exit 1
diff --git a/rustfs_pool_expand.sh b/rustfs_pool_expand.sh
index f21f9770..5e2f8d8c 100755
--- a/rustfs_pool_expand.sh
+++ b/rustfs_pool_expand.sh
@@ -580,6 +580,11 @@ RUSTFS_CONSOLE_ENABLE=${RUSTFS_CONSOLE_ENABLE}
RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL}
RUSTFS_OBS_LOG_DIRECTORY="${RUSTFS_OBS_LOG_DIRECTORY}"
EOF
+ if [ "${RUSTFS_POOL_VALIDATION_MODE:-0}" = 1 ]; then
+ printf 'RUST_LOG=warn,rustfs::ecstore=info,rustfs::rebalance=debug\n'
+ printf 'RUSTFS_SCANNER_ENABLED=true\nRUSTFS_SCANNER_START_DELAY_SECS=0\nRUSTFS_SCANNER_CYCLE=2\n'
+ printf 'HTTP_PROXY=\nHTTPS_PROXY=\nALL_PROXY=\nhttp_proxy=\nhttps_proxy=\nall_proxy=\n'
+ fi
}
# Expand a volume expression's {N...M} range into concrete local paths, e.g.:
diff --git a/scripts/run_validation_case.sh b/scripts/run_validation_case.sh
new file mode 100755
index 00000000..87f36741
--- /dev/null
+++ b/scripts/run_validation_case.sh
@@ -0,0 +1,75 @@
+#!/usr/bin/env bash
+set -Eeuo pipefail
+
+if [ "$#" -ne 4 ]; then
+ printf 'usage: %s CASE_ID CANDIDATE_MANIFEST TESTING_COMMIT ARTIFACTS_DIR\n' "$0" >&2
+ exit 2
+fi
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+CASE_ID="$1"
+CANDIDATE_MANIFEST="$2"
+TESTING_COMMIT="$3"
+ARTIFACTS_DIR="$4"
+
+case "${CASE_ID}" in
+ pool.*)
+ SUITE=scripts/validation_pool/run_case.sh; EXTRA_ARGS=() ;;
+ bucket-replication.*|site-replication.*)
+ SUITE=scripts/validation_replication/run_case.sh; EXTRA_ARGS=() ;;
+ s3.basic-object-round-trip)
+ SUITE=rustfs-s3-compat-test.sh; EXTRA_ARGS=() ;;
+ scanner.usage-convergence|scanner.status-readiness|ilm.configuration-roundtrip|ilm.invalid-configuration|ilm.current-expiration|ilm.tag-filter-expiration|ilm.disabled-rule|ilm.rule-removal|ilm.versioned-current-expiration|ilm.noncurrent-expiration|ilm.newer-noncurrent-retention|ilm.delete-all-versions|ilm.delete-marker-expiration|ilm.multipart-expiration|ilm.restart-persistence|ilm.transition-and-restore)
+ SUITE=rustfs-storage-test.sh; EXTRA_ARGS=() ;;
+ kms.sse-kms-round-trip)
+ SUITE=rustfs-kms-test.sh; EXTRA_ARGS=() ;;
+ tiering.transition-and-restore|events.delivery|audit.request-correlation)
+ SUITE=rustfs-tier-test.sh; EXTRA_ARGS=(--topology single-single) ;;
+ iam.policy-enforcement)
+ SUITE=rustfs-security-test.sh; EXTRA_ARGS=() ;;
+ oidc.login-and-session)
+ SUITE=scripts/validation_oidc/run_case.sh; EXTRA_ARGS=() ;;
+ heal.lock-timeout-and-caller-abort|heal.outcome-counters-and-error-truth|heal.checkpoint-and-mrf-corruption|heal.bucket-incarnation-recreation|heal.admin-healthy-noop|heal.admin-invalid-request|heal.admin-authorization|heal.admin-nolock-safety|heal.race-unversioned-overwrite|heal.race-versioned-put|heal.race-version-delete|heal.race-markers-and-batch-delete|heal.race-multipart-complete-abort|heal.race-copy-and-metadata-update|heal.mixed-version-upgrade-rollback|heal.dedicated-host-forced-restart|heal.race-ilm-expiry-and-transition|heal.race-replication-resync|heal.with-expansion-rebalance|heal.with-rebalance-stop-resume|heal.with-decommission|heal.with-decommission-cancel-clear|heal.after-retired-pool-removal|heal.ec84-multi-set-restart|heal.slow-io-and-eio|heal.kms-and-tier-outage|heal.long-soak|heal.admin-token-cross-node-status|heal.admin-dry-run|heal.missing-part-file|heal.data-shard-bitrot|heal.parity-shard-bitrot|heal.truncated-and-zeroed-shard|heal.completed-multipart-object|heal.range-head-and-conditions|heal.manual-normal-attribution|heal.manual-deep-attribution|heal.historical-versions|heal.repeat-idempotency|heal.versioning-suspended-null|heal.below-read-quorum|heal.missing-or-corrupt-xlmeta|heal.object-metadata-and-tags|heal.multiple-shard-loss|heal.special-keys-and-pagination|heal.object-size-boundaries|heal.node-outage-rejoin|heal.object-lock-retention|heal.metadata-payload-mismatch|heal.read-repair-attribution|heal.disk-readonly-and-permission|heal.scanner-cold-object-repair|heal.delete-marker-semantics|heal.status-partial-disabled-unknown|heal.incremental-cursor-and-truncation|heal.admin-stop-and-stale-token|heal.cancelled-task-restart|heal.admin-overlap-and-force-start|heal.root-graceful-coordinator-restart|heal.below-write-quorum|heal.read-quorum-boundary|heal.stale-version-rejoin|heal.disk-loss-and-repair|heal.disk-replacement-formatted|heal.full-cluster-restart|heal.replacement-auto-rebuild|heal.target-graceful-restart|heal.target-process-crash|heal.mrf-offline-write-replay|heal.disk-replacement-unformatted|heal.performance-abba|heal.admin-scope-isolation|heal.bucket-metadata-integrity|heal.foreign-format-and-unknown-schema|heal.encrypted-objects|heal.coordinator-process-crash|heal.disk-and-node-flapping|heal.enospc-inodes-and-quota|heal.network-partition-and-lock-quorum|heal.multi-pool-target-isolation|heal.conflicting-subquorum-authority|heal.dangling-and-orphan-safety|heal.admin-option-contract|heal.queue-pressure-and-admission)
+ SUITE=scripts/validation_heal/run_case.sh; EXTRA_ARGS=() ;;
+ *)
+ printf 'validation executor is not migrated: %s\n' "${CASE_ID}" >&2
+ exit 2 ;;
+esac
+
+if [[ "${CASE_ID}" == pool.* || "${CASE_ID}" == bucket-replication.* || "${CASE_ID}" == site-replication.* ]]; then
+ python3 -B -c 'import json,sys; catalog=json.load(open(sys.argv[1])); valid={case["id"] for module in catalog["modules"] for case in module["cases"]}; sys.exit(0 if sys.argv[2] in valid else 2)' \
+ "${SCRIPT_DIR}/validation-cases.json" "${CASE_ID}" || {
+ printf 'unknown canonical validation case: %s\n' "${CASE_ID}" >&2
+ exit 2
+ }
+fi
+
+if [ "${SUITE}" = rustfs-tier-test.sh ]; then
+ EXTRA_ARGS+=(--log-file "${ARTIFACTS_DIR}/logs/${CASE_ID}.log")
+fi
+
+[ -f "${CANDIDATE_MANIFEST}" ] || { printf 'candidate manifest missing\n' >&2; exit 2; }
+[[ "${TESTING_COMMIT}" =~ ^[0-9a-f]{40}$ ]] || { printf 'invalid testing commit\n' >&2; exit 2; }
+[ "${TESTING_COMMIT}" = "$(git -C "${SCRIPT_DIR}" rev-parse HEAD)" ] || {
+ printf 'testing commit does not match checkout\n' >&2; exit 2;
+}
+[ ! -e "${ARTIFACTS_DIR}" ] || { printf 'artifacts directory already exists\n' >&2; exit 2; }
+
+mkdir -p -- "${ARTIFACTS_DIR}"
+case_dir="${ARTIFACTS_DIR}/${CASE_ID}"
+suite_rc=0
+bash "${SCRIPT_DIR}/${SUITE}" --validation-case "${CASE_ID}" "${EXTRA_ARGS[@]}" \
+ --candidate-manifest "${CANDIDATE_MANIFEST}" \
+ --testing-commit "${TESTING_COMMIT}" \
+ --validation-artifacts-dir "${case_dir}" -y \
+ || suite_rc=$?
+
+report_rc=0
+python3 "${SCRIPT_DIR}/scripts/validation_report.py" \
+ --catalog "${SCRIPT_DIR}/validation-cases.json" \
+ --results-dir "${case_dir}/cases" \
+ --output "${ARTIFACTS_DIR}/report.md" \
+ --gate-case "${CASE_ID}" || report_rc=$?
+
+if [ "${suite_rc}" -ne 0 ]; then exit "${suite_rc}"; fi
+exit "${report_rc}"
diff --git a/scripts/validation_catalog.py b/scripts/validation_catalog.py
new file mode 100644
index 00000000..436760fe
--- /dev/null
+++ b/scripts/validation_catalog.py
@@ -0,0 +1,208 @@
+#!/usr/bin/env python3
+"""Snapshot and validate the release-validation case catalog."""
+
+from __future__ import annotations
+
+import argparse
+from collections import Counter
+import hashlib
+import json
+from pathlib import Path
+import re
+import subprocess
+import sys
+
+
+EXPECTED_STATUS_COUNTS = {
+ "implemented": 66,
+ "procedure-ready": 84,
+ "planned": 8,
+}
+COMMIT = re.compile(r"[0-9a-f]{40}\Z")
+
+# These are places to inspect, not evidence that a validation assertion passes.
+MODULE_CANDIDATES = {
+ "pool-lifecycle": ["rustfs_pool_expand.sh"],
+ "heal-scanner": ["rustfs_heal_test.sh", "rustfs-fault-tolerance-test.sh"],
+ "heal-extended": ["rustfs_heal_test.sh", "rustfs-fault-tolerance-test.sh"],
+ "ilm-expiration": ["rustfs-tier-test.sh", "rustfs-s3-compat-test.sh"],
+ "bucket-replication": ["rustfs-replication-test.sh"],
+ "site-replication": ["rustfs-replication-test.sh"],
+ "kms": ["rustfs-kms-test.sh"],
+ "data-services": ["rustfs-tier-test.sh"],
+ "identity-access": ["rustfs-security-test.sh"],
+ "s3-compatibility": ["rustfs-s3-compat-test.sh"],
+}
+
+CASE_CANDIDATES = {
+ "scanner.usage-convergence": ["STOR-134"],
+ "ilm.configuration-roundtrip": ["S3C-704"],
+ "ilm.transition-and-restore": ["TIER-105", "TIER-107"],
+ "bucket-replication.source-versioning-required": ["REP-103"],
+ "bucket-replication.put-object": ["REP-104"],
+ "bucket-replication.delete-marker": ["REP-105"],
+ "bucket-replication.existing-object-enabled": ["REP-107"],
+ "bucket-replication.multipart-object": ["REP-108"],
+ "site-replication.add-and-info": ["SITE-101"],
+ "site-replication.object-bidirectional": ["SITE-103"],
+ "site-replication.bucket-policy-metadata": ["SITE-104"],
+ "site-replication.resync": ["SITE-105"],
+ "site-replication.remove-all": ["SITE-106"],
+ "kms.sse-kms-round-trip": ["KMS-104", "KMS-301", "KMS-214"],
+ "tiering.transition-and-restore": ["TIER-105", "TIER-107"],
+ "events.delivery": ["EVT-103", "EVT-104"],
+ "audit.request-correlation": ["AUD-102", "AUD-104", "AUD-106"],
+ "iam.policy-enforcement": ["IAM-105", "IAM-109"],
+ "oidc.login-and-session": ["OIDC-103"],
+ "s3.basic-object-round-trip": [
+ "S3C-101", "S3C-201", "S3C-203", "S3C-205", "S3C-206", "S3C-207",
+ ],
+}
+
+CASE_SUITES = {
+ "scanner.usage-convergence": ["rustfs-storage-test.sh"],
+}
+
+
+def validate_snapshot(snapshot: dict) -> None:
+ if snapshot.get("schema") != 1:
+ raise ValueError("unsupported validation catalog schema")
+ if not isinstance(snapshot.get("source_sha256"), str) or len(snapshot["source_sha256"]) != 64:
+ raise ValueError("missing source catalog SHA-256")
+ if not isinstance(snapshot.get("source_commit"), str) or not COMMIT.fullmatch(snapshot["source_commit"]):
+ raise ValueError("missing source commit")
+
+ modules = snapshot.get("modules")
+ if not isinstance(modules, list):
+ raise ValueError("modules must be a list")
+ ids: set[str] = set()
+ statuses: Counter[str] = Counter()
+ for module in modules:
+ if not module.get("id") or not isinstance(module.get("cases"), list):
+ raise ValueError("invalid module")
+ for case in module["cases"]:
+ case_id = case.get("id")
+ if not isinstance(case_id, str) or not case_id:
+ raise ValueError("case without an ID")
+ if case_id in ids:
+ raise ValueError(f"duplicate case ID: {case_id}")
+ ids.add(case_id)
+ status = case.get("source_status")
+ if status not in EXPECTED_STATUS_COUNTS:
+ raise ValueError(f"invalid source status for {case_id}: {status}")
+ statuses[status] += 1
+ if not isinstance(case.get("assertions"), list) or not case["assertions"]:
+ raise ValueError(f"missing assertions: {case_id}")
+ candidates = case.get("reuse_candidates")
+ if not isinstance(candidates, dict) or not isinstance(candidates.get("suites"), list):
+ raise ValueError(f"missing reuse assessment: {case_id}")
+ if not isinstance(candidates.get("case_ids"), list):
+ raise ValueError(f"invalid reuse case IDs: {case_id}")
+ if dict(statuses) != EXPECTED_STATUS_COUNTS:
+ raise ValueError(f"unexpected case counts: {dict(statuses)}")
+
+
+def validate_reuse_candidates(snapshot: dict, checkout: Path) -> None:
+ for module in snapshot["modules"]:
+ for case in module["cases"]:
+ candidates = case["reuse_candidates"]
+ suites = candidates["suites"]
+ contents = []
+ for suite in suites:
+ if Path(suite).name != suite or not (checkout / suite).is_file():
+ raise ValueError(f"missing reuse suite for {case['id']}: {suite}")
+ contents.append((checkout / suite).read_text(encoding="utf-8"))
+ for old_id in candidates["case_ids"]:
+ marker = re.compile(r"\brun_test\s+" + re.escape(old_id) + r"\b")
+ if not any(marker.search(content) for content in contents):
+ raise ValueError(f"missing reuse case for {case['id']}: {old_id}")
+
+
+def build_snapshot(source: Path) -> dict:
+ try:
+ import yaml
+ except ImportError as exc:
+ raise RuntimeError("PyYAML is required only to refresh the source snapshot") from exc
+
+ raw = source.read_bytes()
+ source_commit = subprocess.run(
+ ["git", "rev-parse", "HEAD"], cwd=source.parent.parent,
+ check=True, capture_output=True, text=True,
+ ).stdout.strip()
+ catalog = yaml.safe_load(raw)
+ if catalog.get("kind") != "ScenarioCatalog":
+ raise ValueError("source is not a ScenarioCatalog")
+ modules = []
+ for module in catalog["spec"]["modules"]:
+ cases = []
+ for source_case in module["cases"]:
+ case = {
+ "id": source_case["id"],
+ "source_status": source_case["status"],
+ "priority": source_case["priority"],
+ "channels": source_case["channels"],
+ "assertions": source_case["assertions"],
+ "probes": source_case.get("probes", {}),
+ "reuse_candidates": {
+ "suites": CASE_SUITES.get(source_case["id"], MODULE_CANDIDATES[module["id"]]),
+ "case_ids": CASE_CANDIDATES.get(source_case["id"], []),
+ },
+ }
+ cases.append(case)
+ modules.append({
+ "id": module["id"],
+ "target_environments": module.get("targetEnvironments", []),
+ "required_capabilities": module.get("requiredCapabilities", []),
+ "cases": cases,
+ })
+ snapshot = {
+ "schema": 1,
+ "source": "rustfs-release-validation/scenarios/catalog.yaml",
+ "source_commit": source_commit,
+ "source_sha256": hashlib.sha256(raw).hexdigest(),
+ "modules": modules,
+ }
+ validate_snapshot(snapshot)
+ return snapshot
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("command", choices=("sync", "check", "summary"))
+ parser.add_argument("--source", type=Path, help="release-validation scenarios/catalog.yaml")
+ parser.add_argument(
+ "--catalog", type=Path,
+ default=Path(__file__).resolve().parent.parent / "validation-cases.json",
+ )
+ args = parser.parse_args()
+ if args.command == "sync":
+ if args.source is None:
+ parser.error("sync requires --source")
+ snapshot = build_snapshot(args.source)
+ args.catalog.write_text(json.dumps(snapshot, ensure_ascii=False, indent=2) + "\n")
+ print(f"wrote {args.catalog}")
+ return 0
+
+ snapshot = json.loads(args.catalog.read_text())
+ validate_snapshot(snapshot)
+ validate_reuse_candidates(snapshot, Path(__file__).resolve().parent.parent)
+ if args.source is not None:
+ current_sha = hashlib.sha256(args.source.read_bytes()).hexdigest()
+ if snapshot["source_sha256"] != current_sha:
+ raise ValueError("source catalog changed; refresh the snapshot")
+ if args.command == "summary":
+ for module in snapshot["modules"]:
+ statuses = Counter(case["source_status"] for case in module["cases"])
+ print(f"{module['id']}: {len(module['cases'])} {dict(statuses)}")
+ else:
+ source_note = "source hash matched" if args.source is not None else "source checkout not supplied"
+ print(f"validation catalog: 158 unique cases, statuses 66/84/8, {source_note}")
+ return 0
+
+
+if __name__ == "__main__":
+ try:
+ sys.exit(main())
+ except (OSError, ValueError, RuntimeError, subprocess.CalledProcessError) as exc:
+ print(f"validation catalog error: {exc}", file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_heal/authority.py b/scripts/validation_heal/authority.py
new file mode 100644
index 00000000..04f3663f
--- /dev/null
+++ b/scripts/validation_heal/authority.py
@@ -0,0 +1,221 @@
+"""C07 authority census and immutable native evidence for actual split fixtures."""
+
+from collections import Counter
+import argparse
+import json
+from pathlib import Path
+import re
+
+try:
+ from .heal_probe import require
+ from .healthy import validate_topology
+ from .shards import files
+ from .versions import public_version_id
+ from . import native_regressions as native
+except ImportError:
+ from heal_probe import require
+ from healthy import validate_topology
+ from shards import files
+ from versions import public_version_id
+ import native_regressions as native
+
+
+CASE_ID = 'heal.conflicting-subquorum-authority'
+BRANCHES = ('disjoint-version-quorums', 'disjoint-incarnations', 'native-authority-fence')
+NATIVE_KEYS = ('conflicting-authority-guard', 'reconstructable-authority-preservation')
+SOURCE_TREE = '8071fb0a9e5353eb7d3fa58c8a58c8622473f2bd'
+CARGO_LOCK = 'd8f7bc416ba67d19873d34e9389e5aa823b22d7d7552706a5a810cee26f6eda2'
+HOSTED_BINARY = '/home/runner/work/rustfs/rustfs/native-tools/ecstore-native-tests'
+
+
+def object_files(snapshot, bucket, key):
+ prefix = bucket + '/' + key + '/'
+ indexed = files(snapshot)
+ return {(node, disk): [{'path': path.removeprefix(prefix), 'size': entry['size'], 'sha256': entry['sha256']}
+ for (owner, drive, path), entry in sorted(indexed.items())
+ if owner == node and drive == disk and path.startswith(prefix)]
+ for node, value in snapshot.items() for disk in range(len(value['formats']))}
+
+
+def census(snapshot, bucket, key, topology):
+ layout = validate_topology(snapshot, topology)
+ indexed = files(snapshot)
+ identities, slots, geometry = Counter(), [], None
+ for node, observed in snapshot.items():
+ for fmt in observed['formats']:
+ disk = fmt['disk']
+ row = indexed.get((node, disk, f'{bucket}/{key}/xl.meta'))
+ require(row and row['metadata'].get('bucket') == bucket and row['metadata'].get('key') == key,
+ 'C07 lost an original metadata member or its exact object identity')
+ versions = row['metadata']['versions']
+ latest = [version for version in versions if version.get('isLatest') is True]
+ require(len(latest) == 1 and latest[0].get('deleted') is False and latest[0].get('inline') is None,
+ 'C07 requires one external current candidate on every original member')
+ version = latest[0]
+ erasure = version['erasure']
+ coding = {name: erasure[name] for name in ('data_blocks', 'parity_blocks', 'block_size', 'distribution', 'algorithm')}
+ require(geometry is None or geometry == coding, 'C07 generations use different coding/distribution contracts')
+ geometry = coding
+ members = fmt['value']['xl']['sets'][0]
+ slot = members.index(fmt['value']['xl']['this'])
+ require(type(erasure['index']) is int and erasure['index'] == erasure['distribution'][slot]
+ and version.get('role') == ('data' if erasure['index'] <= erasure['data_blocks'] else 'parity'),
+ 'C07 object role does not bind its original format slot')
+ # Count every live version separately. An older version hidden in
+ # the other group's metadata must not silently reach read quorum.
+ live = [item for item in versions if item.get('deleted') is False]
+ for candidate in live:
+ identity = {name: candidate.get(name) for name in ('dataDir', 'size', 'parts', 'deleted')}
+ identity['versionId'] = public_version_id(candidate.get('versionId'))
+ require(isinstance(identity['dataDir'], str) and identity['dataDir'], 'C07 version has no physical data identity')
+ digest = json.dumps(identity, sort_keys=True, separators=(',', ':'))
+ identities[digest] += 1
+ slots.append({'node': node, 'disk': disk, 'diskUUID': fmt['value']['xl']['this'],
+ 'metadataSha256': row['sha256'], 'versionId': public_version_id(version.get('versionId')),
+ 'dataDir': version['dataDir'], 'setSlot': slot})
+ total = len(slots)
+ require(type(geometry['data_blocks']) is int and type(geometry['parity_blocks']) is int
+ and geometry['data_blocks'] + geometry['parity_blocks'] == total
+ and sorted(geometry['distribution']) == list(range(1, total + 1))
+ and len({row['diskUUID'] for row in slots}) == total,
+ 'C07 census lacks one exact erasure set and all independent original members')
+ return {'topology': topology, 'layout': layout, 'coding': geometry, 'N': total,
+ 'R': geometry['data_blocks'], 'groups': [{'identity': json.loads(identity), 'members': count}
+ for identity, count in sorted(identities.items())], 'slots': slots}
+
+
+def validate_split(snapshot, generation_a, generation_b, bucket, key, topology, branch):
+ require(branch in BRANCHES[:2], 'native evidence cannot stand in for a physical split')
+ before_a = census(generation_a, bucket, key, topology)
+ before_b = census(generation_b, bucket, key, topology)
+ mixed = census(snapshot, bucket, key, topology)
+ require(before_a['N'] == before_b['N'] == mixed['N'] and before_a['coding'] == before_b['coding'] == mixed['coding']
+ and len(before_a['groups']) == len(before_b['groups']) == 1
+ and before_a['groups'][0]['members'] == before_b['groups'][0]['members'] == mixed['N'],
+ 'C07 archived generations are not independently complete single authorities')
+ identities = [row['groups'][0]['identity'] for row in (before_a, before_b)]
+ require(identities[0] != identities[1] and identities[0]['dataDir'] != identities[1]['dataDir'],
+ 'C07 generations reuse one logical or physical authority')
+ if branch == 'disjoint-version-quorums':
+ require(identities[0]['versionId'] != identities[1]['versionId']
+ and all(identity['versionId'] != 'null' for identity in identities), 'C07 version groups are not distinct real ACKs')
+ else:
+ require(all(identity['versionId'] == 'null' for identity in identities),
+ 'C07 incarnation branch needs distinct unversioned physical generations, not bucket-incarnation claims')
+ require(len(mixed['groups']) == 2 and all(row['identity'] in identities and row['members'] == mixed['N'] // 2
+ and row['members'] < mixed['R'] for row in mixed['groups']),
+ 'C07 live identities do not form two independently sub-quorum groups')
+ a, b, current = (object_files(value, bucket, key) for value in (generation_a, generation_b, snapshot))
+ members = sorted(current)
+ require(set(a) == set(b) == set(current) and mixed['N'] == len(members), 'C07 split omitted original members')
+ for index, member in enumerate(members):
+ require(current[member] == (a if index < len(members) // 2 else b)[member],
+ 'C07 split does not preserve every exact chosen generation byte')
+ return {**mixed, 'everyLiveIdentityBelowR': True, 'crossVersionUnionClaimedAsAuthority': False,
+ 'bucketIncarnationSplitClaimed': False}
+
+
+def validate_negative_task(status, bucket, options):
+ outcome = status.get('outcome') or {}
+ execution = outcome.get('execution') or {}
+ require(status.get('summary') == 'stopped' and status.get('truncated', False) is False
+ and status.get('settings') == options and outcome.get('objectsTruncated') is False
+ and execution.get('state') in {'completed_with_errors', 'aborted'}
+ and outcome.get('coverage') in {'complete', 'partial', 'unknown'},
+ 'C07 Heal lost its explicit terminal refusal/selector/coverage evidence')
+ counters = outcome.get('counters') or {}
+ names = ('processed', 'healed', 'unchanged', 'skipped', 'failed', 'unknown', 'attemptFailures')
+ require(all(type(counters.get(name)) is int and counters[name] >= 0 for name in names)
+ and counters.get('overflowed') is False and counters['healed'] == counters['unchanged'] == 0
+ and (counters['failed'] + counters['unknown'] > 0 or execution == {'state': 'aborted', 'reason': 'untraversable'}),
+ 'C07 task claimed repair/health or concealed its unresolved responsibility')
+ objects = outcome.get('objects')
+ require(isinstance(objects, list), 'C07 task omitted its responsibility diagnostics')
+ if execution.get('state') == 'aborted':
+ require(execution == {'state': 'aborted', 'reason': 'untraversable'}
+ and outcome['coverage'] in {'partial', 'unknown'},
+ 'C07 abort concealed its untraversable responsibility or claimed full coverage')
+ else:
+ require(bool(objects), 'C07 failed task has no exact conflict responsibility')
+ identities = set()
+ for row in objects:
+ identity = row.get('identity') or {}
+ require(identity.get('bucket') == bucket and identity.get('object') == 'conflict.bin'
+ and identity.get('kind') == 'object' and bool(identity.get('bucketIncarnationId'))
+ and row.get('disposition', {}).get('state') in {'failed', 'unknown', 'deferred'},
+ 'C07 task attributed a false repair or refusal to another object')
+ require(type(identity.get('poolIndex')) is int and identity['poolIndex'] == options['pool'] == 0
+ and type(identity.get('setIndex')) is int and identity['setIndex'] == options['set'] == 0,
+ 'C07 refusal escaped the exact requested original pool/set')
+ key = (identity['bucketIncarnationId'], public_version_id(identity.get('versionId')))
+ require(key not in identities, 'C07 refused-task diagnostics repeat one responsibility identity')
+ identities.add(key)
+ states = [row['disposition']['state'] for row in objects]
+ require(counters['processed'] == len(objects) and counters['failed'] == states.count('failed')
+ and counters['skipped'] == states.count('unknown') + states.count('deferred')
+ and counters['unknown'] == states.count('unknown') and counters['attemptFailures'] >= counters['failed'],
+ 'C07 refused-task counters conceal missing or mismatched object diagnostics')
+ return {'safeExplicitRefusal': True, 'healed': 0, 'verifiedHealthy': 0,
+ 'runtimeCommitPauseClaimed': False, 'coverage': outcome['coverage']}
+
+
+def verify_native(directory, candidate):
+ directory = Path(directory)
+ tools, evidence = directory / 'native-tools', directory / 'native-evidence'
+ manifest = json.loads((tools / 'native-regressions.json').read_text())
+ build_run = json.loads((directory / 'tool-run.json').read_text())
+ require(type(build_run.get('id')) is int and build_run['id'] > 0
+ and type(build_run.get('run_attempt')) is int and build_run['run_attempt'] > 0
+ and build_run.get('status') == 'completed' and build_run.get('conclusion') == 'success'
+ and build_run.get('path') in {'.github/workflows/rustfs-functional-chain.yml', '.github/workflows/rustfs-validation-tools.yml'}
+ and manifest.get('runId') == str(build_run['id']) and manifest.get('runAttempt') == str(build_run['run_attempt']),
+ 'C07 native artifact is not bound to the downloaded successful tools build attempt')
+ require(candidate['source_sha'] == native.CANDIDATE and manifest.get('schemaVersion') == 1
+ and manifest.get('candidateCommit') == native.CANDIDATE and manifest.get('candidateSourceTree') == SOURCE_TREE
+ and manifest.get('candidateCargoLockSha256') == CARGO_LOCK
+ and manifest.get('sourceFiles') == {native.SOURCE: native.SOURCE_SHA}
+ and manifest.get('sourceUnmodified') is True and manifest.get('optimized') is True
+ and manifest.get('collectorSha256') == native.sha(Path(native.__file__))
+ and manifest.get('regressions') == native.REGRESSIONS and manifest.get('fleetPassClaimed') is False,
+ 'C07 native artifact changed its exact source/build/collector contract')
+ require(manifest.get('binaryName') == 'ecstore-native-tests' and re.fullmatch(r'[0-9a-f]{64}', manifest.get('binarySha256', '')),
+ 'C07 native executable path or digest is not registered')
+ binary = tools / 'ecstore-native-tests'
+ require(binary.is_file() and not binary.is_symlink() and native.sha(binary) == manifest['binarySha256'],
+ 'C07 native executable differs from its hosted source build')
+ summary = json.loads((evidence / 'native-summary.json').read_text())
+ require(summary.get('complete') is True and summary.get('fleetPassClaimed') is False
+ and summary.get('canonicalCasePassClaimed') is False and summary.get('sourceUnmodified') is True,
+ 'C07 native regressions did not complete independently')
+ records = summary.get('results', [])
+ require(len(records) == len(native.REGRESSIONS) and {row['regression'] for row in records} == set(native.REGRESSIONS),
+ 'C07 native summary omitted, duplicated or added exact tests')
+ listing = (tools / 'test-inventory.log').read_text()
+ runtime_listing = (evidence / 'runtime-inventory.log').read_text()
+ proofs = []
+ for key in native.REGRESSIONS:
+ spec = native.REGRESSIONS[key]
+ row = next(record for record in records if record['regression'] == key)
+ require(row == json.loads((evidence / (key + '.json')).read_text()) and row.get('verdict') == 'PASS'
+ and row.get('test') == spec['test'] and row.get('candidateCommit') == native.CANDIDATE
+ and row.get('binarySha256') == manifest['binarySha256'] and row.get('emittedBarrierLogsClaimed') is False
+ and row.get('fleetPassClaimed') is False
+ and row.get('command') == [HOSTED_BINARY, spec['test'], '--exact', '--nocapture', '--test-threads=1']
+ and row.get('sourceAssertions') == spec['sourceAssertions'] and row.get('fenceKind') == spec['fenceKind'],
+ 'C07 native result does not bind one exact original test execution')
+ native.exact_listing(listing, spec['test'])
+ native.exact_listing(runtime_listing, spec['test'])
+ proof = native.test_result((evidence / (key + '.log')).read_text(), spec['test'], row.get('exitCode'))
+ if key in NATIVE_KEYS:
+ proofs.append({'regression': key, 'test': spec['test'], **proof})
+ return {'candidateCommit': native.CANDIDATE, 'binarySha256': manifest['binarySha256'], 'results': proofs,
+ 'toolsBuildRun': build_run['id'], 'toolsBuildAttempt': build_run['run_attempt'],
+ 'nativeRegressionPassed': True, 'fleetPassClaimed': False, 'runtimeCommitPauseClaimed': False}
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--directory', type=Path, required=True)
+ parser.add_argument('--candidate', type=Path, required=True)
+ args = parser.parse_args()
+ print(json.dumps(verify_native(args.directory, json.loads(args.candidate.read_text())), indent=2))
diff --git a/scripts/validation_heal/authority_runner.py b/scripts/validation_heal/authority_runner.py
new file mode 100644
index 00000000..570014cc
--- /dev/null
+++ b/scripts/validation_heal/authority_runner.py
@@ -0,0 +1,300 @@
+#!/usr/bin/env python3
+"""Execute C07 live authority splits and independently bound native guard evidence."""
+
+import argparse
+from datetime import datetime, timedelta, timezone
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import secrets
+import signal
+import sys
+import time
+from urllib.parse import quote
+import xml.etree.ElementTree as ET
+
+try:
+ from .authority import CASE_ID, BRANCHES, census, object_files, validate_split, validate_negative_task, verify_native
+ from .heal_probe import Probe, canonical, compare_inventories, payload, require, save
+ from .heal_workload import Recorder, verify_seed
+ from .healthy import BASE_OPTIONS, Fleet, physical_content
+ from .inspector import verify as verify_inspector
+ from .matrix import select
+ from .native_regressions import CANDIDATE
+ from .scopes import verify_parts
+ from .shards import map_target
+ from .stop import idle_fleet
+ from .versions import validate_unrecoverable_get, verify_quorum_contract
+except ImportError:
+ from authority import CASE_ID, BRANCHES, census, object_files, validate_split, validate_negative_task, verify_native
+ from heal_probe import Probe, canonical, compare_inventories, payload, require, save
+ from heal_workload import Recorder, verify_seed
+ from healthy import BASE_OPTIONS, Fleet, physical_content
+ from inspector import verify as verify_inspector
+ from matrix import select
+ from native_regressions import CANDIDATE
+ from scopes import verify_parts
+ from shards import map_target
+ from stop import idle_fleet
+ from versions import validate_unrecoverable_get, verify_quorum_contract
+
+
+SIZE = 1024**2 + 73
+
+
+def negative_heal(probe, fleet, bucket, options, active, contract):
+ path = '/rustfs/admin/v3/heal/' + quote(bucket, safe='') + '/conflict.bin'
+ started = fleet.record('negative-authority-admission', probe.admin(path, [], json.dumps(options).encode()))
+ expected_error = contract['unrecoverableGet']
+ if started.get('httpStatus') == expected_error['httpStatus']:
+ body = started.get('response')
+ if isinstance(body, dict):
+ code = body.get('Code') or body.get('code')
+ require(not body.get('clientToken'), 'refused authority admission returned a task token')
+ else:
+ require(isinstance(body, str) and '= 4 and len(nodes) == len(set(nodes)) and all(re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9_.-]*', node) for node in nodes),
+ 'C07 requires four distinct current Heal nodes')
+ results = []
+ try:
+ for topology in ('H1', 'H2'):
+ for branch in BRANCHES:
+ results.append(run_branch(args, topology, nodes[:1] if topology == 'H1' else nodes[:4], branch))
+ if results[-1]['cleanup'] != 'PASS': break
+ if results[-1]['cleanup'] != 'PASS': break
+ finally:
+ complete = len(results) == 6 and all(row['verdict'] == row['cleanup'] == 'PASS' for row in results)
+ save(args.evidence / 'healthy-summary.json', {'caseId': CASE_ID, 'results': results, 'complete': complete,
+ 'cleanup': 'PASS' if results and all(row['cleanup'] == 'PASS' for row in results) else 'FAIL'})
+ require(complete, 'C07 needs both actual live topologies, both split branches, independent native guards and complete cleanup')
+
+
+if __name__ == '__main__':
+ try: main()
+ except Exception as exc:
+ print(f'C07 authority validation failed: {exc}', file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_heal/authorization.py b/scripts/validation_heal/authorization.py
new file mode 100644
index 00000000..b32a93bb
--- /dev/null
+++ b/scripts/validation_heal/authorization.py
@@ -0,0 +1,208 @@
+#!/usr/bin/env python3
+"""A08 exercises actual IAM principals and every Heal control operation."""
+
+import hashlib
+import json
+import secrets
+import time
+from urllib import request
+from urllib.error import HTTPError
+from urllib.parse import quote, urlencode
+import xml.etree.ElementTree as ET
+
+try:
+ from .heal_probe import Probe, compare_inventories, require, validate_binding
+ from .heal_signing import sign_admin_request
+ from .healthy import physical_content
+except ImportError:
+ from heal_probe import Probe, compare_inventories, require, validate_binding
+ from heal_signing import sign_admin_request
+ from healthy import physical_content
+
+
+# The candidate's S3Router::check_access rejects unsigned Admin traffic
+# before authorize_admin_request (whose missing-credential error is 400).
+DENIALS = {"anonymous": (403, "AccessDenied"), "bad-signature": (403, "SignatureDoesNotMatch"),
+ "expired-signature": (403, "RequestTimeTooSkewed"), "denied-user": (403, "AccessDenied")}
+
+
+def response_code(observation):
+ body = observation.get("response")
+ if isinstance(body, dict):
+ return body.get("Code") or body.get("code")
+ if isinstance(body, str):
+ return next((element.text for element in ET.fromstring(body).iter()
+ if element.tag.rsplit("}", 1)[-1] == "Code"), None)
+ return None
+
+
+def validate_denial(observation, mode):
+ status, code = DENIALS[mode]
+ require(observation.get("httpStatus") == status and response_code(observation) == code,
+ f"{mode} did not satisfy its frozen {status}/{code} refusal")
+ body = observation.get("response")
+ forbidden = {"outcome", "objects", "progress", "clienttoken"}
+ def disclosed(value):
+ if isinstance(value, dict):
+ return any(str(key).lower() in forbidden or disclosed(item) for key, item in value.items())
+ if isinstance(value, list):
+ return any(disclosed(item) for item in value)
+ return False
+ if isinstance(body, str):
+ leaked = any(element.tag.rsplit("}", 1)[-1].lower() in forbidden for element in ET.fromstring(body).iter())
+ else:
+ leaked = disclosed(body)
+ require(not leaked, "unauthorized response disclosed task responsibility or admitted a token")
+ return {"status": status, "code": code, "noTaskDisclosure": True}
+
+
+def iam_exchange(probe, fleet, operation, user, *, secret=None, policy=None, document=None):
+ from botocore.awsrequest import AWSRequest
+ validate_binding(probe.binding)
+ require(user.startswith("rvheal") and user.isalnum(), "IAM principal is outside the run namespace")
+ routes = {
+ "add-user": ("PUT", "add-user", {"accessKey": user}, {"secretKey": secret, "status": "enabled"}),
+ "remove-user": ("DELETE", "remove-user", {"accessKey": user}, None),
+ "add-policy": ("PUT", "add-canned-policy", {"name": policy}, document),
+ "remove-policy": ("DELETE", "remove-canned-policy", {"name": policy}, None),
+ "attach": ("POST", "idp/builtin/policy/attach", {}, {"policies": [policy], "user": user}),
+ "detach": ("POST", "idp/builtin/policy/detach", {}, {"policies": [policy], "user": user}),
+ }
+ require(operation in routes and (policy is None or policy.startswith("rvheal") and policy.isalnum()), "invalid owned IAM operation")
+ method, route, query, body = routes[operation]
+ raw = json.dumps(body).encode() if body is not None else b""
+ url = probe.endpoint + "/rustfs/admin/v3/" + route + ("?" + urlencode(query) if query else "")
+ outgoing = AWSRequest(method=method, url=url, data=raw,
+ headers={"Content-Type": "application/json", "x-amz-content-sha256": "UNSIGNED-PAYLOAD"})
+ outgoing.context["payload_signing_enabled"] = False
+ sign_admin_request(outgoing, *probe.credentials())
+ try:
+ response = probe._open_admin(request.Request(url, data=raw if raw else None, method=method,
+ headers=dict(outgoing.headers.items())))
+ except HTTPError as exc:
+ response = exc
+ with response:
+ returned = response.read(64 * 1024 + 1)
+ require(len(returned) <= 64 * 1024, "IAM response exceeds evidence budget")
+ observed = {"operation": operation, "httpStatus": response.code,
+ "principal": "run-owned", "response": returned.decode(errors="replace"),
+ "requestBodyArchived": False, "credentialValuesArchived": False}
+ fleet.record("iam-" + operation, observed)
+ require(observed["httpStatus"] == 200, "owned IAM operation failed: " + operation)
+ return observed
+
+
+def admit(probe, fleet, path, options, active):
+ started = probe.admin(path, [], json.dumps(options).encode())
+ token = (started.get("response") or {}).get("clientToken") if isinstance(started.get("response"), dict) else None
+ if isinstance(token, str) and token:
+ active.append((path, token))
+ fleet.record("authorization-admission", started)
+ require(started.get("httpStatus") == 200 and token, "authorized principal could not admit its own task")
+ return token
+
+
+def authorization_requests(probe, fleet, bucket, before, baseline, active, options):
+ suffix = hashlib.sha256(probe.binding["runId"].encode()).hexdigest()[:16]
+ denied_user, allowed_user, policy = ("rvheal" + suffix + ending for ending in ("denied", "allowed", "policy"))
+ denied_secret, allowed_secret = secrets.token_hex(24), secrets.token_hex(24)
+ fleet.secret_values.update({*probe.credentials(), denied_user, allowed_user, denied_secret, allowed_secret})
+ denied = Probe(probe.binding, credential_pair=(denied_user, denied_secret))
+ allowed = Probe(probe.binding, credential_pair=(allowed_user, allowed_secret))
+ path = "/rustfs/admin/v3/heal/" + quote(bucket, safe="")
+ task_options = {**options, "recursive": True, "dryRun": True}
+ branches, cleanup_errors = {}, []
+ users, policy_created, attached = [], False, False
+ try:
+ for user, secret in ((denied_user, denied_secret), (allowed_user, allowed_secret)):
+ users.append(user) # Preserve ownership even if admission loses its reply.
+ iam_exchange(probe, fleet, "add-user", user, secret=secret)
+ policy_created = True
+ # The candidate admits unscoped Admin actions without an S3 Resource;
+ # its resource parser correctly rejects a bare '*' as an invalid ARN.
+ document = {"Version": "2012-10-17", "Statement": [{"Effect": "Allow", "Action": ["admin:Heal"]}]}
+ iam_exchange(probe, fleet, "add-policy", allowed_user, policy=policy, document=document)
+ attached = True
+ iam_exchange(probe, fleet, "attach", allowed_user, policy=policy)
+ for principal in (denied, allowed):
+ result = fleet.record("least-privilege-s3-denial", principal.s3("get_object", {"Bucket": bucket, "Key": "objects/0008.bin"}))
+ response = result.get("response") or {}
+ require(result.get("acknowledged") is False and result.get("resultUnknown") is False and "payload" not in result
+ and response.get("ResponseMetadata", {}).get("HTTPStatusCode") == 403
+ and response.get("Error", {}).get("Code") == "AccessDenied", "Heal-only/denied principal unexpectedly has S3 read access")
+ root_token = admit(probe, fleet, path, task_options, active)
+ for mode in DENIALS:
+ proofs, failures = {}, []
+ client = denied if mode == "denied-user" else probe
+ auth = "signed" if mode == "denied-user" else mode
+ for operation, query, raw in (
+ ("start", [], json.dumps(task_options).encode()),
+ ("status", [("clientToken", root_token)], b""),
+ ("stop", [("clientToken", root_token), ("forceStop", "true")], b"")):
+ try:
+ observed = client.admin(path, query, raw, auth=auth)
+ token = (observed.get("response") or {}).get("clientToken") if isinstance(observed.get("response"), dict) else None
+ if token and (path, token) not in active:
+ active.append((path, token))
+ fleet.record("auth-" + mode + "-" + operation, observed)
+ proofs[operation] = validate_denial(observed, mode)
+ except Exception as exc:
+ failures.append({"operation": operation, "type": type(exc).__name__, "message": str(exc)})
+ branches[mode] = {"verdict": "FAIL" if failures else "PASS", "operations": proofs, "failures": failures}
+ fleet.record("authorization-negative-branches", branches)
+ deadline = time.monotonic() + 30
+ while True:
+ root_status = fleet.record("root-task-status-after-denials", probe.admin(path, [("clientToken", root_token)], b""))
+ require(root_status.get("httpStatus") == 200 and isinstance(root_status.get("response"), dict), "administrator lost its original token after denied requests")
+ if root_status["response"].get("summary") == "finished":
+ break
+ require(root_status["response"].get("summary") == "running" and time.monotonic() < deadline,
+ "denied control requests stopped the original task or it did not finish before the independent authorized task")
+ time.sleep(0.2)
+ token = admit(allowed, fleet, path, task_options, active)
+ queried = fleet.record("authorized-status", allowed.admin(path, [("clientToken", token)], b""))
+ require(queried.get("httpStatus") == 200 and isinstance(queried.get("response"), dict)
+ and queried["response"].get("summary") in {"running", "finished", "stopped"}, "Heal-only principal cannot query its admitted token")
+ stopped = fleet.record("authorized-stop", allowed.admin(path, [("clientToken", token), ("forceStop", "true")], b""))
+ require(stopped.get("httpStatus") == 200 and isinstance(stopped.get("response"), dict), "Heal-only principal cannot stop its admitted token")
+ require(stopped["response"].get("summary") in {"stopped", "finished"},
+ "Heal-only stop did not return terminal task control")
+ branches["least-privilege-user"] = {"verdict": "PASS", "admissionStatusStop": True,
+ "s3ReadDenied": True, "policyActions": ["admin:Heal"]}
+ finally:
+ for task_path, token in list(active):
+ try:
+ observed = fleet.record("auth-final-stop", probe.admin(task_path, [("clientToken", token), ("forceStop", "true")], b""))
+ require(observed.get("httpStatus") == 200, "authorization task finalization failed")
+ active.remove((task_path, token))
+ except Exception as exc:
+ cleanup_errors.append(str(exc))
+ if attached:
+ try:
+ iam_exchange(probe, fleet, "detach", allowed_user, policy=policy)
+ except Exception as exc:
+ cleanup_errors.append(str(exc))
+ for user in users:
+ try:
+ iam_exchange(probe, fleet, "remove-user", user)
+ except Exception as exc:
+ cleanup_errors.append(str(exc))
+ if policy_created:
+ try:
+ iam_exchange(probe, fleet, "remove-policy", allowed_user, policy=policy)
+ except Exception as exc:
+ cleanup_errors.append(str(exc))
+ fleet.record("iam-finalization", {"cleanup": "FAIL" if cleanup_errors else "PASS", "errors": cleanup_errors})
+ require(not cleanup_errors, "temporary principals, policy or task finalization failed")
+ for principal in (denied, allowed):
+ observed = fleet.record("revoked-credential-denial", principal.admin(path, [], json.dumps(task_options).encode()))
+ require(observed.get("httpStatus") == 403 and response_code(observed) == "InvalidAccessKeyId",
+ "removed temporary credentials still authenticate or return an unexpected outcome")
+ after = fleet.snapshot("physical-after-authorization")
+ require(physical_content(before) == physical_content(after), "authorization/dry-run controls mutated the object fixture")
+ current = fleet.record("s3-after-authorization", probe.inventory(bucket, unversioned_current_tags=True))
+ fleet.record("authorization-controls", {"physicalUnchanged": True, "s3": compare_inventories(baseline, current)})
+ fleet.record("authorization-all-branches", branches)
+ require(set(branches) == {*DENIALS, "least-privilege-user"}
+ and all(branch["verdict"] == "PASS" for branch in branches.values()), "authorization requires every start/status/stop principal branch")
+ return branches
diff --git a/scripts/validation_heal/boundaries.py b/scripts/validation_heal/boundaries.py
new file mode 100644
index 00000000..bcf23abf
--- /dev/null
+++ b/scripts/validation_heal/boundaries.py
@@ -0,0 +1,111 @@
+#!/usr/bin/env python3
+"""D01 size parameters from the fixed candidate contract and observed EC geometry."""
+
+import hashlib
+from pathlib import Path
+import re
+
+try:
+ from .heal_probe import payload, require
+except ImportError:
+ from heal_probe import payload, require
+
+
+BRANCHES = ("zero", "one", "4kib-minus-exact-plus", "inline-minus-exact-plus",
+ "stripe-minus-exact-plus", "large-external")
+
+
+def verify_size_contract(manifest, candidate):
+ contract = manifest.get("sizeContract") or {}
+ extractor = Path(__file__).with_name("prepare_heal_meta_inspector.py")
+ require(contract.get("recognized") is True and contract.get("candidateCommit") == candidate["source_sha"]
+ and contract.get("extractorSourceSha256") == hashlib.sha256(extractor.read_bytes()).hexdigest()
+ and contract.get("inlineRule") == "ceil(size/dataBlocks)<=effectiveInlineBlock AND size<=actualECBlock"
+ and contract.get("effectiveDefaultRule") == "min(ceil(2*defaultInlineBlock/dataBlocks),defaultInlineBlock)"
+ and type(contract.get("defaultInlineBlock")) is int and contract["defaultInlineBlock"] > 0
+ and contract.get("defaultObjectBudgetFactor") == 2
+ and set(contract.get("sourceFiles", {})) == {
+ "crates/ecstore/src/config/storageclass.rs", "crates/ecstore/src/set_disk/mod.rs",
+ "crates/ecstore/src/set_disk/ops/object.rs", "crates/ecstore/src/erasure/coding/erasure.rs"}
+ and all(re.fullmatch(r"[0-9a-f]{64}", value) for value in contract["sourceFiles"].values()),
+ "size boundaries require a recognized fresh exact-candidate source contract")
+ return contract
+
+
+def boundary_sizes(contract, target, processes, *, standard_class=None):
+ require(standard_class in {None, "EC:8"}, "unreviewed boundary storage class")
+ require(processes and all(entry.get("storageClassEnvironment") == {
+ "RUSTFS_STORAGE_CLASS_INLINE_BLOCK": None, "RUSTFS_STORAGE_CLASS_STANDARD": standard_class}
+ for entry in processes.values()), "size policy requires actual process environment confirmation")
+ geometry = target["logical"]["erasure"]
+ data, block = geometry["data_blocks"], geometry["block_size"]
+ require(standard_class is None or data == geometry.get("parity_blocks") == 8
+ and len(geometry.get("distribution", [])) == 16,
+ "EC:8 boundary calibration requires the actual balanced sixteen-member object")
+ require(type(data) is int and data > 0 and type(block) is int and 4097 < block < 12 * 1024**2
+ and target["logical"]["usesLegacyChecksum"] is False,
+ "observed size calibration needs bounded current-codec EC geometry")
+ default = contract["defaultInlineBlock"]
+ effective = min((2 * default + data - 1) // data, default)
+ inline = min(effective * data, block)
+ require(4097 < inline < block, "candidate policy lacks independently testable inline and stripe boundaries")
+ groups = {"zero": [0], "one": [1], "4kib-minus-exact-plus": [4095, 4096, 4097],
+ "inline-minus-exact-plus": [inline - 1, inline, inline + 1],
+ "stripe-minus-exact-plus": [block - 1, block, block + 1], "large-external": [5 * block + 123]}
+ require(set(groups) == set(BRANCHES), "size parameter matrix omitted a source boundary")
+ return {"actualDataBlocks": data, "actualECBlockSize": block, "defaultInlineBlock": default,
+ "effectiveInlineShardBudget": effective, "actualLogicalInlineLimit": inline,
+ "sourceBoundRule": contract["inlineRule"], "groups": groups,
+ "processConfigurationReadBack": True, "codec": "current"}
+
+
+def seed_boundaries(recorder, bucket, parameters):
+ expected, instances = [], []
+ for branch, sizes in parameters["groups"].items():
+ for size in sizes:
+ key = f"boundaries/{len(expected):04d}-{size}.bin"
+ body = payload(f"{bucket}/{key}/0", size)
+ response = recorder.call("put_object", {"Bucket": bucket, "Key": key,
+ "Metadata": {"heal-fixture": "D01", "size": str(size)}, "ContentType": "application/octet-stream"}, body)
+ require(response.get("VersionId") in {None, "null"}, "boundary fixture became versioned")
+ entry = {"key": key, "versionId": "null", "deleteMarker": False, "isLatest": True,
+ "payload": {"size": size, "sha256": hashlib.sha256(body).hexdigest()}}
+ expected.append(entry)
+ shape = "zero-metadata" if size == 0 else "inline" if size <= parameters["actualLogicalInlineLimit"] else "external"
+ for role in ("data", "parity"):
+ for kind in ("missing", "bitflip"):
+ instances.append({"name": f"{branch}-s{size}-{role}-{kind}", "sourceBranch": branch,
+ "size": size, "key": key, "role": role, "kind": kind, "expectedShape": shape,
+ "ecBlockSize": parameters["actualECBlockSize"]})
+ return expected, instances
+
+
+def verify_boundary_ranges(probe, fleet, bucket, expected, block):
+ proofs = []
+ for entry in expected:
+ key, size = entry["key"], entry["payload"]["size"]
+ if size == 0:
+ denied = fleet.record("zero-range", probe.s3("get_object", {
+ "Bucket": bucket, "Key": key, "VersionId": "null", "Range": "bytes=0-0"}))
+ response = denied.get("response") or {}
+ require(denied.get("acknowledged") is False and denied.get("resultUnknown") is False
+ and "payload" not in denied and response.get("ResponseMetadata", {}).get("HTTPStatusCode") == 416
+ and response.get("Error", {}).get("Code") == "InvalidRange",
+ "empty object Range must satisfy the frozen S3 416/InvalidRange contract")
+ proofs.append({"key": key, "zeroRangeRejected": True, "status": 416, "code": "InvalidRange"})
+ continue
+ body = payload(f"{bucket}/{key}/0", size)
+ intervals = {(size - 1, size - 1), (0, min(size - 1, 46))}
+ if size > block:
+ intervals.add((max(0, block - 17), min(size - 1, block + 29)))
+ for start, end in sorted(intervals):
+ observed = fleet.record("size-range", probe.s3("get_object", {
+ "Bucket": bucket, "Key": key, "VersionId": "null", "Range": f"bytes={start}-{end}"}))
+ response = observed.get("response") or {}
+ require(observed.get("acknowledged") is True
+ and response.get("ResponseMetadata", {}).get("HTTPStatusCode") == 206
+ and response.get("ContentRange") == f"bytes {start}-{end}/{size}"
+ and observed.get("payload") == {"size": end - start + 1,
+ "sha256": hashlib.sha256(body[start:end + 1]).hexdigest()}, "actual size/stripe Range changed bytes or bounds")
+ proofs.append({"key": key, "start": start, "end": end, "httpStatus": 206})
+ return proofs
diff --git a/scripts/validation_heal/bounded_volume.py b/scripts/validation_heal/bounded_volume.py
new file mode 100644
index 00000000..e4ff0dd1
--- /dev/null
+++ b/scripts/validation_heal/bounded_volume.py
@@ -0,0 +1,364 @@
+"""I02: fixed-size filesystems bound only into the run-owned service's roots."""
+
+import errno
+import json
+import os
+from pathlib import Path
+import re
+import shutil
+import stat
+import subprocess
+import time
+import uuid
+
+
+MARKER = "bounded-volumes.json"
+IMAGE_BYTES = 128 * 1024**2
+MAX_INODES = 1024
+MIN_SHARED_FREE = 1024**3
+PROBE = '''import errno,json,os,sys
+folder=sys.argv[1]
+info=os.statvfs(folder)
+result={"uid":os.geteuid(),"gid":os.getegid(),"device":os.stat(folder).st_dev,
+ "freeBlocks":info.f_bfree,"availableBlocks":info.f_bavail,"freeInodes":info.f_ffree}
+name=folder+"/.i02-write-witness"
+fd=None
+try:
+ fd=os.open(name,os.O_CREAT|os.O_EXCL|os.O_WRONLY|os.O_NOFOLLOW,0o600)
+ os.write(fd,b"i02"*2048)
+ os.fsync(fd)
+except OSError as exc:
+ result["writeErrno"]=exc.errno
+else:
+ result["writeErrno"]=0
+finally:
+ if fd is not None:
+ os.close(fd)
+ os.unlink(name)
+print(json.dumps(result))
+'''
+
+
+def require(value, message):
+ if not value:
+ raise ValueError(message)
+
+
+def command(*args):
+ result = subprocess.run(args, capture_output=True, text=True, timeout=90)
+ require(result.returncode == 0, f"bounded-volume {args[0]} failed: {result.stderr[-800:]}")
+ return result.stdout
+
+
+def regular(path, *, size=None):
+ info = path.lstat()
+ require(stat.S_ISREG(info.st_mode) and info.st_uid == 0 and info.st_nlink == 1
+ and stat.S_IMODE(info.st_mode) == 0o600 and (size is None or info.st_size == size),
+ "bounded-volume file ownership/type/size differs")
+ return info
+
+
+def read(state, roots, owner):
+ path = state / MARKER
+ if not path.exists():
+ return None
+ require(regular(path).st_size < 16384, "bounded-volume marker exceeds budget")
+ value = json.loads(path.read_text())
+ require(value.get("schemaVersion") == 1 and value.get("runId") == owner["runId"]
+ and value.get("branch") in {"bounded-volume-enospc", "bounded-inode-exhaustion"}
+ and value.get("imageBytes") == IMAGE_BYTES and value.get("maxInodes") == MAX_INODES
+ and len(value.get("volumes", [])) == len(roots) == 4,
+ "bounded-volume contract differs from its original run")
+ require(owner.get("runtimeProfile") != "journal-bounded" or value["branch"] == "bounded-volume-enospc",
+ "bounded journal runtime has no reviewed inode-exhaustion branch")
+ for index, row in enumerate(value["volumes"]):
+ require(type(row.get("disk")) is int and row["disk"] == index and row.get("image") == str(roots[index] / "bounded-volume.img")
+ and row.get("mount") == str(state / "bounded" / f"disk{index}")
+ and str(uuid.UUID(row["uuid"])) == row["uuid"], "bounded-volume paths or filesystem identities differ")
+ folder = Path(row["mount"])
+ require(not any(path.is_symlink() for path in (folder, *folder.parents)), "symlink in bounded mount boundary")
+ return value
+
+
+def loop_for(image):
+ rows = json.loads(command("losetup", "--json", "--list", "--output", "NAME,BACK-FILE,BACK-INO,BACK-MAJ:MIN"))["loopdevices"]
+ selected = [row for row in rows if row["back-file"] == str(image)]
+ if selected:
+ info = regular(image)
+ require(all(int(row["back-ino"]) == info.st_ino
+ and row["back-maj:min"] == f"{os.major(info.st_dev)}:{os.minor(info.st_dev)}" for row in selected),
+ "loop attachment belongs to a different backing inode/device")
+ found = [row["name"] for row in selected]
+ require(len(found) <= 1 and all(re.fullmatch(r"/dev/loop[0-9]+", item) for item in found),
+ "bounded image has ambiguous loop attachments")
+ return found[0] if found else None
+
+
+def mounted(row):
+ observed = subprocess.run(["findmnt", "--json", "--mountpoint", row["mount"], "--output",
+ "SOURCE,FSTYPE,UUID,TARGET,MAJ:MIN,OPTIONS"], capture_output=True, text=True, timeout=15)
+ if observed.returncode == 1:
+ return None
+ require(observed.returncode == 0, "bounded mount identity unavailable")
+ entries = json.loads(observed.stdout).get("filesystems", [])
+ require(len(entries) == 1, "bounded mount is ambiguous")
+ entry = entries[0]
+ require(entry["target"] == row["mount"] and entry["fstype"] == "ext4" and entry["uuid"] == row["uuid"]
+ and entry["source"] == loop_for(Path(row["image"])) and "rw" in entry["options"].split(","),
+ "bounded mount no longer belongs to its exact image/UUID")
+ return entry
+
+
+def configure_block_exhaustion(row):
+ # ext4's internal reserve is separate from mkfs -m 0. Only this newly
+ # created, UUID/backing-inode-bound loop may expose that reserve to padding.
+ entry = mounted(row)
+ require(entry is not None and re.fullmatch(r"/dev/loop[0-9]+", entry["source"]),
+ "block exhaustion requires its exact mounted private loop")
+ source = entry["source"]
+ knob = Path("/sys/fs/ext4") / Path(source).name / "reserved_clusters"
+ fd = os.open(knob, os.O_RDWR | os.O_NOFOLLOW)
+ try:
+ info = os.fstat(fd)
+ require(stat.S_ISREG(info.st_mode) and info.st_uid == 0,
+ "private ext4 reserve control is not root-owned sysfs data")
+ raw = os.read(fd, 64)
+ require(re.fullmatch(rb"[0-9]{1,20}\n", raw) is not None,
+ "private ext4 reserve is not a bounded cluster count")
+ before = int(raw)
+ require(before <= IMAGE_BYTES // 1024 and mounted(row) == entry,
+ "private ext4 mount changed before configuring its reserve")
+ os.lseek(fd, 0, os.SEEK_SET)
+ require(os.write(fd, b"0\n") == 2, "private ext4 reserve write was incomplete")
+ os.lseek(fd, 0, os.SEEK_SET)
+ require(os.read(fd, 64) == b"0\n" and mounted(row) == entry,
+ "private ext4 reserve or mount differs after configuration")
+ finally:
+ os.close(fd)
+ return {"disk": row["disk"], "uuid": row["uuid"], "source": source,
+ "reservedClustersBefore": before, "reservedClustersAfter": 0,
+ "sharedFilesystemModified": False}
+
+
+def create(state, roots, owner, branch):
+ require(owner.get("runtimeProfile") in {"bounded-capacity", "journal-bounded"} and read(state, roots, owner) is None
+ and branch in {"bounded-volume-enospc", "bounded-inode-exhaustion"}, "invalid bounded capacity allocation")
+ require(owner.get("runtimeProfile") != "journal-bounded" or branch == "bounded-volume-enospc",
+ "bounded journal allocation accepts only fixed block exhaustion")
+ require(all(not any((root / "data").iterdir()) and not (root / "bounded-volume.img").exists()
+ and shutil.disk_usage(root).free >= MIN_SHARED_FREE + IMAGE_BYTES for root in roots),
+ "bounded allocation requires empty owned roots and shared-drive safety margin")
+ value = {"schemaVersion": 1, "runId": owner["runId"], "branch": branch, "imageBytes": IMAGE_BYTES,
+ "maxInodes": MAX_INODES, "volumes": [{"disk": index, "image": str(root / "bounded-volume.img"),
+ "mount": str(state / "bounded" / f"disk{index}"), "uuid": str(uuid.uuid4())}
+ for index, root in enumerate(roots)]}
+ # The complete undo identity exists before allocating an image or loop.
+ fd = os.open(state / MARKER, os.O_CREAT | os.O_EXCL | os.O_WRONLY | os.O_NOFOLLOW, 0o600)
+ with os.fdopen(fd, "w") as stream:
+ json.dump(value, stream)
+ stream.flush()
+ os.fsync(stream.fileno())
+ (state / "bounded").mkdir(mode=0o700)
+ reserve_configuration = []
+ for row in value["volumes"]:
+ image = Path(row["image"])
+ descriptor = os.open(image, os.O_CREAT | os.O_EXCL | os.O_RDWR | os.O_NOFOLLOW, 0o600)
+ try:
+ os.posix_fallocate(descriptor, 0, IMAGE_BYTES)
+ os.fsync(descriptor)
+ finally:
+ os.close(descriptor)
+ regular(image, size=IMAGE_BYTES)
+ # mkfs receives an exclusively created bounded regular file, never a
+ # supplied device. The subsequently attached loop is derived from it.
+ command("mkfs.ext4", "-F", "-q", "-m", "0", "-N", str(MAX_INODES), "-U", row["uuid"], str(image))
+ loop = command("losetup", "--find", "--show", "--nooverlap", str(image)).strip()
+ require(loop == loop_for(image), "new loop identity differs")
+ folder = Path(row["mount"])
+ folder.mkdir(mode=0o700)
+ command("mount", "-t", "ext4", "-o", "nodev,nosuid,noexec", loop, str(folder))
+ mounted(row)
+ if branch == "bounded-volume-enospc":
+ reserve_configuration.append(configure_block_exhaustion(row))
+ folder.chmod(0o700)
+ for name in ("data", "quarantine", "padding"):
+ (folder / name).mkdir(mode=0o700)
+ info = os.statvfs(folder)
+ require(info.f_blocks * info.f_frsize <= IMAGE_BYTES and 0 < info.f_files <= MAX_INODES,
+ "actual bounded filesystem exceeds registered block/inode limits")
+ return {**value, "ext4ReservedClusterConfiguration": reserve_configuration,
+ "hostSharedMountRemounted": False, "allocatedBytesPerNode": len(roots) * IMAGE_BYTES,
+ "independentPhysicalDriveCountIncreased": False}
+
+
+def views(state, roots, owner):
+ value = read(state, roots, owner)
+ if value is None:
+ require(owner.get("runtimeProfile") not in {"bounded-capacity", "journal-bounded"}, "bounded runtime has no durable allocation intent")
+ return roots
+ result = []
+ for original, row in zip(roots, value["volumes"], strict=True):
+ require(not any((original / "data").iterdir()), "candidate wrote outside its private bounded binding")
+ regular(Path(row["image"]), size=IMAGE_BYTES)
+ require(mounted(row) is not None, "bounded filesystem is not mounted")
+ folder = Path(row["mount"])
+ require(folder.stat().st_uid == 0 and stat.S_IMODE(folder.stat().st_mode) == 0o700,
+ "bounded filesystem root is not private/root-owned")
+ for name in ("data", "quarantine", "padding"):
+ path = folder / name
+ info = path.lstat()
+ require(stat.S_ISDIR(info.st_mode) and info.st_uid == 0 and not path.is_symlink()
+ and info.st_dev == folder.stat().st_dev and stat.S_IMODE(info.st_mode) == 0o700,
+ "bounded data/quarantine/padding boundary differs")
+ result.append(folder)
+ return tuple(result)
+
+
+def properties(state, roots, owner):
+ if read(state, roots, owner) is None:
+ require(owner.get("runtimeProfile") not in {"bounded-capacity", "journal-bounded"}, "bounded runtime is not configured")
+ return []
+ actual = views(state, roots, owner)
+ return ["--property=PrivateMounts=true", "--property=BindPaths=" + " ".join(
+ f"{view / 'data'}:{root / 'data'}" for view, root in zip(actual, roots, strict=True))]
+
+
+def fill(folder, branch):
+ padding = folder / "padding"
+ require(not any(padding.iterdir()), "bounded filler is already present")
+ deadline, written, count = time.monotonic() + 60, 0, 0
+ if branch == "bounded-volume-enospc":
+ fd = os.open(padding / "blocks.bin", os.O_CREAT | os.O_EXCL | os.O_WRONLY | os.O_NOFOLLOW, 0o600)
+ chunk, file_written = 1024**2, 0
+ try:
+ while True:
+ require(time.monotonic() < deadline and written < IMAGE_BYTES, "bounded block fill exceeded its budget")
+ try:
+ requested = min(chunk, IMAGE_BYTES - written)
+ actual = os.write(fd, b"\0" * requested)
+ require(0 < actual <= requested, "bounded block fill made no valid byte progress")
+ written += actual
+ file_written += actual
+ os.fsync(fd)
+ except OSError as error:
+ require(error.errno == errno.ENOSPC, "bounded fill reached an unrelated IO error")
+ info = os.statvfs(folder)
+ if info.f_bfree == 0:
+ break
+ require(type(info.f_frsize) is int and 0 < info.f_frsize <= chunk,
+ "bounded fill has no valid minimum allocation unit")
+ if info.f_frsize < chunk:
+ chunk = info.f_frsize
+ continue
+ # A separate private inode may allocate a final block that
+ # the current file cannot extend into. Never retry an empty
+ # inode or exceed the fixture's 128 padding-inode budget.
+ require(file_written > 0 and count < IMAGE_BYTES // 1024**2 - 1,
+ f"bounded minimum-block fill left {info.f_bfree} free blocks")
+ os.close(fd)
+ fd = None
+ fd = os.open(padding / f"inode-{count:04d}", os.O_CREAT | os.O_EXCL | os.O_WRONLY | os.O_NOFOLLOW, 0o600)
+ count += 1
+ file_written = 0
+ finally:
+ if fd is not None:
+ os.close(fd)
+ else:
+ require(branch == "bounded-inode-exhaustion", "unreviewed bounded fill mode")
+ while True:
+ require(count < MAX_INODES and time.monotonic() < deadline, "bounded inode fill exceeded its budget")
+ try:
+ fd = os.open(padding / f"inode-{count:04d}", os.O_CREAT | os.O_EXCL | os.O_WRONLY | os.O_NOFOLLOW, 0o600)
+ except OSError as error:
+ require(error.errno == errno.ENOSPC, "bounded inode fill reached an unrelated IO error")
+ break
+ os.fsync(fd)
+ os.close(fd)
+ count += 1
+ info = os.statvfs(folder)
+ require(info.f_bfree == 0 if branch == "bounded-volume-enospc" else info.f_ffree == 0,
+ "bounded fill did not exhaust its registered resource")
+ return {"allocatedFillerBytes": written, "allocatedFillerInodes": count, "actualErrno": errno.ENOSPC,
+ "freeBlocks": info.f_bfree, "freeInodes": info.f_ffree}
+
+
+def release_filler(folder):
+ padding = folder / "padding"
+ entries = list(padding.iterdir())
+ require(len(entries) <= MAX_INODES and all(path.name == "blocks.bin" or re.fullmatch(r"inode-[0-9]{4}", path.name)
+ for path in entries), "unrecognized file in bounded filler; retain namespace")
+ for path in entries:
+ require(regular(path).st_size <= IMAGE_BYTES, "bounded filler grew outside its limit")
+ path.unlink()
+ descriptor = os.open(padding, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
+ try:
+ os.fsync(descriptor)
+ finally:
+ os.close(descriptor)
+ info = os.statvfs(folder)
+ require(info.f_bfree > 0 and info.f_ffree > 0, "bounded resource was not released")
+ return {"removedOwnedFillerFiles": len(entries), "freeBlocks": info.f_bfree, "freeInodes": info.f_ffree}
+
+
+def witness(state, roots, owner, pid, *, exhausted):
+ actual = views(state, roots, owner)
+ namespace = Path(f"/proc/{pid}/ns/mnt")
+ require(pid > 1 and namespace.readlink() != Path('/proc/self/ns/mnt').readlink(),
+ "bounded witness lacks a private candidate mount namespace")
+ fields = dict(line.split(":", 1) for line in Path(f"/proc/{pid}/status").read_text().splitlines() if ":" in line)
+ require(list(map(int, fields["Uid"].split()))[1] == list(map(int, fields["Gid"].split()))[1] == 0,
+ "bounded witness service UID/GID changed")
+ branch = read(state, roots, owner)["branch"]
+ rows = []
+ for disk, (root, view) in enumerate(zip(roots, actual, strict=True)):
+ raw = json.loads(command("nsenter", "--mount=" + str(namespace), "python3", "-c", PROBE, str(root / "data")))
+ validate_probe(raw, view.stat().st_dev, branch, exhausted)
+ rows.append({"disk": disk, **raw, "sameServiceUidAndMountNamespace": True})
+ require(all(shutil.disk_usage(root).free >= MIN_SHARED_FREE for root in roots),
+ "shared backing drive crossed its retained safety margin")
+ return {"branch": branch, "disks": rows, "hostSharedMountRemounted": False,
+ "realMediaDamageClaimed": False, "sharedDriveSafetyMarginBytes": MIN_SHARED_FREE}
+
+
+def validate_probe(raw, device, branch, exhausted):
+ require(branch in {"bounded-volume-enospc", "bounded-inode-exhaustion"} and type(exhausted) is bool
+ and all(type(raw.get(name)) is int and raw[name] >= 0 for name in
+ ("uid", "gid", "device", "writeErrno", "freeBlocks", "freeInodes", "availableBlocks")),
+ "bounded probe omitted a real integer resource/identity value")
+ require(raw["uid"] == raw["gid"] == 0 and raw["device"] == device
+ and raw["writeErrno"] == (errno.ENOSPC if exhausted else 0),
+ "candidate mount/UID did not witness the exact bounded write error")
+ if exhausted:
+ require(raw["freeBlocks"] == 0 if branch == "bounded-volume-enospc" else raw["freeInodes"] == 0,
+ "candidate resource view differs from the exhausted filesystem")
+ else:
+ require(raw["freeBlocks"] > 0 and raw["freeInodes"] > 0, "released candidate resource remains exhausted")
+
+
+def dispose(state, roots, owner):
+ value = read(state, roots, owner)
+ if value is None:
+ return {"boundedVolumesAbsent": True}
+ for row in value["volumes"]:
+ image, folder = Path(row["image"]), Path(row["mount"])
+ if image.exists():
+ info = regular(image)
+ require(info.st_size in {0, IMAGE_BYTES}, "partially allocated image has unrecognized size")
+ entry = mounted(row)
+ if entry is not None:
+ command("umount", str(folder))
+ require(mounted(row) is None, "bounded filesystem remains mounted")
+ loop = loop_for(image)
+ if loop:
+ command("losetup", "--detach", loop)
+ require(loop_for(image) is None, "bounded loop remains attached")
+ image.unlink()
+ else:
+ require(not folder.exists() or not os.path.ismount(folder), "missing image has a retained mount")
+ if folder.exists():
+ require(not folder.is_symlink() and not any(folder.iterdir()), "bounded mountpoint is not empty after detach")
+ folder.rmdir()
+ if (state / "bounded").exists():
+ (state / "bounded").rmdir()
+ (state / MARKER).unlink()
+ return {"boundedVolumesRemoved": True, "originalSharedMountsRetained": True}
diff --git a/scripts/validation_heal/bucket_metadata.py b/scripts/validation_heal/bucket_metadata.py
new file mode 100644
index 00000000..0f8ec4c6
--- /dev/null
+++ b/scripts/validation_heal/bucket_metadata.py
@@ -0,0 +1,340 @@
+#!/usr/bin/env python3
+"""D11: bucket-config recovery and unknown-envelope refusal on H1/H2."""
+
+import argparse
+from datetime import datetime, timedelta, timezone
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import secrets
+import signal
+import sys
+import time
+from urllib.parse import quote
+
+try:
+ from .heal_probe import Probe, compare_inventories, payload, require, save
+ from .heal_workload import Recorder, verify_seed
+ from .healthy import BASE_OPTIONS, Fleet, physical_content, validate_topology
+ from .inspector import verify as verify_inspector
+ from .matrix import select
+ from .bucket_records import bucket_path, IAM_SENSITIVE_SUBTREES
+ from .versions import validate_version_receipts
+ from .stop import idle_fleet
+except ImportError:
+ from heal_probe import Probe, compare_inventories, payload, require, save
+ from heal_workload import Recorder, verify_seed
+ from healthy import BASE_OPTIONS, Fleet, physical_content, validate_topology
+ from inspector import verify as verify_inspector
+ from matrix import select
+ from bucket_records import bucket_path, IAM_SENSITIVE_SUBTREES
+ from versions import validate_version_receipts
+ from stop import idle_fleet
+
+
+CASE_ID = "heal.bucket-metadata-integrity"
+BRANCHES = ("versioning-config", "bucket-policy-tags", "lock-config", "unknown-record-negative")
+REFERENCE = {"candidateCommit": "072b57b37559fbe626aeda8abf61617fdcbdd926", "sourceFiles": {
+ "crates/ecstore/src/bucket/metadata.rs": "3b4f9ed4dba259f259653ba4bf1568c54a10b71ff0f6111135394e225a38d990",
+ "crates/filemeta/src/filemeta/codec.rs": "ef82b3390b0a377e423f99fb384c6a8a96beaf5722b0c907852cc0c904c4f04d",
+ "crates/iam/src/manager.rs": "c58d60946204973f8c5cfc81f08130614abb51102f31de0494ba4e22b88aa97c"},
+ "physicalRecord": ".rustfs.sys/buckets//.metadata.bin/xl.meta",
+ "representation": "system object XL2 envelope, not a raw JSON or raw inner bucket-msgpack file",
+ "unknownNegative": "outer XL2 major 1 -> 2; all other bytes retained; decoder rejects before checksum",
+ "repairSourcesAllowed": ["candidate startup/config read", "explicit bucket Heal"],
+ "credentialBoundary": "fresh owned root-only service; exact non-sensitive IAM format read-only; sensitive subtrees absent and never traversed"}
+GETTERS = ("get_bucket_versioning", "get_bucket_policy", "get_bucket_tagging", "get_object_lock_configuration")
+
+
+def seed_fixture(recorder, buckets):
+ expected = {}
+ for bucket in buckets:
+ recorder.call("create_bucket", {"Bucket": bucket, "ObjectLockEnabledForBucket": True})
+ recorder.call("put_bucket_versioning", {"Bucket": bucket, "VersioningConfiguration": {"Status": "Enabled"}})
+ expected[bucket] = []
+ for key in ("objects/target.bin", "objects/control.bin"):
+ for nonce in range(2):
+ body = payload(f"D11/{bucket}/{key}/{nonce}", 1024**2 + 97)
+ response = recorder.call("put_object", {"Bucket": bucket, "Key": key,
+ "Metadata": {"suite": "D11", "nonce": str(nonce)}, "Tagging": "suite=heal&fixture=DS"}, body)
+ version = response.get("VersionId")
+ require(isinstance(version, str) and version not in {"", "null"}, "bucket fixture lacks a version ACK")
+ expected[bucket].append({"key": key, "versionId": version, "isLatest": nonce == 1, "deleteMarker": False,
+ "payload": {"size": len(body), "sha256": hashlib.sha256(body).hexdigest()}})
+ document = {"Version": "2012-10-17", "Statement": [{"Effect": "Allow", "Principal": "*",
+ "Action": "s3:GetObject", "Resource": f"arn:aws:s3:::{bucket}/objects/*"}]}
+ recorder.call("put_bucket_policy", {"Bucket": bucket, "Policy": json.dumps(document)})
+ recorder.call("put_bucket_tagging", {"Bucket": bucket, "Tagging": {"TagSet": [
+ {"Key": "suite", "Value": "D11"}, {"Key": "bucket", "Value": bucket}]}})
+ # Configure defaults AFTER writes. The historical fixture has no
+ # retained objects, so namespace finalization cannot bypass retention.
+ recorder.call("put_object_lock_configuration", {"Bucket": bucket, "ObjectLockConfiguration": {
+ "ObjectLockEnabled": "Enabled", "Rule": {"DefaultRetention": {"Mode": "GOVERNANCE", "Days": 1}}}})
+ return expected
+
+
+def configuration(observed, operation):
+ require(observed.get("acknowledged") is True and observed.get("resultUnknown") is False,
+ "bucket configuration read failed or has an unknown outcome")
+ response = {key: value for key, value in observed["response"].items() if key != "ResponseMetadata"}
+ if operation == "get_bucket_policy":
+ response["Policy"] = json.loads(response["Policy"])
+ if operation == "get_bucket_tagging":
+ response["TagSet"] = sorted(response["TagSet"], key=lambda tag: (tag["Key"], tag["Value"]))
+ require(bool(response), "bucket configuration silently became an empty default")
+ return response
+
+
+def configs(probe, fleet, bucket, label):
+ return {op: configuration(fleet.record(label, {"bucket": bucket, **probe.s3(op, {"Bucket": bucket})}), op)
+ for op in GETTERS}
+
+
+def system_snapshot(fleet, buckets, label, faults=None):
+ return fleet.record(label, {node: fleet.remote(node, "bucket-record-snapshot", buckets=buckets,
+ metadataFaults=(faults or {}).get(node, {})) for node in fleet.nodes})
+
+
+def system_files(value):
+ result = {(node, row["disk"], row["path"]): {name: row[name] for name in ("size", "sha256")}
+ for node, sample in value.items() for row in sample["files"]}
+ require(len(result) == sum(len(sample["files"]) for sample in value.values()), "duplicate system file identity")
+ require(all(sample["credentialsRead"] is False and bool(sample["iamNamespace"])
+ and all(row["traversed"] is False and row["formatOnlyRead"] is True
+ and row["sensitiveSubtrees"] == dict.fromkeys(IAM_SENSITIVE_SUBTREES, False)
+ for row in sample["iamNamespace"]) for sample in value.values()), "credential boundary changed")
+ return result
+
+
+def record_plan(snapshot, bucket, topology, physical):
+ path = bucket_path(bucket, bucket) + "/xl.meta"
+ targets = [(node, row) for node, sample in snapshot.items() for row in sample["files"] if row["path"] == path]
+ require(len(targets) == (4 if topology == "H1" else 16), "bucket config lacks every original member")
+ geometry, indices, latest = None, set(), None
+ for node, row in targets:
+ metadata = row.get("metadata") or {}
+ require(metadata.get("bucket") == ".rustfs.sys" and metadata.get("key") == f"buckets/{bucket}/.metadata.bin",
+ "system object decoder identity differs")
+ versions = metadata.get("versions", [])
+ require(len(versions) == 1 and versions[0].get("deleted") is False and versions[0].get("isLatest") is True,
+ "bucket record must have one authoritative live version")
+ version = versions[0]
+ erasure = version["erasure"]
+ current = {key: erasure[key] for key in ("data_blocks", "parity_blocks", "distribution", "block_size", "algorithm")}
+ require(geometry is None or current == geometry, "bucket config lacks one erasure authority")
+ geometry = current
+ layouts = [record["value"]["xl"] for record in physical[node]["formats"] if record["disk"] == row["disk"]]
+ require(len(layouts) == 1 and len(layouts[0]["sets"]) == 1,
+ "system object member lacks its actual disk/set format")
+ layout = layouts[0]
+ require(erasure["index"] == erasure["distribution"][layout["sets"][0].index(layout["this"])],
+ "system shard index differs from its physical disk UUID/set slot")
+ require(erasure["index"] not in indices and version.get("size", 0) > 4, "duplicate or empty config shard")
+ indices.add(erasure["index"])
+ identity = version.get("versionId"), version.get("dataDir"), version["size"], version["parts"]
+ require(latest is None or identity == latest, "bucket-config version/dataDir differs across members")
+ latest = identity
+ require(geometry["data_blocks"] + geometry["parity_blocks"] == len(targets)
+ and indices == set(range(1, len(targets) + 1)) and geometry["parity_blocks"] >= 1,
+ "system record geometry lacks one recoverable-member loss")
+ return targets
+
+
+def refusal(observed):
+ response = observed.get("response") or {}
+ require(observed.get("acknowledged") is False and observed.get("resultUnknown") is False
+ and response.get("ResponseMetadata", {}).get("HTTPStatusCode") in {500, 503}
+ and response.get("Error", {}).get("Code") in {"InternalError", "XMinioReadQuorum", "ServiceUnavailable"},
+ "unknown bucket-config envelope was served as a default or lacks a structured server refusal")
+
+
+def bucket_heal(fleet, probe, bucket, active, *, negative=False):
+ path = "/rustfs/admin/v3/heal/" + quote(bucket, safe="")
+ admitted = fleet.record("bucket-heal-admission", probe.admin(path, [], json.dumps({**BASE_OPTIONS, "recursive": True}).encode()))
+ require(admitted.get("httpStatus") == 200 and isinstance(admitted.get("response"), dict), "bucket Heal was not admitted")
+ token = admitted["response"].get("clientToken")
+ require(isinstance(token, str) and token, "bucket Heal omitted task identity")
+ active.append((path, token))
+ deadline = time.monotonic() + 180
+ while True:
+ raw = fleet.record("bucket-heal-status", probe.admin(path, [("clientToken", token)], b""))
+ require(raw.get("httpStatus") == 200 and isinstance(raw.get("response"), dict), "bucket Heal lost its task")
+ current = raw["response"]
+ if current.get("summary") != "running":
+ break
+ require(time.monotonic() < deadline, "bucket Heal exceeded its fixed deadline")
+ time.sleep(.2)
+ active.remove((path, token))
+ if negative:
+ outcome = current.get("outcome", {})
+ require(outcome.get("execution", {}).get("state") in {"failed", "completed_with_errors"}
+ and outcome.get("counters", {}).get("healed") == 0,
+ "unknown record became successful bucket Heal responsibility")
+ return current
+
+
+def run_branch(args, topology, nodes, branch):
+ directory = args.evidence / topology / branch
+ directory.mkdir(parents=True)
+ run_id = f"gh-{os.environ['GITHUB_RUN_ID']}-{os.environ.get('GITHUB_RUN_ATTEMPT', '1')}-{topology.lower()}"
+ fleet = Fleet(nodes, directory, run_id, args.tool_directory)
+ bucket = "rv-heal-" + hashlib.sha256(run_id.encode()).hexdigest()[:16]
+ buckets = [bucket, bucket + "-control"]
+ fleet.snapshot_buckets = buckets
+ candidate = json.loads(args.candidate.read_text())
+ require(candidate["source_sha"] == REFERENCE["candidateCommit"], "bucket-config schema contract is unreviewed for this candidate")
+ access, secret = secrets.token_hex(16), secrets.token_hex(24)
+ os.environ.update(HEAL_ACCESS_KEY=access, HEAL_SECRET_KEY=secret)
+ binding = {"schemaVersion": 1, "runId": run_id, "jumpHostOnly": True, "exclusiveHealService": True,
+ "leaseExpiresAt": (datetime.now(timezone.utc) + timedelta(minutes=30)).isoformat(), "bucketPrefix": bucket,
+ "endpoints": {f"node{index}": f"http://rustfs-node{index}:9000" for index in range(1, len(nodes) + 1)},
+ "defaultEndpoint": "node1", "candidateCommit": candidate["source_sha"], "harnessCommit": args.testing_commit,
+ "candidatePackageSha256": candidate["package_sha256"], "candidateBinarySha256": args.binary_sha,
+ "rcBinarySha256": hashlib.sha256(args.rc_bin.read_bytes()).hexdigest(), "maxInventoryObjects": 32,
+ "maxInventoryBytes": 64 * 1024**2}
+ fleet.record("source-bound-system-record-contract", REFERENCE)
+ fleet.record("inspector-build", verify_inspector(args.tool_directory, candidate))
+ fleet.record("binding", binding)
+ probe, active = Probe(binding), []
+ result = {"topology": topology, "branch": branch, "verdict": "RUNNING", "cleanup": "PENDING"}
+ try:
+ fleet.deploy(args.binary_sha, bucket, access, secret)
+ expected = seed_fixture(Recorder(probe, directory / "fixture"), buckets)
+ baseline_config = {name: configs(probe, fleet, name, "config-baseline") for name in buckets}
+ baseline_s3 = {name: fleet.record("S3-baseline", probe.inventory(name)) for name in buckets}
+ for name in buckets:
+ fleet.record("ACK-manifest", {"bucket": name, "expected": expected[name], **verify_seed(expected[name], baseline_s3[name])})
+ fleet.stop()
+ original = fleet.snapshot("physical-baseline", quiescent=True)
+ validate_topology(original, topology)
+ records = system_snapshot(fleet, buckets, "system-record-baseline")
+ targets = record_plan(records, bucket, topology, original)
+ record_plan(records, buckets[1], topology, original)
+ negative = branch == "unknown-record-negative"
+ selected = targets if negative else targets[:1]
+ kind = "future-xl-schema" if negative else "truncate" if branch == "bucket-policy-tags" else "missing"
+ fault_map, registered = {}, []
+ wanted_fault = system_files(records).copy()
+ for index, (node, target) in enumerate(selected):
+ params = {"disk": target["disk"], "faultId": f"D11-{branch}-{index}"}
+ fleet.bucket_record_faults.append((node, params))
+ registered.append((node, params))
+ receipt = fleet.record("exact-system-record-fault", fleet.remote(node, "bucket-record-apply", **params,
+ bucket=bucket, relativePath=target["path"], expectedSha256=target["sha256"], kind=kind))
+ pair = node, target["disk"], target["path"]
+ if receipt["fault"] is None:
+ wanted_fault.pop(pair)
+ else:
+ wanted_fault[pair] = receipt["fault"]
+ fault_map.setdefault(node, {})[f"{target['disk']}:{target['path']}"] = receipt["fault"]
+ fault_scene = system_snapshot(fleet, buckets, "system-record-fault-witness", fault_map)
+ require(system_files(fault_scene) == wanted_fault, "system fault changed an unselected record")
+ require(physical_content(original) == physical_content(fleet.snapshot("business-fault-control", quiescent=True)),
+ "system fault changed business objects or disk formats")
+ fleet.start()
+ if negative:
+ for endpoint in binding["endpoints"]:
+ observer = Probe(binding, endpoint)
+ for operation in GETTERS:
+ refusal(fleet.record("unknown-record-config-refusal", observer.s3(operation, {"Bucket": bucket})))
+ require(configs(observer, fleet, buckets[1], "healthy-config-control") == baseline_config[buckets[1]],
+ "unknown primary record changed the control bucket")
+ # Public refusal and untouched raw unknown records are the oracle;
+ # startup failure alone cannot satisfy this branch.
+ task = bucket_heal(fleet, probe, bucket, active, negative=True)
+ else:
+ task = bucket_heal(fleet, probe, bucket, active)
+ validate_version_receipts(task, bucket, expected[bucket], 2)
+ for endpoint in binding["endpoints"]:
+ for name in buckets:
+ require(configs(Probe(binding, endpoint), fleet, name, "post-repair-config") == baseline_config[name],
+ "versioning/policy/tags/lock config changed after record repair")
+ idle_fleet(fleet, binding)
+ fleet.stop()
+ after = system_snapshot(fleet, buckets, "system-record-before-rescue-and-business-GET", fault_map if negative else None)
+ require(system_files(after) == (wanted_fault if negative else system_files(records)),
+ "unknown record was rewritten or recovery failed to restore complete original config bytes")
+ require(physical_content(original) == physical_content(fleet.snapshot("physical-before-GET", quiescent=True)),
+ "bucket metadata recovery changed business versions or disk formats")
+ # Positive repair is already witnessed. Negative rescue is separately
+ # recorded and is never counted as candidate repair.
+ for node, params in registered:
+ if negative:
+ rescue = fleet.record("unknown-record-backup-rescue", fleet.remote(node, "bucket-record-recover", **params))
+ require(rescue["physicalRepairPassAllowed"] is False, "backup rescue claimed repair")
+ fleet.bucket_record_faults.remove((node, params))
+ fleet.start()
+ for endpoint in binding["endpoints"]:
+ observer = Probe(binding, endpoint)
+ for name in buckets:
+ require(configs(observer, fleet, name, "final-config") == baseline_config[name], "final config differs")
+ actual = fleet.record("all-peer-S3-after", {"endpoint": endpoint, **observer.inventory(name)})
+ fleet.record("full-S3-proof", {**verify_seed(expected[name], actual), **compare_inventories(baseline_s3[name], actual)})
+ fleet.stop()
+ require(system_files(system_snapshot(fleet, buckets, "final-system-records")) == system_files(records),
+ "final system records differ from same-run baseline")
+ require(physical_content(original) == physical_content(fleet.snapshot("final-business-physical", quiescent=True)),
+ "business reads changed the proven result")
+ result.update(verdict="PASS", configAPIs=list(GETTERS), completeSystemBytes=True, taskId=task.get("clientToken"),
+ unknownSchemaRefused=negative, backupRescueExcludedFromRepair=negative, credentialsRead=False)
+ except (Exception, KeyboardInterrupt) as exc:
+ result.update(verdict="FAIL", failure={"type": type(exc).__name__, "message": str(exc)})
+ try:
+ fleet.stop()
+ fleet.snapshot("physical-failure", quiescent=True)
+ except Exception as error:
+ result["failureEvidenceError"] = str(error)
+ finally:
+ for path, token in active:
+ try:
+ fleet.record("stop-unresolved", probe.admin(path, [("clientToken", token), ("forceStop", "true")], b""))
+ except Exception as exc:
+ fleet.record("stop-error", {"error": str(exc)})
+ result["cleanup"] = "PASS" if fleet.cleanup() else "FAIL"
+ fleet.record("branch-result", result)
+ return result
+
+
+def main():
+ def interrupted(signum, frame):
+ raise KeyboardInterrupt(f"bucket metadata validation interrupted by signal {signum}")
+ signal.signal(signal.SIGTERM, interrupted)
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--candidate", type=Path, required=True)
+ parser.add_argument("--case-id", choices=(CASE_ID,), required=True)
+ parser.add_argument("--testing-commit", required=True)
+ parser.add_argument("--binary-sha", required=True)
+ parser.add_argument("--rc-bin", type=Path, required=True)
+ parser.add_argument("--evidence", type=Path, required=True)
+ parser.add_argument("--tool-directory", type=Path, required=True)
+ args = parser.parse_args()
+ nodes = os.environ.get("RUSTFS_NODES", "vm000 vm001 vm002 vm003").split()
+ require(len(nodes) >= 4 and len(nodes) == len(set(nodes)) and all(re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", node) for node in nodes),
+ "four original Heal nodes required")
+ require(re.fullmatch(r"[1-9][0-9]*", os.environ.get("GITHUB_RUN_ID", ""))
+ and re.fullmatch(r"[0-9a-f]{64}", args.binary_sha), "immutable workflow/candidate identities required")
+ require(select(CASE_ID)[0]["variants"] == list(BRANCHES), "bucket metadata executor omitted a source branch")
+ results = []
+ try:
+ for topology, selected_nodes in (("H1", nodes[:1]), ("H2", nodes[:4])):
+ for branch in BRANCHES:
+ results.append(run_branch(args, topology, selected_nodes, branch))
+ if results[-1]["cleanup"] != "PASS":
+ break
+ if results[-1]["cleanup"] != "PASS":
+ break
+ finally:
+ complete = len(results) == 8 and all(row["verdict"] == "PASS" and row["cleanup"] == "PASS" for row in results)
+ save(args.evidence / "healthy-summary.json", {"caseId": CASE_ID, "results": results,
+ "cleanup": "PASS" if results and all(row["cleanup"] == "PASS" for row in results) else "FAIL", "complete": complete})
+ require(complete, "D11 requires all eight H1/H2 record recovery/refusal instances and clean finalization")
+
+
+if __name__ == "__main__":
+ try:
+ main()
+ except Exception as exc:
+ print(f"Heal bucket metadata validation failed: {exc}", file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_heal/bucket_records.py b/scripts/validation_heal/bucket_records.py
new file mode 100644
index 00000000..c69ce467
--- /dev/null
+++ b/scripts/validation_heal/bucket_records.py
@@ -0,0 +1,198 @@
+"""D11 node backend: exact owned bucket-config records, never an IAM walk."""
+
+import json
+import os
+import re
+import uuid
+
+
+IAM_SENSITIVE_SUBTREES = ("users", "service-accounts", "sts", "groups", "policies", "policydb")
+IAM_FORMAT_KEY = "config/iam/format.json"
+
+
+def require(value, message):
+ if not value:
+ raise ValueError(message)
+
+
+def bucket_path(bucket, prefix):
+ require(isinstance(bucket, str) and (bucket == prefix or bucket.startswith(prefix + "-"))
+ and re.fullmatch(r"[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]", bucket),
+ "system record is outside the exact owned bucket prefix")
+ return f".rustfs.sys/buckets/{bucket}/.metadata.bin"
+
+
+def record_parts(relative, bucket, prefix):
+ base = bucket_path(bucket, prefix)
+ require(relative == base + "/xl.meta", "only the owned bucket-config xl.meta is mutable")
+ return relative.split("/")
+
+
+def changed_bytes(before, kind):
+ require(kind in {"missing", "truncate", "future-xl-schema"}, "unsupported bucket-record fault")
+ require(len(before) >= 8 and before[:4] == b"XL2 " and int.from_bytes(before[4:6], "little") == 1,
+ "bucket record is not the reviewed candidate XL2 major 1 envelope")
+ if kind == "missing":
+ return None
+ if kind == "truncate":
+ return before[:len(before) // 2]
+ # The candidate rejects an outer major above 1 before decoding/checksums.
+ # Preserve every other byte; this is not a fabricated inner msgpack record.
+ return before[:4] + (2).to_bytes(2, "little") + before[6:]
+
+
+def apply(root_fd, owner, fault, request):
+ bucket, identity = request["bucket"], request["faultId"]
+ parts = record_parts(request["relativePath"], bucket, owner["bucketPrefix"])
+ require(re.fullmatch(r"D11-[a-z0-9-]{1,48}", identity), "invalid bucket-record fault ID")
+ require(re.fullmatch(r"[0-9a-f]{64}", request["expectedSha256"]), "missing frozen record digest")
+ with fault.parent(root_fd, ["data", *parts[:-1]]) as target, fault.parent(root_fd, ["quarantine"]) as backup:
+ before = fault.read_file(target, parts[-1], 4 * 1024**2)
+ require(fault.digest(before) == request["expectedSha256"], "bucket record changed before injection")
+ changed = changed_bytes(before, request["kind"])
+ receipt = {"schemaVersion": 1, "runId": owner["runId"], "bucket": bucket, "faultId": identity,
+ "relativePath": request["relativePath"], "kind": request["kind"], "backup": identity + ".original",
+ "before": {"sha256": fault.digest(before), "size": len(before)},
+ "fault": None if changed is None else {"sha256": fault.digest(changed), "size": len(changed)},
+ "backend": "exact-owned-bucket-config-envelope", "requiresQuiescentService": True,
+ "credentialsRead": False, "innerMetadataPayloadRewritten": False}
+ fault.new_file(backup, identity + ".intent.json", fault.encode(receipt))
+ fault.new_file(backup, receipt["backup"], before)
+ require(fault.read_file(target, parts[-1]) == before, "record changed after its intent")
+ if changed is None:
+ os.unlink(parts[-1], dir_fd=target)
+ else:
+ temporary = ".bucket-fault-" + identity
+ fault.new_file(target, temporary, changed)
+ os.rename(temporary, parts[-1], src_dir_fd=target, dst_dir_fd=target)
+ os.fsync(target)
+ if changed is None:
+ try:
+ os.stat(parts[-1], dir_fd=target, follow_symlinks=False)
+ except FileNotFoundError:
+ pass
+ else:
+ raise ValueError("bucket record removal was not witnessed")
+ else:
+ require(fault.read_file(target, parts[-1]) == changed, "bucket record mutation was not witnessed")
+ fault.new_file(backup, identity + ".applied.json", fault.encode(receipt))
+ return receipt
+
+
+def recover(root_fd, owner, fault, request):
+ identity = request["faultId"]
+ require(re.fullmatch(r"D11-[a-z0-9-]{1,48}", identity), "invalid bucket-record recovery ID")
+ with fault.parent(root_fd, ["quarantine"]) as backup:
+ receipt = json.loads(fault.read_file(backup, identity + ".intent.json", 16384))
+ require(receipt["runId"] == owner["runId"] and receipt["faultId"] == identity
+ and receipt["backup"] == identity + ".original", "bucket-record recovery identity differs")
+ parts = record_parts(receipt["relativePath"], receipt["bucket"], owner["bucketPrefix"])
+ before = fault.read_file(backup, receipt["backup"])
+ require({"sha256": fault.digest(before), "size": len(before)} == receipt["before"], "record backup changed")
+ with fault.parent(root_fd, ["data", *parts[:-1]]) as target:
+ try:
+ current = fault.read_file(target, parts[-1])
+ except FileNotFoundError:
+ current = None
+ restored = current != before
+ if restored:
+ require(current is None or {"sha256": fault.digest(current), "size": len(current)} == receipt["fault"],
+ "bucket record has an unrecognized post-fault rewrite; retain namespace")
+ if current is not None:
+ name = identity + ".failed-" + fault.digest(current)
+ try:
+ fault.new_file(backup, name, current)
+ except FileExistsError:
+ require(fault.read_file(backup, name) == current, "preserved record differs")
+ temporary = ".bucket-rescue-" + identity
+ try:
+ fault.new_file(target, temporary, before)
+ except FileExistsError:
+ require(fault.read_file(target, temporary) == before, "interrupted rescue bytes differ")
+ os.rename(temporary, parts[-1], src_dir_fd=target, dst_dir_fd=target)
+ os.fsync(target)
+ require(fault.read_file(target, parts[-1]) == before, "record rescue did not restore exact baseline")
+ return {"faultId": identity, "restored": restored, "physicalRepairPassAllowed": False,
+ "restorationIsProductRepair": False}
+
+
+def snapshot(roots, owner, request, checked, digest, tool, command):
+ buckets = request["buckets"]
+ require(isinstance(buckets, list) and len(buckets) == 2 and len(set(buckets)) == 2
+ and owner["bucketPrefix"] in buckets, "exact primary/control bucket pair required")
+ for bucket in buckets:
+ bucket_path(bucket, owner["bucketPrefix"])
+ entries, iam = [], []
+ for disk, root in enumerate(roots):
+ data = root / "data"
+ # Startup writes the non-sensitive IAM format object even for a fresh
+ # root-only service. Check sensitive subtrees by exact path only; never
+ # enumerate the IAM namespace or read any credential/config payload.
+ iam_root = checked(data / ".rustfs.sys/config/iam", data)
+ sensitive = {name: checked(iam_root / name, data).exists() for name in IAM_SENSITIVE_SUBTREES}
+ require(not any(sensitive.values()), "D11 requires no persisted sensitive IAM subtree")
+ iam.append({"disk": disk, "present": iam_root.exists(), "traversed": False,
+ "sensitiveSubtrees": sensitive, "formatOnlyRead": True})
+ objects = [(bucket, f"buckets/{bucket}/{name}", "bucket-record") for bucket in buckets
+ for name in (".metadata.bin", ".bucket-incarnation")]
+ objects.append((None, IAM_FORMAT_KEY, "iam-format"))
+ for bucket, key, record_class in objects:
+ base = ".rustfs.sys/" + key
+ folder = checked(data / base, data)
+ if not folder.exists():
+ continue
+ for directory, children, names in os.walk(folder, followlinks=False):
+ for name in (*children, *names):
+ checked(type(folder)(directory) / name, data)
+ for name in sorted(names):
+ path = type(folder)(directory) / name
+ relative = path.relative_to(folder).as_posix()
+ segments = relative.split("/")
+ valid = relative == "xl.meta" or len(segments) == 2 and bool(re.fullmatch(
+ r"part\.[1-9][0-9]*(?:\.integrity\.[a-z0-9-]+)?", segments[1]))
+ if len(segments) == 2:
+ require(str(uuid.UUID(segments[0])) == segments[0], "noncanonical bucket-config dataDir")
+ require(valid and len(entries) < 128 and path.stat().st_size <= 4 * 1024**2,
+ "bucket-config record exceeds the exact file/byte allowlist")
+ entry = {"disk": disk, "bucket": bucket, "recordClass": record_class, "path": str(path.relative_to(data)),
+ "size": path.stat().st_size, "sha256": digest(path)}
+ if relative == "xl.meta":
+ decoded = command(str(tool), str(path), ".rustfs.sys", key, check=False)
+ if decoded.returncode == 0:
+ entry["metadata"] = json.loads(decoded.stdout)
+ require(entry["metadata"].get("bucket") == ".rustfs.sys"
+ and entry["metadata"].get("key") == key, "system decoder identity differs")
+ versions = entry["metadata"].get("versions", [])
+ require(len(versions) == 1 and not versions[0].get("deleted"),
+ "system streaming proof requires one live record")
+ version = versions[0]
+ version_id = version.get("versionId") or "null"
+ args = [str(tool), "--verify-inline", str(path), ".rustfs.sys",
+ key, version_id]
+ if version.get("inline") is not None:
+ entry["streaming"] = [json.loads(command(*args).stdout)]
+ else:
+ data_dir = version.get("dataDir")
+ require(isinstance(data_dir, str) and str(uuid.UUID(data_dir)) == data_dir,
+ "system record lacks a canonical external dataDir")
+ proofs = []
+ for part in version["parts"]:
+ number = part["number"]
+ require(type(number) is int and 0 < number <= 8, "unbounded system record part")
+ part_path = checked(folder / data_dir / f"part.{number}", data)
+ proofs.append(json.loads(command(str(tool), "--verify-part", str(path), ".rustfs.sys",
+ key, str(part_path), version_id, str(number)).stdout))
+ require(proofs, "system record has no streaming part proof")
+ entry["streaming"] = proofs
+ require(all(proof.get("physicalBitrotVerified") is True and proof.get("bucket") == ".rustfs.sys"
+ and proof.get("key") == key for proof in entry["streaming"]),
+ "system record streaming proof differs from its exact identity")
+ else:
+ expected = request.get("metadataFaults", {}).get(f"{disk}:{entry['path']}")
+ require(record_class == "bucket-record"
+ and expected == {name: entry[name] for name in ("size", "sha256")},
+ "bucket record decoding failed outside its exact witnessed mutation")
+ entry["decodeFailure"] = True
+ entries.append(entry)
+ return {"files": entries, "iamNamespace": iam, "credentialsRead": False,
+ "scope": "two exact owned bucket metadata object directories plus read-only IAM format; no IAM namespace walk"}
diff --git a/scripts/validation_heal/caller_abort.py b/scripts/validation_heal/caller_abort.py
new file mode 100644
index 00000000..3b68a91e
--- /dev/null
+++ b/scripts/validation_heal/caller_abort.py
@@ -0,0 +1,149 @@
+#!/usr/bin/env python3
+"""Disconnect one signed, exact-version GET; never infer server ownership."""
+
+import argparse
+import hashlib
+import http.client
+import json
+import os
+from pathlib import Path
+import socket
+import time
+from urllib.parse import quote, urlencode, urlsplit
+import uuid
+
+try:
+ from .heal_probe import CLOCK_DOMAIN, Probe, canonical, require, save, validate_binding, validate_s3_request
+except ImportError:
+ from heal_probe import CLOCK_DOMAIN, Probe, canonical, require, save, validate_binding, validate_s3_request
+
+
+def disconnect_get(probe, bucket, key, version_id, output, *, before_disconnect=None,
+ receive_buffer_bytes=None):
+ """Record intent, send once, close the caller socket, retain UNKNOWN.
+
+ The caller must independently establish server admission, lock ownership,
+ drain and final object/FD state. Sending bytes alone proves none of these.
+ A populated output directory prevents accidental replay of this request.
+ """
+ validate_binding(probe.binding)
+ require(receive_buffer_bytes is None or type(receive_buffer_bytes) is int
+ and 4096 <= receive_buffer_bytes <= 1024**2,
+ 'invalid bounded caller receive buffer')
+ require(isinstance(key, str) and 0 < len(key.encode()) <= 1024, 'bounded exact object key required')
+ require(isinstance(version_id, str) and str(uuid.UUID(version_id)) == version_id
+ and uuid.UUID(version_id).int != 0, 'exact nonnil historical version required')
+ params = {'Bucket': bucket, 'Key': key, 'VersionId': version_id}
+ validate_s3_request(probe.binding, 'get_object', params)
+ bound = canonical(probe.binding)
+ require(probe.endpoint_name in probe.binding['endpoints']
+ and probe.endpoint == probe.binding['endpoints'][probe.endpoint_name].rstrip('/'),
+ 'caller endpoint differs from the current binding')
+ endpoint = urlsplit(probe.endpoint)
+ marker = 'auto-testing-i05/' + str(uuid.uuid4())
+ path = '/' + quote(bucket, safe='') + '/' + quote(key, safe='/~')
+ target = path + '?' + urlencode({'versionId': version_id})
+ from botocore.auth import S3SigV4Auth
+ from botocore.awsrequest import AWSRequest
+ from botocore.credentials import Credentials
+ access, secret = probe.credentials()
+ token = None if probe.credential_pair else os.environ.get('HEAL_SESSION_TOKEN')
+ outgoing = AWSRequest(method='GET', url=probe.endpoint + target, headers={
+ 'Host': endpoint.netloc, 'x-amz-content-sha256': hashlib.sha256(b'').hexdigest(),
+ 'Connection': 'close', 'Content-Length': '0', 'User-Agent': marker})
+ S3SigV4Auth(Credentials(access, secret, token), 's3', 'us-east-1').add_auth(outgoing)
+ output = Path(output)
+ output.mkdir(exist_ok=False)
+ parent = os.open(output.parent, os.O_RDONLY)
+ try:
+ os.fsync(parent)
+ finally:
+ os.close(parent)
+ intent_path = output / 'request.json'
+ save(intent_path, {'schemaVersion': 1, 'runId': probe.binding['runId'],
+ 'candidateCommit': probe.binding['candidateCommit'], 'harnessCommit': probe.binding['harnessCommit'],
+ 'candidatePackageSha256': probe.binding['candidatePackageSha256'],
+ 'candidateBinarySha256': probe.binding['candidateBinarySha256'],
+ 'bindingSha256': hashlib.sha256(bound.encode()).hexdigest(),
+ 'endpoint': probe.endpoint_name, 'endpointUrl': probe.endpoint,
+ 'operation': 'get_object', 'params': params,
+ 'callerMarker': marker, 'requestTarget': target,
+ 'receiveBufferBytesRequested': receive_buffer_bytes,
+ 'clockDomain': CLOCK_DOMAIN, 'preparedMonotonicNs': time.monotonic_ns(),
+ 'retryAllowed': False, 'canonicalCasePassClaimed': False})
+ evidence = {'schemaVersion': 1, 'runId': probe.binding['runId'],
+ 'requestSha256': hashlib.sha256(intent_path.read_bytes()).hexdigest(),
+ 'callerMarker': marker, 'requestTarget': target,
+ 'clockDomain': CLOCK_DOMAIN, 'requestInvocationStarted': False, 'requestWriteCompleted': False,
+ 'responseObserved': False, 'resultUnknown': True, 'acknowledged': False,
+ 'retryAllowed': False, 'serverOwnershipProven': False, 'canonicalCasePassClaimed': False}
+ connection_type = http.client.HTTPSConnection if endpoint.scheme == 'https' else http.client.HTTPConnection
+ class OnceConnection(connection_type):
+ attempts = 0
+ def connect(self):
+ self.attempts += 1
+ require(self.attempts == 1, 'caller-disconnect request cannot reconnect')
+ super().connect()
+ connection = OnceConnection(endpoint.hostname, endpoint.port, timeout=5)
+ sock = None
+ try:
+ validate_binding(probe.binding)
+ require(canonical(probe.binding) == bound, 'caller binding changed before connection')
+ connection.connect()
+ sock = connection.sock
+ require(sock is not None, 'caller socket unavailable')
+ receive_buffer_effective = None
+ if receive_buffer_bytes is not None:
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, receive_buffer_bytes)
+ receive_buffer_effective = sock.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF)
+ require(type(receive_buffer_effective) is int and receive_buffer_bytes <= receive_buffer_effective
+ <= receive_buffer_bytes * 4,
+ 'caller receive buffer did not retain its bounded setting')
+ evidence.update(socketDescriptor=sock.fileno(), localAddress=list(sock.getsockname()),
+ peerAddress=list(sock.getpeername()),
+ receiveBufferBytesRequested=receive_buffer_bytes,
+ receiveBufferBytesEffective=receive_buffer_effective)
+ validate_binding(probe.binding)
+ require(canonical(probe.binding) == bound, 'caller binding changed before request')
+ evidence.update(requestInvocationStarted=True, callStartedMonotonicNs=time.monotonic_ns())
+ connection.request('GET', target, headers=dict(outgoing.headers.items()))
+ evidence.update(requestWriteCompleted=True, requestWrittenMonotonicNs=time.monotonic_ns())
+ save(output / 'request-written.json', evidence)
+ if before_disconnect is not None:
+ # Observe the service without reading this request's response.
+ # An observer failure still reaches the socket-closing finally.
+ before_disconnect(json.loads(json.dumps(evidence)))
+ validate_binding(probe.binding)
+ require(canonical(probe.binding) == bound, 'caller binding changed during service observation')
+ evidence['disconnectStartedMonotonicNs'] = time.monotonic_ns()
+ sock.shutdown(socket.SHUT_RDWR)
+ evidence['shutdownCompleted'] = True
+ except BaseException as error:
+ evidence['errorType'] = type(error).__name__
+ raise
+ finally:
+ try:
+ connection.close()
+ finally:
+ evidence.update(connectionAttempts=connection.attempts, clientClosedMonotonicNs=time.monotonic_ns(),
+ clientSocketClosed=sock is None or sock.fileno() == -1)
+ save(output / 'disconnected.json', evidence)
+ return evidence
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--binding', type=Path, required=True)
+ parser.add_argument('--bucket', required=True)
+ parser.add_argument('--key', required=True)
+ parser.add_argument('--version-id', required=True)
+ parser.add_argument('--endpoint')
+ parser.add_argument('--output', type=Path, required=True)
+ args = parser.parse_args()
+ proof = disconnect_get(Probe(json.loads(args.binding.read_text()), args.endpoint),
+ args.bucket, args.key, args.version_id, args.output)
+ print(json.dumps(proof))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/scripts/validation_heal/caller_socket.py b/scripts/validation_heal/caller_socket.py
new file mode 100644
index 00000000..e477d20a
--- /dev/null
+++ b/scripts/validation_heal/caller_socket.py
@@ -0,0 +1,141 @@
+"""Read the exact caller socket from one run-owned Linux candidate process."""
+
+import hashlib
+import ipaddress
+import os
+from pathlib import Path
+import re
+import socket
+import struct
+import time
+
+
+def require(value, message):
+ if not value:
+ raise ValueError(message)
+
+
+def address(value):
+ require(isinstance(value, list) and len(value) == 2 and isinstance(value[0], str)
+ and type(value[1]) is int and 0 < value[1] < 65536, 'exact numeric TCP endpoint required')
+ ip = ipaddress.ip_address(value[0])
+ if isinstance(ip, ipaddress.IPv6Address) and ip.ipv4_mapped:
+ ip = ip.ipv4_mapped
+ return str(ip), value[1]
+
+
+def proc_address(value, ipv6):
+ parts = value.split(':')
+ require(len(parts) == 2 and re.fullmatch(r'[0-9A-Fa-f]{32}' if ipv6 else r'[0-9A-Fa-f]{8}', parts[0])
+ and re.fullmatch(r'[0-9A-Fa-f]{4}', parts[1]), 'invalid Linux TCP address')
+ packed = b''.join(struct.pack('=I', int(parts[0][offset:offset + 8], 16))
+ for offset in range(0, len(parts[0]), 8))
+ ip = ipaddress.ip_address(socket.inet_ntop(socket.AF_INET6 if ipv6 else socket.AF_INET, packed))
+ if isinstance(ip, ipaddress.IPv6Address) and ip.ipv4_mapped:
+ ip = ip.ipv4_mapped
+ return str(ip), int(parts[1], 16)
+
+
+def tcp_rows(raw, ipv6):
+ require(isinstance(raw, str) and len(raw.encode()) <= 4 * 1024**2, 'bounded Linux TCP table required')
+ lines = raw.splitlines()
+ require(lines and 'local_address' in lines[0] and 'inode' in lines[0], 'Linux TCP table header differs')
+ found = []
+ for line in lines[1:]:
+ fields = line.split()
+ require(len(fields) >= 10 and fields[9].isdigit() and re.fullmatch(r'[0-9A-Fa-f]{2}', fields[3]),
+ 'incomplete Linux TCP connection row')
+ local, remote = proc_address(fields[1], ipv6), proc_address(fields[2], ipv6)
+ queues = fields[4].split(':')
+ require(len(queues) == 2 and all(re.fullmatch(r'[0-9A-Fa-f]+', value) for value in queues),
+ 'invalid Linux TCP queue counters')
+ found.append({'inode': int(fields[9]), 'state': fields[3].upper(),
+ 'localAddress': list(local), 'remoteAddress': list(remote),
+ 'txQueueBytes': int(queues[0], 16), 'rxQueueBytes': int(queues[1], 16)})
+ return found
+
+
+def connections(raw, ipv6, client, server):
+ found = [{k: v for k, v in row.items() if k not in {'localAddress', 'remoteAddress'}}
+ for row in tcp_rows(raw, ipv6)
+ if tuple(row['localAddress']) == server and tuple(row['remoteAddress']) == client]
+ require(len(found) <= 1, 'ambiguous exact TCP connection in network namespace')
+ return found
+
+
+def descriptor_census(root):
+ """Read owned FD links and TCP metadata, never descriptor contents or paths.
+
+ This is a bounded snapshot. A descriptor may disappear or be reused while
+ it is read; neither a stable count nor these fingerprints proves no leak.
+ The caller binds process birth and network namespace around this census.
+ """
+ started = time.monotonic_ns()
+ fds = list((root / 'fd').iterdir())
+ require(len(fds) <= 65536, 'owned process descriptor census exceeds its budget')
+ descriptors, vanished, owned_sockets = [], 0, {}
+ for fd in sorted(fds, key=lambda entry: int(entry.name)):
+ try:
+ target = os.readlink(fd)
+ except FileNotFoundError:
+ vanished += 1
+ continue
+ match = re.fullmatch(r'(socket|pipe):\[([0-9]+)\]', target)
+ kind = match[1] if match else ('anonymous' if target.startswith('anon_inode:') else 'path')
+ row = {'descriptor': int(fd.name), 'kind': kind,
+ 'targetSha256': hashlib.sha256(os.fsencode(target)).hexdigest()}
+ if match:
+ row['inode'] = int(match[2])
+ if kind == 'socket':
+ owned_sockets.setdefault(row['inode'], []).append(row['descriptor'])
+ descriptors.append(row)
+ tcp = []
+ for table, ipv6 in (('tcp', False), ('tcp6', True)):
+ # The namespace table alone does not establish process ownership.
+ with (root / 'net' / table).open() as stream:
+ raw = stream.read(4 * 1024**2 + 1)
+ for row in tcp_rows(raw, ipv6):
+ if row['inode'] in owned_sockets:
+ tcp.append({**row, 'descriptors': owned_sockets[row['inode']], 'table': table})
+ return {'startedMonotonicNs': started, 'finishedMonotonicNs': time.monotonic_ns(),
+ 'fileDescriptors': len(fds), 'vanishedDescriptors': vanished,
+ 'descriptors': descriptors, 'ownedTcpConnections': tcp,
+ 'descriptorContentsRead': False, 'rawDescriptorPathsIncluded': False,
+ 'snapshotOnly': True, 'noLeakProven': False}
+
+
+def run(request, node):
+ require(set(request) == {'runId', 'action', 'expectedProcess', 'clientAddress', 'serverAddress'}
+ and request['action'] == 'caller-socket', 'caller socket accepts no arbitrary PID or path')
+ client, server = address(request['clientAddress']), address(request['serverAddress'])
+ require(server[1] == 9000, 'caller socket requires the bound service port')
+ _, _, owner = node['owned'](request['runId'])
+ require(owner.get('runtimeProfile') in {'lock-timeout', 'lock-successor'},
+ 'caller socket requires the isolated I05 runtime')
+ expected = request['expectedProcess']
+ require(isinstance(expected, dict) and all(name in expected for name in node['PROCESS_FIELDS']),
+ 'caller socket requires the original process birth')
+ def process():
+ observed = node['process'](request)
+ require(type(observed.get('pid')) is int and observed['pid'] > 1
+ and all(observed.get(name) == expected[name] for name in node['PROCESS_FIELDS']),
+ 'caller socket crossed a process birth or runtime change')
+ return observed
+ before = process()
+ root = Path(f'/proc/{before["pid"]}')
+ network_namespace = os.readlink(root / 'ns/net')
+ def sample():
+ census = descriptor_census(root)
+ matches = [{k: v for k, v in row.items() if k not in {'localAddress', 'remoteAddress'}}
+ for row in census['ownedTcpConnections']
+ if tuple(row['localAddress']) == server and tuple(row['remoteAddress']) == client]
+ require(len(matches) <= 1, 'caller TCP connection maps to ambiguous owned sockets')
+ process()
+ require(os.readlink(root / 'ns/net') == network_namespace, 'caller socket network namespace changed')
+ return {**census, 'ownedMatches': matches}
+ samples = [sample(), sample()]
+ process()
+ return {'process': {name: before[name] for name in node['PROCESS_FIELDS']}, 'clockDomain': before['bootId'],
+ 'clientAddress': list(client), 'serverAddress': list(server), 'samples': samples,
+ 'networkNamespace': network_namespace,
+ 'readOnly': True, 'continuousAbsenceProven': False, 'canonicalCasePassClaimed': False}
diff --git a/scripts/validation_heal/cancel_restart.py b/scripts/validation_heal/cancel_restart.py
new file mode 100644
index 00000000..2fce4d53
--- /dev/null
+++ b/scripts/validation_heal/cancel_restart.py
@@ -0,0 +1,127 @@
+#!/usr/bin/env python3
+"""F06: cancelled Admin journal survives the same-unit full fleet restart."""
+
+from __future__ import annotations
+
+import sys
+
+try:
+ from .heal_probe import canonical, require
+ from .healthy import physical_content
+ from .cursor import main as dx_main, wanted_pairs
+ from .versions import public_version_id
+ from .stop import observe_task as stop_task, status, idle_fleet
+except ImportError:
+ from heal_probe import canonical, require
+ from healthy import physical_content
+ from cursor import main as dx_main, wanted_pairs
+ from versions import public_version_id
+ from stop import observe_task as stop_task, status, idle_fleet
+
+
+CASE_ID = "heal.cancelled-task-restart"
+BRANCHES = ("cancel-restart", "old-token-after-restart", "force-new-after-cancel")
+REFERENCE = {"candidateCommit": "072b57b37559fbe626aeda8abf61617fdcbdd926",
+ "terminalSchema": 1, "reportSchema": 1, "intentPrefix": "root-heal-",
+ "terminalPrefix": "terminal-root-heal-", "reportPrefix": "heal-terminal-report-",
+ "sources": {"crates/heal/src/heal/manager/root_recovery.rs": "ba6beba9d9d14985e4c5448d85727036388d3d72f28879ac5c67fc5f760048ba",
+ "crates/heal/src/heal/manager/root_recovery/report.rs": "9beca8d450e426d66315e10ea2cebf4b375719c06d9019a7263e20f559070009"},
+ "role": "exact run-owned read-only journal witness; no synthesized checkpoint or record replay"}
+
+
+def validate_journal(observed, token, bucket, cancelled, expected):
+ wanted = wanted_pairs(expected)
+ present, all_records = {}, []
+ for node, value in observed.items():
+ require(value.get("readOnly") is True and value.get("taskId") == token
+ and isinstance(value.get("records"), list) and len(value["records"]) == 12,
+ "Admin journal did not cover each actual node's four drives and three exact names")
+ seen = set()
+ for entry in value["records"]:
+ identity = (node, entry.get("disk"), entry.get("kind"))
+ require(type(identity[1]) is int and 0 <= identity[1] < 4 and identity[2] in {"intent", "terminal", "report"}
+ and identity not in seen and type(entry.get("present")) is bool, "invalid or duplicate journal witness")
+ seen.add(identity)
+ if entry["present"]:
+ require(entry["kind"] not in present, "Admin task has duplicate physical journal owners")
+ present[entry["kind"]] = (node, entry)
+ all_records.append({"node": node, **entry})
+ require(set(present) == {"terminal", "report"}, "cancelled task retained runnable intent or lost its durable terminal/report")
+ terminal_node, terminal_entry = present["terminal"]
+ report_node, report_entry = present["report"]
+ require((terminal_node, terminal_entry["disk"]) == (report_node, report_entry["disk"]), "journal terminal/report owners disagree")
+ terminal, report = terminal_entry["value"], report_entry["value"]
+ require(type(terminal.get("schema")) is int and terminal["schema"] == 1
+ and terminal.get("task_id") == token and terminal.get("status") == "Cancelled"
+ and terminal.get("heal_type") == {"type": "prefix", "bucket": bucket, "prefix": "objects/"},
+ "durable terminal has the wrong task, scope, schema or cancelled state")
+ require(type(report.get("schema")) is int and report["schema"] == 1 and report.get("terminal") == terminal
+ and canonical(report.get("outcome")) == canonical(cancelled.get("outcome")),
+ "durable report differs from the settled cancelled responsibility")
+ require(type(report.get("next_seq")) is int and type(report.get("min_seq")) is int
+ and 1 <= report["min_seq"] <= report["next_seq"] and isinstance(report.get("seqed_items"), list)
+ and 0 < len(report["seqed_items"]) <= 1024, "durable cancelled report has invalid sequence bounds")
+ previous, seen_items = 0, set()
+ for item in report["seqed_items"]:
+ require(isinstance(item, list) and len(item) == 2 and type(item[0]) is int
+ and max(previous + 1, report["min_seq"]) <= item[0] < report["next_seq"]
+ and item[1].get("bucket") == bucket
+ and (item[1].get("object"), public_version_id(item[1].get("versionId"))) in wanted
+ and item[1].get("objectSize") == wanted[item[1]["object"], public_version_id(item[1].get("versionId"))]["payload"]["size"],
+ "durable report cursor/item crossed the old task responsibility")
+ pair = item[1]["object"], public_version_id(item[1].get("versionId"))
+ require(pair not in seen_items, "durable cancelled report duplicated an ACK identity")
+ seen_items.add(pair)
+ previous = item[0]
+ return {"ownerNode": terminal_node, "ownerDisk": terminal_entry["disk"], "cancelledTaskId": token,
+ "runnableIntentAbsentOnEveryDrive": True, "terminalAndReportExactlyMatch": True,
+ "allRecords": all_records}
+
+
+def observe_task(probe, fleet, binding, bucket, expected, targets, active, *, original):
+ fleet.record("journal-source-contract", REFERENCE)
+ archived = {}
+ def restart(path, old, cancelled, quiet):
+ fleet.stop()
+ before = fleet.record("cancelled-journal-before-restart", {node: fleet.remote(node, "task-records", taskId=old,
+ quiescent=True) for node in fleet.nodes})
+ journal = validate_journal(before, old, bucket, cancelled, expected)
+ archived.update(records=before, cancelled=cancelled, path=path, old=old)
+ stopped = fleet.snapshot("cancelled-before-restart", quiescent=True)
+ require(physical_content(stopped) == physical_content(quiet), "graceful stop changed cancelled object files/formats")
+ fleet.start()
+ idle = idle_fleet(fleet, binding)
+ old_after = status(probe, fleet, path, old, "cancelled-status-after-restart")
+ require(old_after.get("summary") == "stopped" and canonical(old_after.get("outcome")) == canonical(cancelled["outcome"]),
+ "cancelled task revived, lost its responsibility or falsely completed after restart")
+ restarted = fleet.snapshot("cancelled-live-after-restart")
+ require(physical_content(restarted) == physical_content(quiet) and all(
+ restarted[node]["pid"] > 0 and restarted[node]["pid"] != quiet[node]["pid"]
+ and restarted[node]["unit"] == quiet[node]["unit"] and restarted[node]["binarySha256"] == quiet[node]["binarySha256"]
+ for node in fleet.nodes), "restart replaced topology/candidate/roots or failed to start new processes")
+ after = fleet.record("cancelled-journal-after-restart", {node: fleet.remote(node, "task-records", taskId=old)
+ for node in fleet.nodes})
+ validate_journal(after, old, bucket, old_after, expected)
+ require(canonical(before) == canonical(after), "restart rewrote the original cancelled task's journal")
+ return {"journal": journal, "sameOriginalUnitsRootsAndCandidate": True, "newProcessOnEveryNode": True,
+ "oldCancelledOutcomePreserved": True, "oldJournalBytesPreserved": True, "allPeerIdle": idle}
+ proof = stop_task(probe, fleet, binding, bucket, expected, targets, active, original=original,
+ after_cancelled=restart, force_new=True)
+ final_old = status(probe, fleet, archived["path"], archived["old"], "old-status-after-new-completion")
+ final_journal = fleet.record("old-journal-after-new-completion", {node: fleet.remote(node, "task-records", taskId=archived["old"])
+ for node in fleet.nodes})
+ validate_journal(final_journal, archived["old"], bucket, final_old, expected)
+ require(canonical(final_journal) == canonical(archived["records"]),
+ "new forceStart task rewrote or removed the original cancelled task's journal")
+ return {"cancel-restart": {"verdict": "PASS", "cancellation": proof["stop-active"]},
+ "old-token-after-restart": {"verdict": "PASS", "oldStatus": proof["old-status"],
+ "retiredStop": proof["old-stop-new-task"]["staleStop"], "oldJournalPreservedThroughNewCompletion": True},
+ "force-new-after-cancel": {"verdict": "PASS", "forceStart": True, **proof["old-stop-new-task"]}}
+
+
+if __name__ == "__main__":
+ try:
+ dx_main(case_id=CASE_ID, observer=observe_task, branches=BRANCHES)
+ except Exception as exc:
+ print(f"Heal cancelled-restart validation failed: {exc}", file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_heal/capacity.py b/scripts/validation_heal/capacity.py
new file mode 100644
index 00000000..78cc50c3
--- /dev/null
+++ b/scripts/validation_heal/capacity.py
@@ -0,0 +1,380 @@
+#!/usr/bin/env python3
+"""I02: actual bounded ENOSPC/inode exhaustion and owned hard-quota recovery."""
+
+import argparse
+from datetime import datetime, timedelta, timezone
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import secrets
+import signal
+import sys
+
+try:
+ from .heal_probe import Probe, compare_inventories, payload, require, save
+ from .heal_workload import Recorder, seed, verify_seed
+ from .healthy import Fleet, physical_content, validate_topology
+ from .inspector import verify as verify_inspector
+ from .matrix import select
+ from .shards import (map_target, run_task, validate_fault, validate_repaired_physical,
+ verify_cohort, pending_inventory, verify_ranges)
+ from .versions import validate_version_receipts, verify_quorum_contract
+ from .drive_gate import validate_unwritable
+ from .stop import idle_fleet
+ from .write_quorum import resolve_put
+ from .bounded_volume import IMAGE_BYTES, MAX_INODES, MIN_SHARED_FREE
+except ImportError:
+ from heal_probe import Probe, compare_inventories, payload, require, save
+ from heal_workload import Recorder, seed, verify_seed
+ from healthy import Fleet, physical_content, validate_topology
+ from inspector import verify as verify_inspector
+ from matrix import select
+ from shards import (map_target, run_task, validate_fault, validate_repaired_physical,
+ verify_cohort, pending_inventory, verify_ranges)
+ from versions import validate_version_receipts, verify_quorum_contract
+ from drive_gate import validate_unwritable
+ from stop import idle_fleet
+ from write_quorum import resolve_put
+ from bounded_volume import IMAGE_BYTES, MAX_INODES, MIN_SHARED_FREE
+
+
+CASE_ID = "heal.enospc-inodes-and-quota"
+BRANCHES = ("bounded-volume-enospc", "bounded-inode-exhaustion", "bucket-quota")
+SIZE = 5 * 1024**2 + 97
+CONTRACT = {"candidateCommit": "072b57b37559fbe626aeda8abf61617fdcbdd926",
+ "sourceFiles": {"rustfs/src/admin/handlers/quota.rs":
+ "6a2b062d3d1c4cd81d934c570d21cb44dcd2c1883f6a85d2265e01b466924f5c",
+ "rustfs/src/error.rs": "656d38972c06339015c174405571a94f19d8dd64a680e1a7ca631f80205f99a7"},
+ "quota": {"blockedNewLogicalPUT": {"httpStatus": 400, "code": "InvalidRequest",
+ "message": "Bucket quota exceeded."}, "limitBytes": 1024**2, "releasedLimitBytes": 256 * 1024**2,
+ "existingObjectShardRepair": "does not reserve a new S3 object or increase logical bucket usage"},
+ "volume": {"imageBytesPerOriginalDisk": IMAGE_BYTES, "maximumInodesPerImage": MAX_INODES,
+ "sharedDriveFreeMarginBytes": MIN_SHARED_FREE, "loopVolumesIncreasePhysicalTopologyCount": False},
+ "load": {"DBObjects": 8, "completedMPUObjects": 1, "pendingMPUs": 2,
+ "uniqueNegativePUTs": 1, "uniqueReleasedCapacityPUTs": 1, "uncertainRequestRetries": 0, "fillDeadlineSecondsPerDisk": 60}}
+
+
+def refusal(observed, *, quota):
+ require(observed.get("acknowledged") is False and observed.get("clientCallInvoked") is True,
+ "capacity-negative PUT succeeded or was never invoked")
+ if observed.get("resultUnknown") is True:
+ return {"state": "UNKNOWN_PENDING_RESOLUTION", "retryAllowed": False}
+ response = observed.get("response") or {}
+ error = response.get("Error") or {}
+ if quota:
+ require(response.get("ResponseMetadata", {}).get("HTTPStatusCode") == 400
+ and error.get("Code") == "InvalidRequest" and error.get("Message", "").startswith("Bucket quota exceeded."),
+ "quota-negative PUT did not return the reviewed actual hard-quota refusal")
+ else:
+ require(response.get("ResponseMetadata", {}).get("HTTPStatusCode") in {500, 503}
+ and error.get("Code") in {"ServiceUnavailable", "XMinioWriteQuorum", "InternalError"},
+ "capacity-negative PUT lacks a reviewed structured storage refusal")
+ require(observed.get("resultUnknown") is False, "capacity PUT is neither refused nor explicitly unresolved")
+ return {"state": "REFUSED", "code": error["Code"], "retryAllowed": False}
+
+
+def quota_set_ack(probe, fleet, bucket, limit):
+ path = "/rustfs/admin/v3/quota/" + bucket
+ body = json.dumps({"quota": limit, "quota_type": "HARD"}).encode()
+ configured = fleet.record("owned-bucket-quota-PUT", probe.admin(path, [], body, method="PUT"))
+ require(configured.get("httpStatus") == 200 and configured.get("response", {}).get("bucket") == bucket
+ and configured["response"].get("quota") == limit and configured["response"].get("quota_type") == "HARD",
+ "hard quota was not acknowledged on the exact owned bucket")
+ return path
+
+
+def quota_config(probe, fleet, bucket, limit):
+ path = quota_set_ack(probe, fleet, bucket, limit)
+ for endpoint in probe.binding["endpoints"]:
+ observed = fleet.record("all-peer-owned-quota-GET", Probe(probe.binding, endpoint).admin(path, [], b"", method="GET"))
+ require(observed.get("httpStatus") == 200 and observed.get("response", {}).get("bucket") == bucket
+ and observed["response"].get("quota") == limit and observed["response"].get("quota_type") == "HARD",
+ "peer did not observe the exact acknowledged quota limit")
+ return {"bucket": bucket, "limitBytes": limit, "allPeersVerified": True, "sharedFilesystemQuotaChanged": False}
+
+
+def release_quota(probe, fleet, bucket, limit):
+ # The failure scene is already archived before rescue. Config-only GET
+ # proves the restored limit without claiming an authoritative usage value.
+ if any(fleet.remote(node, "process")["pid"] == 0 for node in fleet.nodes):
+ fleet.start()
+ quota_set_ack(probe, fleet, bucket, limit)
+ for endpoint in probe.binding["endpoints"]:
+ observed = fleet.record("recovery-config-only-quota", Probe(probe.binding, endpoint).admin(
+ "/rustfs/admin/v3/get-bucket-quota", [("bucket", bucket)], b"", method="GET"))
+ value = observed.get("response") or {}
+ require(observed.get("httpStatus") == 200 and value.get("quota") == value.get("size") == limit
+ and value.get("quotatype") == "hard", "quota recovery did not restore the exact owned config")
+ return {"ownedQuotaConfigRestored": True, "authoritativeUsageClaimed": False, "productRepairClaimed": False}
+
+
+def verify_s3(fleet, binding, bucket, expected, baseline, pending):
+ result = {}
+ for endpoint in binding["endpoints"]:
+ probe = Probe(binding, endpoint)
+ inventory = fleet.record("all-peer-final-complete-S3", {"endpoint": endpoint,
+ **probe.inventory(bucket, unversioned_current_tags=True)})
+ proof = {**verify_seed(expected, inventory), **compare_inventories(baseline, inventory)}
+ require(fleet.record("all-peer-pending-MPU", pending_inventory(probe, bucket)) == pending,
+ "capacity failure changed an acknowledged pending MPU part or UploadId")
+ proof["ranges"] = [verify_ranges(probe, fleet, bucket, entry["key"], expected_body=payload(
+ f"{bucket}/{entry['key']}/0", entry["payload"]["size"])) for entry in expected]
+ result[endpoint] = proof
+ return result
+
+
+def run_branch(args, topology, nodes, branch):
+ directory = args.evidence / topology / branch
+ directory.mkdir(parents=True)
+ run_id = f"gh-{os.environ['GITHUB_RUN_ID']}-{os.environ.get('GITHUB_RUN_ATTEMPT', '1')}-{topology.lower()}"
+ bounded = branch != "bucket-quota"
+ fleet = Fleet(nodes, directory, run_id, args.tool_directory,
+ runtime_profile="bounded-capacity" if bounded else "manual")
+ candidate = json.loads(args.candidate.read_text())
+ require(candidate["source_sha"] == CONTRACT["candidateCommit"], "unreviewed I02 error/quota contract")
+ manifest = fleet.record("candidate-bound-inspector", verify_inspector(args.tool_directory, candidate))
+ verify_quorum_contract(manifest, candidate)
+ fleet.record("registered-capacity-contract", CONTRACT)
+ bucket = "rv-heal-" + hashlib.sha256(run_id.encode()).hexdigest()[:16]
+ access, secret = secrets.token_hex(16), secrets.token_hex(24)
+ os.environ.update(HEAL_ACCESS_KEY=access, HEAL_SECRET_KEY=secret)
+ binding = {"schemaVersion": 1, "runId": run_id, "jumpHostOnly": True, "exclusiveHealService": True,
+ "leaseExpiresAt": (datetime.now(timezone.utc) + timedelta(minutes=30)).isoformat(), "bucketPrefix": bucket,
+ "endpoints": {f"node{index}": f"http://rustfs-node{index}:9000" for index in range(1, len(nodes) + 1)},
+ "defaultEndpoint": "node1", "candidateCommit": candidate["source_sha"], "harnessCommit": args.testing_commit,
+ "candidatePackageSha256": candidate["package_sha256"], "candidateBinarySha256": args.binary_sha,
+ "rcBinarySha256": hashlib.sha256(args.rc_bin.read_bytes()).hexdigest(),
+ "maxInventoryObjects": 128, "maxInventoryBytes": 512 * 1024**2}
+ fleet.record("binding", binding)
+ probe, active, allocated = Probe(binding), [], []
+ result = {"topology": topology, "branch": branch, "verdict": "RUNNING", "cleanup": "PENDING"}
+ attempted, negative_body, negative_key = None, None, "capacity-negative/unique.bin"
+ released_attempted, released_body, released_key = None, None, "capacity-released/different-nonce.bin"
+ quota_set = False
+ fill_initiated = False
+
+ def allocate():
+ for node in nodes:
+ allocated.append(node)
+ fleet.record("fixed-owned-volume-allocation", {"node": node, **fleet.remote(
+ node, "bounded-volumes-create", branch=branch)})
+
+ def release():
+ nonlocal quota_set
+ if bounded:
+ for node in allocated:
+ identity = fleet.remote(node, "process")
+ if identity["pid"]:
+ fleet.record("release-only-owned-filler", {"node": node, **fleet.remote(
+ node, "bounded-volumes-release", expected=identity)})
+ else:
+ fleet.record("release-only-owned-filler", {"node": node, **fleet.remote(
+ node, "bounded-volumes-release", quiescent=True)})
+ elif quota_set:
+ fleet.record("quota-recovery-config-proof", release_quota(probe, fleet, bucket, CONTRACT["quota"]["releasedLimitBytes"]))
+ quota_set = False
+
+ try:
+ fleet.deploy(args.binary_sha, bucket, access, secret, before_start=allocate if bounded else None)
+ recorder = Recorder(probe, directory / "fixture")
+ expected = seed(recorder, bucket, "DB", 8, [SIZE])
+ expected += seed(recorder, bucket, "DM", 1, [SIZE], existing_bucket=True)
+ pending = fleet.record("independent-pending-MPU-ACKs", pending_inventory(probe, bucket))
+ baseline = fleet.record("complete-S3-baseline", probe.inventory(bucket, unversioned_current_tags=True))
+ fleet.record("all-fixture-ACKs", {"expected": expected, **verify_seed(expected, baseline)})
+ fleet.stop()
+ original = fleet.snapshot("original-complete-physical", quiescent=True)
+ fleet.record("actual-independent-topology", validate_topology(original, topology))
+ targets = [map_target(original, bucket, expected[index]["key"], "data", 1) for index in (0, 8)]
+ for target in targets:
+ fleet.record("original-target-streaming-proof", verify_cohort(fleet, target, "original-target"))
+ for index, target in enumerate(targets):
+ params = {"disk": target["disk"], "faultId": f"I02-target-{index}"}
+ fleet.faults.append((target["node"], params))
+ receipt = fleet.record("exact-missing-part", fleet.remote(target["node"], "apply-fault", **params,
+ relativePath=target["relativePath"], kind="missing", expectedSha256=target["sha256"]))
+ scene = fleet.snapshot("exact-missing-part-witness", quiescent=True)
+ prior = original if index == 0 else faulted
+ fleet.record("missing-part-proof", validate_fault(prior, scene, target, receipt))
+ faulted = scene
+ fleet.start()
+ require(physical_content(faulted) == physical_content(fleet.snapshot("manual-source-isolation-before-fault")),
+ "capacity target changed before the registered capacity fault")
+ if bounded:
+ fill_initiated = True
+ for node in nodes:
+ identity = fleet.remote(node, "process")
+ fleet.record("actual-resource-exhaustion", {"node": node, **fleet.remote(
+ node, "bounded-volumes-fill", expected=identity)})
+ else:
+ quota_set = True
+ result["quota"] = quota_config(probe, fleet, bucket, CONTRACT["quota"]["limitBytes"])
+ negative_body = payload(f"{bucket}/{negative_key}/0", SIZE)
+ # This unique call has durable request and ACK files even when the
+ # expected refusal causes Recorder to reject it. No retry is issued.
+ negative = Recorder(probe, directory / "negative-PUT")
+ attempted = {"acknowledged": False, "resultUnknown": True, "clientCallInvoked": True,
+ "reason": "durable request must be resolved if its ACK cannot be read"}
+ try:
+ negative.call("put_object", {"Bucket": bucket, "Key": negative_key}, negative_body)
+ except ValueError:
+ pass
+ ack = json.loads((negative.output / "000001-ack.json").read_text())
+ attempted = ack["observation"]
+ result["negativePUT"] = refusal(attempted, quota=not bounded)
+ if not bounded:
+ require(result["negativePUT"]["state"] == "REFUSED", "quota dependency failed without its exact quota refusal")
+ result["negativeTasks"] = []
+ for entry in (expected[0], expected[8]):
+ current = run_task(probe, fleet, {"bucket": bucket, "key": entry["key"]}, 2, active)
+ proof = validate_unwritable(current, bucket, entry) if bounded else validate_version_receipts(
+ current, bucket, [entry], 2, required_repairs={(entry["key"], "null")})
+ result["negativeTasks"].append(proof)
+ if bounded:
+ for node in nodes:
+ identity = fleet.remote(node, "process")
+ fleet.record("resource-still-exhausted-after-Heal", {"node": node, **fleet.remote(
+ node, "bounded-volumes-witness", expected=identity)})
+ fleet.stop()
+ negative_scene = fleet.snapshot("negative-complete-physical-before-release", quiescent=True)
+ if bounded:
+ require(physical_content(faulted) == physical_content(negative_scene),
+ "unwritable Heal or PUT changed a healthy source, metadata or the missing target")
+ else:
+ result["quotaPhysical"] = validate_repaired_physical(original, negative_scene, targets[0], repair_targets=targets)
+ for target in targets:
+ fleet.record("quota-existing-object-streaming-proof", verify_cohort(fleet, target, "quota-repaired"))
+ for node in nodes:
+ fleet.record("negative-owned-service-diagnostics", {"node": node, **fleet.remote(node, "logs")})
+ if bounded:
+ release()
+ fleet.start()
+ else:
+ fleet.start()
+ release()
+ require(physical_content(negative_scene) == physical_content(fleet.snapshot("capacity-return-before-recovery-Heal")),
+ "capacity return altered files before the independent recovery task")
+ result["recoveryTasks"] = []
+ for entry in (expected[0], expected[8]):
+ current = run_task(probe, fleet, {"bucket": bucket, "key": entry["key"]}, 2, active)
+ result["recoveryTasks"].append(validate_version_receipts(current, bucket, [entry], 2,
+ required_repairs={(entry["key"], "null")} if bounded else ()))
+ idle_fleet(fleet, binding)
+ fleet.stop()
+ repaired = fleet.snapshot("complete-product-repaired-before-S3", quiescent=True)
+ result["physical"] = validate_repaired_physical(original, repaired, targets[0], repair_targets=targets)
+ for target in targets:
+ fleet.record("product-reconstructed-streaming-proof", verify_cohort(fleet, target, "all-repaired-targets"))
+ fleet.faults.clear()
+ fleet.start()
+ reads = [fleet.record("single-negative-PUT-final-resolution", Probe(binding, endpoint).s3("get_object", {
+ "Bucket": bucket, "Key": negative_key})) for endpoint in binding["endpoints"]]
+ result["putResolution"] = resolve_put(attempted, reads, {"size": len(negative_body),
+ "sha256": hashlib.sha256(negative_body).hexdigest()})
+ require(result["putResolution"]["state"] == "ABSENT", "capacity-refused unique PUT committed unexpectedly")
+ result["s3"] = verify_s3(fleet, binding, bucket, expected, baseline, pending)
+ fleet.stop()
+ require(physical_content(repaired) == physical_content(fleet.snapshot("physical-after-full-S3", quiescent=True)),
+ "S3 verification changed the frozen product-repaired scene")
+ fleet.start()
+ released_body = payload(f"{bucket}/{released_key}/0", SIZE)
+ released_payload = {"size": len(released_body), "sha256": hashlib.sha256(released_body).hexdigest()}
+ positive = Recorder(probe, directory / "capacity-released-PUT")
+ released_attempted = {"acknowledged": False, "resultUnknown": True, "clientCallInvoked": True}
+ try:
+ positive.call("put_object", {"Bucket": bucket, "Key": released_key}, released_body)
+ finally:
+ ack_path = positive.output / "000001-ack.json"
+ if ack_path.exists():
+ released_attempted = json.loads(ack_path.read_text())["observation"]
+ released_reads = []
+ for endpoint in binding["endpoints"]:
+ observed = fleet.record("released-capacity-success-ACK-GET", Probe(binding, endpoint).s3(
+ "get_object", {"Bucket": bucket, "Key": released_key}))
+ require(observed.get("acknowledged") is True and observed.get("resultUnknown") is False
+ and observed.get("payload") == released_payload, "released capacity PUT ACK cannot be redeemed")
+ released_reads.append(observed)
+ result["releasedPUTResolution"] = resolve_put(released_attempted, released_reads, released_payload)
+ result["releasedCapacityPUT"] = {"differentUniqueKey": released_key, "allPeersRedeemed": True,
+ "originalNegativePUTRetried": False}
+ result["verdict"] = "PASS"
+ except (Exception, KeyboardInterrupt) as exc:
+ result.update(verdict="FAIL", failure={"type": type(exc).__name__, "message": str(exc)})
+ try:
+ fleet.stop()
+ fleet.snapshot("physical-original-failure-before-any-rescue", quiescent=True)
+ except Exception as evidence_error:
+ result["failureEvidenceError"] = str(evidence_error)
+ finally:
+ for path, token in active:
+ try: fleet.record("stop-unresolved", probe.admin(path, [("clientToken", token), ("forceStop", "true")], b""))
+ except Exception as exc: fleet.record("stop-unresolved-error", {"error": str(exc)})
+ recovery_failed = False
+ try:
+ if fill_initiated or quota_set:
+ release()
+ if attempted and "putResolution" not in result:
+ if fleet.remote(nodes[0], "process")["pid"] == 0:
+ fleet.start()
+ reads = [fleet.record("failed-case-PUT-resolution", Probe(binding, endpoint).s3("get_object", {
+ "Bucket": bucket, "Key": negative_key})) for endpoint in binding["endpoints"]]
+ result["putResolution"] = resolve_put(attempted, reads, {"size": len(negative_body),
+ "sha256": hashlib.sha256(negative_body).hexdigest()})
+ if released_attempted and "releasedPUTResolution" not in result:
+ if fleet.remote(nodes[0], "process")["pid"] == 0:
+ fleet.start()
+ reads = [fleet.record("failed-case-released-PUT-resolution", Probe(binding, endpoint).s3("get_object", {
+ "Bucket": bucket, "Key": released_key})) for endpoint in binding["endpoints"]]
+ result["releasedPUTResolution"] = resolve_put(released_attempted, reads, {"size": len(released_body),
+ "sha256": hashlib.sha256(released_body).hexdigest()})
+ except Exception as exc:
+ recovery_failed = True
+ result["recoveryError"] = str(exc)
+ if recovery_failed:
+ result["cleanup"] = "FAIL"
+ fleet.record("retained-unresolved-capacity-environment", {"state": "DIRTY", "namespacesRetained": True,
+ "productRepairClaimed": False, "failureVerdictPreserved": True})
+ else:
+ result["cleanup"] = "PASS" if fleet.cleanup() else "FAIL"
+ fleet.record("branch-result", result)
+ return result
+
+
+def main():
+ def interrupted(signum, frame):
+ raise KeyboardInterrupt(f"I02 workflow interrupted by signal {signum}")
+ signal.signal(signal.SIGTERM, interrupted)
+ parser = argparse.ArgumentParser(description=__doc__)
+ for name in ("candidate", "rc-bin", "evidence", "tool-directory"):
+ parser.add_argument("--" + name, type=Path, required=True)
+ parser.add_argument("--case-id", choices=[CASE_ID], required=True)
+ parser.add_argument("--testing-commit", required=True)
+ parser.add_argument("--binary-sha", required=True)
+ args = parser.parse_args()
+ require(re.fullmatch(r"[0-9a-f]{64}", args.binary_sha) and re.fullmatch(r"[1-9][0-9]*", os.environ.get("GITHUB_RUN_ID", "")),
+ "immutable workflow candidate identity required")
+ require(select(CASE_ID)[0]["variants"] == list(BRANCHES), "I02 source branch omitted")
+ nodes = os.environ.get("RUSTFS_NODES", "vm000 vm001 vm002 vm003").split()
+ require(len(nodes) >= 4 and len(nodes) == len(set(nodes)) and all(re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", n) for n in nodes),
+ "four current Heal nodes required")
+ results = []
+ try:
+ for topology, selected in (("H1", nodes[:1]), ("H2", nodes[:4])):
+ for branch in BRANCHES:
+ results.append(run_branch(args, topology, selected, branch))
+ if results[-1]["cleanup"] != "PASS": break
+ if results[-1]["cleanup"] != "PASS": break
+ finally:
+ complete = len(results) == 6 and all(row["verdict"] == "PASS" and row["cleanup"] == "PASS" for row in results)
+ save(args.evidence / "healthy-summary.json", {"caseId": CASE_ID, "results": results, "complete": complete,
+ "cleanup": "PASS" if results and all(row["cleanup"] == "PASS" for row in results) else "FAIL"})
+ require(complete, "I02 requires all real H1/H2 capacity faults, complete ACK/physical proofs and clean finalization")
+
+
+if __name__ == "__main__":
+ try: main()
+ except Exception as exc:
+ print(f"I02 capacity validation failed: {exc}", file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_heal/check_bitrot_tool.py b/scripts/validation_heal/check_bitrot_tool.py
new file mode 100644
index 00000000..b68ab41a
--- /dev/null
+++ b/scripts/validation_heal/check_bitrot_tool.py
@@ -0,0 +1,88 @@
+#!/usr/bin/env python3
+"""Check the read-only bitrot tool against pinned external release captures."""
+
+import argparse
+import hashlib
+import json
+from pathlib import Path
+import subprocess
+import tempfile
+
+
+def check(binary: Path, source: Path):
+ verified, rejected = 0, 0
+ for kind in ("minio", "legacy"):
+ root = source / "crates/ecstore/tests/fixtures/erasure-shards" / kind
+ manifest = json.loads((root / "manifest.json").read_text())
+ for relative, digest in manifest["files"].items():
+ if hashlib.sha256((root / relative).read_bytes()).hexdigest() != digest:
+ raise ValueError("external corpus differs from its pinned file manifest")
+ for relative in sorted(manifest["files"]):
+ part = root / relative
+ if part.name != "part.1":
+ continue
+ metadata = part.parent.parent / "xl.meta"
+ decoded = json.loads(subprocess.check_output([str(binary), str(metadata), manifest["bucket"], manifest["object"]], text=True))
+ version = decoded["versions"][0].get("versionId") or "null"
+ args = [str(binary), "--verify-part", str(metadata), manifest["bucket"], manifest["object"]]
+ result = json.loads(subprocess.check_output([*args, str(part), version, "1"], text=True))
+ if result.get("physicalBitrotVerified") is not True or result["physicalSize"] != part.stat().st_size:
+ raise ValueError("external healthy shard did not pass the bitrot oracle")
+ verified += 1
+ if verified not in {1, 13}:
+ continue
+ original = part.read_bytes()
+ changes = {"first": bytes([original[0] ^ 1]) + original[1:],
+ "middle": original[:len(original) // 2] + bytes([original[len(original) // 2] ^ 1]) + original[len(original) // 2 + 1:],
+ "last": original[:-1] + bytes([original[-1] ^ 1]),
+ "truncate": original[:-1], "zero": bytes(len(original)), "trailing": original + b"x"}
+ with tempfile.TemporaryDirectory(prefix="heal-bitrot-oracle-") as directory:
+ copied = Path(directory) / part.parent.name / part.name
+ copied.parent.mkdir()
+ for name, value in changes.items():
+ copied.write_bytes(value)
+ bad = subprocess.run([*args, str(copied), version, "1"], capture_output=True, text=True)
+ if bad.returncode == 0 or not any(message in bad.stderr for message in
+ ("hash mismatch", "short shard read", "trailing shard bytes")):
+ raise ValueError(f"{kind} {name} corruption was not rejected by the integrity oracle")
+ rejected += 1
+ if verified != 24 or rejected != 12:
+ raise ValueError("external bitrot corpus coverage is incomplete")
+ captured = bytes.fromhex((source / "crates/filemeta/tests/fixtures/issue_2288_legacy_xlmeta.hex").read_text())
+ if hashlib.sha256(captured).hexdigest() != "5997675004972399e2d279a83b725c360a12f117edbf9dffc8c02d76c8753452":
+ raise ValueError("captured inline fixture differs from its frozen released-process bytes")
+ with tempfile.TemporaryDirectory(prefix="heal-inline-oracle-") as directory:
+ metadata = Path(directory) / "xl.meta"
+ metadata.write_bytes(captured)
+ decoded = json.loads(subprocess.check_output([str(binary), str(metadata), "fixture", "object"], text=True))
+ inline_versions = [version for version in decoded["versions"] if version.get("inline")]
+ if len(inline_versions) != 1 or inline_versions[0]["inline"]["rawOccurrenceCount"] != 1:
+ raise ValueError("captured inline identity is ambiguous")
+ version = inline_versions[0]
+ inline = version["inline"]
+ value = captured[inline["rawOffset"]:inline["rawOffset"] + inline["length"]]
+ if hashlib.sha256(value).hexdigest() != inline["sha256"]:
+ raise ValueError("decoded inline value is not its exact physical slice")
+ args = [str(binary), "--verify-inline", str(metadata), "fixture", "object", version["versionId"] or "null"]
+ good = json.loads(subprocess.check_output(args, text=True))
+ if good.get("physicalBitrotVerified") is not True or good["inlineSha256"] != inline["sha256"]:
+ raise ValueError("captured inline framing did not verify")
+ inline_rejected = 0
+ for position in (0, inline["length"] // 2, inline["length"] - 1):
+ offset = inline["rawOffset"] + position
+ metadata.write_bytes(captured[:offset] + bytes([captured[offset] ^ 1]) + captured[offset + 1:])
+ bad = subprocess.run(args, capture_output=True, text=True)
+ if bad.returncode == 0 or "hash mismatch" not in bad.stderr:
+ raise ValueError("inline corruption was not rejected by the independent framing checker")
+ inline_rejected += 1
+ return {"externalHealthyShardsVerified": verified, "corruptedShardsRejected": rejected,
+ "releasedInlineVerified": 1, "corruptedInlineValuesRejected": inline_rejected,
+ "corpora": ["MinIO released GF8", "RustFS released legacy GF16"], "liveCasePassClaimed": False}
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--binary", type=Path, required=True)
+ parser.add_argument("--source", type=Path, required=True)
+ args = parser.parse_args()
+ print(json.dumps(check(args.binary.resolve(strict=True), args.source.resolve(strict=True))))
diff --git a/scripts/validation_heal/checkpoint.py b/scripts/validation_heal/checkpoint.py
new file mode 100644
index 00000000..0f00e795
--- /dev/null
+++ b/scripts/validation_heal/checkpoint.py
@@ -0,0 +1,285 @@
+"""Locate original Admin erasure-set responsibility before F07/C11 faults."""
+
+import base64
+import json
+import time
+
+try:
+ from . import journal_fault as journal
+ from .healthy import BASE_OPTIONS
+ from .heal_probe import canonical, require
+ from .fleet_node import PROCESS_FIELDS
+except ImportError:
+ import journal_fault as journal
+ from healthy import BASE_OPTIONS
+ from heal_probe import canonical, require
+ from fleet_node import PROCESS_FIELDS
+
+
+PATH = "/rustfs/admin/v3/heal/"
+OPTIONS = {**BASE_OPTIONS, "recursive": True}
+REFERENCE = {"candidateCommit": "072b57b37559fbe626aeda8abf61617fdcbdd926", "sourceFiles": {
+ "rustfs/src/admin/handlers/heal.rs": "7e4007c29a8329ced8e48b88111400e79cffd845bd6ddc6243b79f3cd48e8199",
+ "crates/heal/src/heal/channel.rs": "7f669ca43b9559db9b97fba6b419a6c76a94d76acd8526db132d74b7c534d6b0",
+ "crates/heal/src/heal/resume.rs": "ed55baa03fafa79bf662393eafcf53b4f7752ca8b2f77824cc6deff085a0cf1d",
+ "crates/heal/src/heal/resume/checkpoint.rs": "fe067b39ee406e16517bd665f7f49457daffa05d5d7bc298bd965e927b1381c1",
+ "crates/heal/src/heal/manager/root_recovery.rs": "ba6beba9d9d14985e4c5448d85727036388d3d72f28879ac5c67fc5f760048ba",
+ "crates/heal/src/heal/erasure_healer/admin.rs": "776c03e4dfbd27f51da506d8f637de3a3973d86d22dc83a7cda24f2e0d642ee5"},
+ "route": "empty bucket/object plus explicit pool=0,set=0 selects Admin ErasureSet",
+ "checkpointSchema": 8, "resumeSchema": 7, "durableOwnerRequired": True,
+ "bucketOrClusterScanIsCheckpointProof": False, "canonicalPassClaimed": False}
+
+
+def bytes_of(row):
+ require(row.get("present") is True and isinstance(row.get("rawBase64"), str), "record has no original bytes")
+ raw = base64.b64decode(row["rawBase64"], validate=True)
+ require(journal.entry(raw) == {name: row.get(name) for name in ("size", "sha256")},
+ "record bytes differ from their exact archived size/digest")
+ return raw
+
+
+def owned_checkpoint(observed, token, bucket, *, require_progress=True):
+ journal.task_id(token)
+ names = journal.names("checkpoint", token)
+ require(isinstance(observed, dict) and len(observed) in {1, 4}, "checkpoint census omitted bound nodes")
+ present = {}
+ for node, census in observed.items():
+ rows = census.get("records")
+ require(census.get("readOnly") is True and census.get("family") == "checkpoint"
+ and census.get("taskId") == token and census.get("directoryEnumerationUsed") is False
+ and (census.get("quiescent") is True or census.get("stable") is True
+ and census.get("processBound") is True and census.get("independentReads") == 2)
+ and isinstance(rows, list) and len(rows) == 16, "checkpoint has no stopped or stable process-bound census")
+ seen = set()
+ for row in rows:
+ pair = row.get("disk"), row.get("relativePath")
+ require(type(pair[0]) is int and 0 <= pair[0] < 4 and pair[1] in names and pair not in seen
+ and type(row.get("present")) is bool, "checkpoint census duplicated or escaped its exact task/disk paths")
+ seen.add(pair)
+ if row["present"]:
+ require(pair[1] not in present, "Admin task has multiple physical checkpoint owners")
+ bytes_of(row)
+ present[pair[1]] = {"node": node, **row}
+ require(set(present) == set(names[:2]),
+ "NOT_OBSERVED: original runnable Admin checkpoint/resume pair is absent, blocked or has an unexpected digest record")
+ checkpoint, resume = present[names[0]], present[names[1]]
+ require((checkpoint["node"], checkpoint["disk"]) == (resume["node"], resume["disk"]),
+ "Admin checkpoint and resume state have different physical owners")
+ value = journal.checkpoint_value(bytes_of(checkpoint), token, bucket)
+ admin = value["admin"]
+ require(admin.get("set_disk_id") == "pool_0_set_0" and canonical(admin.get("options")) == canonical(OPTIONS),
+ "checkpoint changed the exact admitted set or Admin options")
+ outcome = admin.get("outcome") or {}
+ counters = outcome.get("counters") or {}
+ require(outcome.get("execution", {}).get("state") == "running"
+ and outcome.get("coverage") == "partial"
+ and all(type(counters.get(name)) is int and 0 <= counters[name] < 2**64
+ for name in ("processed", "healed", "unchanged", "skipped", "failed", "unknown", "attemptFailures"))
+ and counters.get("overflowed") is False, "Admin checkpoint omitted truthful running outcome counters")
+ require(counters["processed"] == sum(counters[name] for name in ("healed", "unchanged", "skipped", "failed"))
+ and counters["unknown"] <= counters["skipped"] and counters["attemptFailures"] >= counters["failed"],
+ "Admin checkpoint counters do not account for the actual durable dispositions")
+ require(not require_progress or counters["processed"] > 0,
+ "NOT_OBSERVED: original Admin checkpoint has no durable object progress")
+ state = journal.decode_json(bytes_of(resume))
+ require(type(state.get("schema_version")) is int and state["schema_version"] == 7
+ and state.get("task_id") == token and state.get("task_type") == "admin_erasure_set"
+ and state.get("set_disk_id") == "pool_0_set_0" and state.get("completed") is False
+ and not state.get("replacement_generation") and not state.get("replacement_targets"),
+ "checkpoint companion is not the original runnable Admin erasure-set resume state")
+ return {"taskId": token, "bucket": bucket, "bucketIncarnationId": admin["buckets"][bucket],
+ "node": checkpoint["node"], "disk": checkpoint["disk"], "checkpoint": checkpoint,
+ "resume": resume, "value": value, "resumeValue": state, "originalDurableResponsibility": True}
+
+
+def owned_root_admission(observed, token):
+ journal.task_id(token)
+ names = journal.names("root", token)
+ require(isinstance(observed, dict) and len(observed) in {1, 4}, "root admission omitted bound nodes")
+ present = []
+ for node, census in observed.items():
+ rows = census.get("records")
+ require(census.get("readOnly") is True and census.get("family") == "root" and census.get("taskId") == token
+ and census.get("directoryEnumerationUsed") is False
+ and (census.get("quiescent") is True or census.get("stable") is True
+ and census.get("processBound") is True and census.get("independentReads") == 2)
+ and isinstance(rows, list) and len(rows) == 12,
+ "root admission lacks a stopped or stable process-bound exact-path census")
+ seen = set()
+ for row in rows:
+ pair = row.get("disk"), row.get("relativePath")
+ require(type(pair[0]) is int and 0 <= pair[0] < 4 and pair[1] in names
+ and pair not in seen and type(row.get("present")) is bool,
+ "root admission census duplicated or escaped its task/disk paths")
+ seen.add(pair)
+ if row["present"]: present.append({"node": node, **row})
+ require(len(present) == 1 and present[0]["relativePath"] == names[0],
+ "NOT_OBSERVED: runnable root admission is absent, duplicated or already terminal")
+ owner = present[0]
+ value = journal.decode_json(bytes_of(owner))
+ require(type(value.get("schema")) is int and value["schema"] == 3 and value.get("task_id") == token
+ and journal.same_json(value.get("heal_type"),
+ {"type": "erasure_set", "buckets": [], "set_disk_id": "pool_0_set_0", "all_buckets": True})
+ and value.get("bucket_incarnation_id") is None and value.get("priority") == "High"
+ and type(value.get("retry_attempts")) is int and value["retry_attempts"] == 0,
+ "root admission does not bind the original explicit Admin erasure-set task")
+ required = {"scan_mode": 2, "remove_corrupted": False, "recreate_missing": False, "update_parity": False,
+ "recursive": True, "dry_run": False, "no_lock": False, "pool_index": 0, "set_index": 0}
+ options = value.get("options")
+ require(isinstance(options, dict) and all(name in options and journal.same_json(options[name], expected)
+ for name, expected in required.items()), "root admission changed the original Admin options/selectors")
+ created = value.get("created_at")
+ require(isinstance(created, dict) and set(created) == {"secs_since_epoch", "nanos_since_epoch"}
+ and type(created["secs_since_epoch"]) is int and 0 <= created["secs_since_epoch"] < 2**64
+ and type(created["nanos_since_epoch"]) is int and 0 <= created["nanos_since_epoch"] < 1_000_000_000,
+ "root admission lost its original admitted time")
+ return {**owner, "value": value, "originalDurableResponsibility": True}
+
+
+def records(fleet, token, label, *, quiescent=False, family="checkpoint", nodes=None):
+ result = {}
+ selected = fleet.nodes if nodes is None else nodes
+ require(selected and set(selected) <= set(fleet.nodes), "checkpoint observation escaped its bound nodes")
+ for node in selected:
+ params = {"family": family, "taskId": token}
+ if not quiescent:
+ params["expected"] = fleet.record(label + "-process", {"node": node, **fleet.remote(node, "process")})
+ result[node] = fleet.remote(node, "journal-census" if quiescent else "journal-observe", **params)
+ return fleet.record(label, result)
+
+
+def admit(probe, fleet, active):
+ fleet.record("Admin-checkpoint-route-contract", REFERENCE)
+ result = fleet.record("Admin-erasure-set-admission", probe.admin(PATH, [], json.dumps(OPTIONS).encode()))
+ require(result.get("httpStatus") == 200 and isinstance(result.get("response"), dict),
+ "Admin erasure-set admission failed")
+ token = journal.task_id(result["response"].get("clientToken"))
+ active.append((PATH, token))
+ return token
+
+
+def capture_earlier(fleet, token, bucket, identifier, *, timeout=60, advance=False):
+ require(type(timeout) is int and 0 < timeout <= 120, "unreviewed checkpoint capture budget")
+ require(type(advance) is bool and (not advance or timeout == 60), "atomic checkpoint capture uses its frozen budget")
+ owner, identity = freeze(fleet, token, bucket, timeout=timeout)
+ node = owner["node"]
+ receipt = fleet.record("Admin-checkpoint-original-capture", fleet.remote(node,
+ "journal-capture-advance" if advance else "journal-capture",
+ family="checkpoint", taskId=token, disk=owner["disk"], relativePath=owner["checkpoint"]["relativePath"],
+ faultId=identifier, expectedBefore=journal.entry(bytes_of(owner["checkpoint"])), expected=identity))
+ require(receipt.get("processBound") is True and receipt.get("originalProductBytes") is True
+ and receipt.get("productMetadataMutated") is False
+ and all(journal.same_json(receipt.get(name), value) for name, value in {
+ "runId": fleet.run_id, "family": "checkpoint", "taskId": token, "disk": owner["disk"],
+ "relativePath": owner["checkpoint"]["relativePath"], "faultId": identifier,
+ "binarySha256": identity.get("binarySha256"),
+ "captured": journal.entry(bytes_of(owner["checkpoint"]))}.items()),
+ "checkpoint capture lost its original process, bytes or exact physical owner")
+ if advance:
+ require(receipt.get("checkpointAdvancedAndFrozen") is True and receipt.get("pidfdBound") is True
+ and receipt.get("serviceFrozen") is True and receipt.get("processIdentity") == identity
+ and type(receipt.get("observedProcessed")) is int
+ and receipt["observedProcessed"] > owner["value"]["admin"]["outcome"]["counters"]["processed"],
+ "atomic checkpoint advance lacks its original frozen process and real progress")
+ return {**owner, "captureId": identifier, "captureReceipt": receipt,
+ "advancedAndFrozen": True, "processIdentity": identity}
+ fleet.record("checkpoint-capture-resume", fleet.remote(node, "process-signal", signal="SIGCONT", expected=identity))
+ resumed = fleet.record("checkpoint-capture-resumed-process", fleet.remote(node, "process"))
+ require(resumed.get("state") in {"R", "S", "D", "I"}
+ and all(resumed.get(name) == identity.get(name) for name in PROCESS_FIELDS),
+ "checkpoint capture did not resume its original process")
+ del fleet.frozen_processes[node]
+ return {**owner, "captureId": identifier, "captureReceipt": receipt}
+
+
+def after_generation(current, earlier):
+ require(all(journal.same_json(current.get(name), earlier.get(name))
+ for name in ("taskId", "bucket", "bucketIncarnationId", "node", "disk")),
+ "checkpoint advanced on a different original task, incarnation or physical owner")
+ require(current["value"]["admin"]["outcome"]["counters"]["processed"] >
+ earlier["value"]["admin"]["outcome"]["counters"]["processed"],
+ "NOT_OBSERVED: original checkpoint has not durably advanced past its captured generation")
+ # The same admission, counters, cursor and digest checks precede actual mutation.
+ journal.transform(bytes_of(current["checkpoint"]), {"family": "checkpoint", "taskId": current["taskId"],
+ "relativePath": current["checkpoint"]["relativePath"], "kind": "stale-generation"},
+ {"bucket": current["bucket"]}, None, earlier=bytes_of(earlier["checkpoint"]))
+
+
+def freeze(fleet, token, bucket, *, timeout=60, earlier=None):
+ require(type(timeout) is int and 0 < timeout <= 120, "unreviewed checkpoint observation budget")
+ if earlier is not None and earlier.get("advancedAndFrozen") is True:
+ owner, node = earlier, earlier["node"]
+ identity = fleet.frozen_processes.get(node)
+ require(node in fleet.nodes and isinstance(identity, dict) and identity == earlier.get("processIdentity")
+ and all(name in identity for name in PROCESS_FIELDS) and type(identity.get("pid")) is int
+ and identity["pid"] > 1, "atomic checkpoint advance lost its registered original frozen birth")
+ else:
+ deadline = time.monotonic() + timeout
+ while True:
+ owner = None
+ for candidate in fleet.nodes:
+ try:
+ observed = records(fleet, token, "Admin-checkpoint-live", nodes=(candidate,))
+ owner = owned_checkpoint(observed, token, bucket)
+ if earlier is not None: after_generation(owner, earlier)
+ except ValueError as exc:
+ if not str(exc).startswith("NOT_OBSERVED:"): raise
+ owner = None
+ continue
+ identity = observed[candidate].get("processIdentity")
+ require(owner["node"] == candidate and observed[candidate].get("processBound") is True
+ and observed[candidate].get("stable") is True and observed[candidate].get("independentReads") == 2
+ and isinstance(identity, dict) and all(name in identity for name in PROCESS_FIELDS)
+ and type(identity.get("pid")) is int and identity["pid"] > 1,
+ "checkpoint owner has no census-bound process birth identity")
+ break
+ if owner is not None: break
+ require(time.monotonic() < deadline, "NOT_OBSERVED: actual durable Admin checkpoint window exceeded its frozen budget")
+ time.sleep(.1)
+ node = owner["node"]
+ # Freeze the witnessed owner before another SSH read can outlast its task.
+ # The complete census below still rejects any second physical owner.
+ fleet.record("checkpoint-owner-process", identity)
+ require(node not in fleet.frozen_processes, "checkpoint owner is already tracked as frozen")
+ fleet.frozen_processes[node] = identity
+ fleet.record("checkpoint-owner-freeze", fleet.remote(node, "process-signal", signal="SIGSTOP", expected=identity))
+ frozen = fleet.record("checkpoint-owner-frozen-process", fleet.remote(node, "process"))
+ require(frozen.get("state") == "T" and all(frozen.get(name) == identity.get(name) for name in PROCESS_FIELDS),
+ "checkpoint freeze missed the original process")
+ current = owned_checkpoint(records(fleet, token, "Admin-checkpoint-frozen"), token, bucket)
+ require((current["node"], current["disk"], current["bucketIncarnationId"]) ==
+ (owner["node"], owner["disk"], owner["bucketIncarnationId"]), "checkpoint owner changed during freeze")
+ if earlier is not None: after_generation(current, earlier)
+ # Reading another live census can outlast the actual checkpoint window.
+ # Validate the runnable root only after freezing its original checkpoint owner.
+ frozen_root = owned_root_admission(records(fleet, token, "Admin-root-frozen", family="root"), token)
+ return {**current, "rootAdmission": frozen_root}, identity
+
+
+def stop_for_fault(fleet, token, bucket, frozen, identity):
+ node = frozen["node"]
+ require(fleet.frozen_processes.get(node) == identity, "checkpoint crash lost its tracked original process")
+ exit_proof = fleet.record("checkpoint-owner-exact-crash", fleet.remote(node, "process-crash", expected=identity))
+ require(exit_proof.get("actualSignal") == 9 and exit_proof.get("pidfdBound") is True
+ and exit_proof.get("graceful") is False and exit_proof.get("platformEventIssued") is False,
+ "checkpoint interruption has no exact process-bound SIGKILL witness")
+ del fleet.frozen_processes[node]
+ fleet.stop()
+ current = owned_checkpoint(records(fleet, token, "Admin-checkpoint-stopped", quiescent=True), token, bucket)
+ require(current["checkpoint"] == frozen["checkpoint"] and current["resume"] == frozen["resume"],
+ "Admin durable responsibility changed between freeze and stopped fault boundary")
+ root = owned_root_admission(records(fleet, token, "Admin-root-stopped", quiescent=True, family="root"), token)
+ try:
+ from .root_restart import same_owner
+ except ImportError:
+ from root_restart import same_owner
+ # A different root coordinator may shut down gracefully when all peers
+ # stop. Preserve every non-timeout field and forbid a refunded budget.
+ previous = frozen["rootAdmission"]
+ if all(root[name] == previous[name] for name in ("node", "disk", "relativePath", "size", "sha256")):
+ continuity = {"samePhysicalOwner": True, "nonTimeoutIntentUnchanged": True,
+ "budgetNotRefunded": True, "gracefulTimeoutCheckpointClaimed": False}
+ else:
+ continuity = same_owner(previous, root)
+ fleet.record("Admin-root-stop-continuity", continuity)
+ return {**current, "rootAdmission": root, "rootContinuity": continuity}
diff --git a/scripts/validation_heal/coordinator_crash.py b/scripts/validation_heal/coordinator_crash.py
new file mode 100644
index 00000000..2335124c
--- /dev/null
+++ b/scripts/validation_heal/coordinator_crash.py
@@ -0,0 +1,434 @@
+#!/usr/bin/env python3
+"""F04: distinct H2 root, MRF and replacement coordinator SIGKILL boundaries."""
+
+from __future__ import annotations
+
+import argparse
+import copy
+from datetime import datetime, timedelta, timezone
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import secrets
+import signal
+import sys
+import time
+from urllib.parse import quote
+
+try:
+ from .heal_probe import Probe, canonical, require, save
+ from .heal_workload import Recorder, seed, verify_seed
+ from .healthy import Fleet, validate_topology
+ from .inspector import verify as verify_inspector
+ from .matrix import select
+ from .cursor import run_topology as dx_topology
+ from . import root_restart, mrf_replay, replacement, replacement_journal
+ from .mrf_records import checkpoint, REFERENCE as MRF_REFERENCE
+ from .shards import files
+ from .scanner_repair import event_time
+ from .stop import validate_unknown_token
+ from .unformatted import binding as lab_binding
+ from .fleet_node import PROCESS_FIELDS
+except ImportError:
+ from heal_probe import Probe, canonical, require, save
+ from heal_workload import Recorder, seed, verify_seed
+ from healthy import Fleet, validate_topology
+ from inspector import verify as verify_inspector
+ from matrix import select
+ from cursor import run_topology as dx_topology
+ import root_restart, mrf_replay, replacement, replacement_journal
+ from mrf_records import checkpoint, REFERENCE as MRF_REFERENCE
+ from shards import files
+ from scanner_repair import event_time
+ from stop import validate_unknown_token
+ from unformatted import binding as lab_binding
+ from fleet_node import PROCESS_FIELDS
+
+
+CASE_ID = "heal.coordinator-process-crash"
+BRANCHES = ("root-coordinator-kill", "mrf-coordinator-kill", "replacement-coordinator-kill")
+MRF_COUNT = 64
+REPLACEMENT_COUNT = 1025
+WINDOW_SECONDS = 90
+REFERENCE = {"candidateCommit": root_restart.REFERENCE["candidateCommit"],
+ "root": root_restart.REFERENCE, "mrf": MRF_REFERENCE, "replacement": replacement_journal.REFERENCE,
+ "concurrencyConfigSource": {"crates/config/src/constants/heal.rs":
+ "82b8614dfa7cc111e694138035e4a84ee23035c719f0286fc0565d7181ca014f"},
+ "supportTable": {
+ "root": "durable original admission and token; SIGKILL cannot checkpoint elapsed timeout or cursor",
+ "mrf": "durable exact ACK responsibility; task token is volatile, automatic rescan uses a new token",
+ "replacement": "durable original generation and target identities; target process remains alive"},
+ "fault": "one exact run-owned paused coordinator process, Linux pidfd SIGKILL; same boot/roots/candidate"}
+
+
+def processes(fleet, label):
+ return fleet.record(label, {node: fleet.remote(node, "process") for node in fleet.nodes})
+
+
+def continuity(before, after, owner, *, private_mounts=False):
+ require(len(before) == 4 and set(before) == set(after) and owner in before,
+ "coordinator crash omitted an actual H2 peer")
+ for node, old in before.items():
+ new = after[node]
+ ignored = {"pid", "startTicks"} | ({"mountNamespace"} if private_mounts else set()) if node == owner else set()
+ require(all(new.get(name) == old.get(name) for name in PROCESS_FIELDS if name not in ignored),
+ "coordinator restart changed a target/peer process, boot, candidate, unit or environment")
+ require(type(new.get("pid")) is int and new["pid"] > 1 and type(new.get("startTicks")) is int,
+ "coordinator recovery omitted an actual running process")
+ if node == owner:
+ require((new["pid"], new["startTicks"]) != (old["pid"], old["startTicks"])
+ and isinstance(new.get("mountNamespace"), str) and new["mountNamespace"],
+ "coordinator did not actually restart in a witnessed namespace")
+ return {"onlyCoordinatorRestarted": True, "everyTargetAndPeerKeptOriginalProcess": True,
+ "sameBootUnitCandidateSettingsAndRoots": True, "privateMountNamespaceMayRestart": private_mounts}
+
+
+def freeze(fleet, owner, before):
+ identity = before[owner]
+ fleet.frozen_processes[owner] = identity
+ fleet.record("coordinator-SIGSTOP", fleet.remote(owner, "process-signal", signal="SIGSTOP", expected=identity))
+ current = fleet.record("coordinator-frozen-process", fleet.remote(owner, "process"))
+ require(current.get("state") == "T" and all(current.get(name) == identity.get(name) for name in PROCESS_FIELDS),
+ "coordinator freeze missed its original process birth")
+ return identity
+
+
+def kill(fleet, owner, identity, peers):
+ proof = fleet.record("coordinator-actual-SIGKILL", fleet.remote(owner, "process-crash", expected=identity))
+ require(proof.get("actualSignal") == 9 and proof.get("signal") == "SIGKILL" and proof.get("graceful") is False
+ and proof.get("pidfdBound") is True and proof.get("platformEventIssued") is False,
+ "coordinator interruption lacks its exact non-graceful process-bound exit")
+ del fleet.frozen_processes[owner]
+ for node in fleet.nodes:
+ if node == owner: continue
+ actual = fleet.record("peer-kept-running-during-crash", fleet.remote(node, "process"))
+ require(all(actual.get(name) == peers[node].get(name) for name in PROCESS_FIELDS),
+ "target or non-coordinator peer changed process during SIGKILL")
+ fleet.record("exited-coordinator-unit-released", fleet.remote(owner, "stop"))
+ return proof
+
+
+def replacement_checkpoint(observed, generation):
+ token, endpoint = generation["taskId"], generation["targetSlots"][0]
+ require(observed.get("taskId") == token and observed.get("readOnly") is True
+ and observed.get("stable") is True and len(observed.get("records", [])) == 12,
+ "replacement checkpoint lost its exact generation or whole physical-root census")
+ seen, intents, seals = set(), {}, set()
+ for row in observed["records"]:
+ pair = row.get("disk"), row.get("kind")
+ require(type(pair[0]) is int and 0 <= pair[0] < 4 and pair[1] in replacement_journal.FILES
+ and pair not in seen and type(row.get("present")) is bool,
+ "replacement checkpoint duplicated or invented a disk/name identity")
+ seen.add(pair)
+ suffix = replacement_journal.FILES[pair[1]][0]
+ require(row.get("relativePath") == f".rustfs.sys/buckets/ahm-replacement/{token}_{suffix}",
+ "replacement checkpoint escaped the exact reviewed generation name")
+ if not row["present"]: continue
+ require(type(row.get("size")) is int and row["size"] > 0
+ and bool(re.fullmatch(r"[0-9a-f]{64}", row.get("sha256", ""))), "replacement checkpoint lacks whole-file identity")
+ if pair[1] == "seal":
+ require(row.get("sealed") is True and row["size"] == 6
+ and row["sha256"] == hashlib.sha256(b"sealed").hexdigest(), "replacement intent is not committed")
+ seals.add(pair[0])
+ elif pair[1] == "proof":
+ raise ValueError("NOT_OBSERVED: replacement already has completion proof before its required crash")
+ else:
+ value = row.get("value", {})
+ require(type(value.get("schema_version")) is int and value["schema_version"] == 7
+ and value.get("task_id") == token and value.get("replacement_generation") == token
+ and value.get("set_disk_id") == generation["setDiskId"]
+ and value.get("replacement_targets") == [endpoint] and value.get("completed") is False,
+ "replacement intent lost its original generation/set/target or is already complete")
+ identities = value.get("replacement_target_identities")
+ require(isinstance(identities, list) and len(identities) == 1 and identities[0].get("endpoint") == endpoint
+ and isinstance(identities[0].get("canonical_path"), str) and identities[0]["canonical_path"].startswith("/")
+ and isinstance(identities[0].get("physical_device_ids"), list) and identities[0]["physical_device_ids"]
+ and all(isinstance(item, str) and item for item in identities[0]["physical_device_ids"])
+ and isinstance(identities[0].get("filesystem_identity"), str) and identities[0]["filesystem_identity"],
+ "replacement checkpoint omitted the actual target path/device/filesystem identity")
+ intents[pair[0]] = row
+ require(intents and set(intents) <= seals, "NOT_OBSERVED: coordinator has no sealed durable replacement intent")
+ return {"generation": token, "sealedRecordDisks": sorted(intents), "originalTargetBound": True,
+ "physicalPartialWindowRequiredSeparately": True}
+
+
+def interrupt_replacement(fleet, owner, proofs, original_format, generation):
+ target = proofs[0]["node"]
+ require(owner != target and len(fleet.nodes) == 4, "F04 requires a coordinator distinct from the live replacement target")
+ before = processes(fleet, "replacement-coordinator-and-target-processes")
+ identity = freeze(fleet, owner, before)
+ scene = fleet.snapshot("replacement-coordinator-frozen-scene")
+ progress = replacement.target_progress(scene, proofs)
+ require(progress["partial"], "NOT_OBSERVED: replacement was not physically partial at coordinator freeze")
+ require(all(scene[node]["formats"] == original_format[node]["formats"] for node in fleet.nodes),
+ "replacement changed its target UUID/set before the coordinator crash")
+ original = fleet.record("replacement-coordinator-durable-checkpoint", fleet.remote(owner,
+ "replacement-records", taskId=generation["taskId"]))
+ checkpoint_proof = replacement_checkpoint(original, generation)
+ exit_proof = kill(fleet, owner, identity, before)
+ after = fleet.record("replacement-coordinator-records-after-SIGKILL", fleet.remote(owner,
+ "replacement-records", taskId=generation["taskId"], quiescent=True))
+ replacement_checkpoint(after, generation)
+ require(canonical(original) == canonical(after), "SIGKILL lost or rewrote the coordinator's committed replacement intent")
+ stopped_scene = fleet.record("replacement-target-still-partial-after-coordinator-exit", {
+ node: fleet.remote(node, "snapshot", quiescent=node == owner) for node in fleet.nodes})
+ after_progress = replacement.target_progress(stopped_scene, proofs)
+ require(after_progress["partial"] and set(progress["completeDataVersions"]) <= set(after_progress["completeDataVersions"]),
+ "coordinator exit lost restored parts or missed the unfinished replacement window")
+ require(all(stopped_scene[node]["formats"] == original_format[node]["formats"] for node in fleet.nodes),
+ "coordinator crash changed a replacement disk identity")
+ fleet.record("same-replacement-coordinator-restarted", fleet.remote(owner, "start"))
+ fleet.wait_ready()
+ restarted = processes(fleet, "replacement-processes-after-coordinator-restart")
+ continuity_proof = continuity(before, restarted, owner, private_mounts=True)
+ archive = fleet.record("original-target-archive-still-refused", fleet.remote(target, "replacement-probe"))
+ require(archive.get("serviceReadDenied") is True and archive.get("sameOriginalServiceProcess") is True,
+ "live target gained backup access during the coordinator restart")
+ return {"branch": "replacement-coordinator-kill", "coordinator": owner, "targetKeptRunning": target,
+ "originalGeneration": generation, "before": progress, "afterActualExit": after_progress,
+ "checkpoint": checkpoint_proof, "actualExit": exit_proof, "processContinuity": continuity_proof,
+ "sameCommittedGenerationRequiredForCompletion": True, "adminCompensationTasks": 0}
+
+
+def mrf_progress(snapshot, authorities, bucket, obligations, target):
+ """Observe partial physical ACK replay; final cohort streaming is a separate gate."""
+ validate_topology(snapshot, "H2")
+ current, original, complete = files(snapshot), files(authorities), []
+ for item in obligations:
+ wanted, metadata_path = item["versionId"], f"{bucket}/{item['key']}/xl.meta"
+ candidates = []
+ for (node, disk, path), entry in original.items():
+ if node == target or path != metadata_path: continue
+ versions = [value for value in entry.get("metadata", {}).get("versions", [])
+ if (value.get("versionId") or "null") == wanted]
+ require(len(versions) == 1, "MRF physical authority omitted its exact ACK version")
+ value = versions[0]
+ logical = {name: value.get(name) for name in
+ ("versionId", "isLatest", "deleted", "dataDir", "size", "parts", "usesLegacyChecksum")}
+ logical["erasure"] = {name: value.get("erasure", {}).get(name) for name in
+ ("data_blocks", "parity_blocks", "block_size", "distribution", "algorithm")}
+ candidates.append(logical)
+ require(len(candidates) == 12 and all(value == candidates[0] for value in candidates)
+ and candidates[0]["deleted"] is False and candidates[0]["isLatest"] is True,
+ "MRF progress has no single original twelve-member ACK authority")
+ authority = candidates[0]
+ restored = 0
+ for layout in snapshot[target]["formats"]:
+ disk = layout["disk"]
+ metadata = current.get((target, disk, metadata_path), {}).get("metadata", {})
+ matches = [value for value in metadata.get("versions", []) if (value.get("versionId") or "null") == wanted]
+ if not matches: continue
+ require(len(matches) == 1 and metadata.get("bucket") == bucket and metadata.get("key") == item["key"],
+ "MRF replay confused physical key/version identities")
+ version, erasure = matches[0], matches[0].get("erasure", {})
+ logical = {name: version.get(name) for name in authority if name != "erasure"}
+ logical["erasure"] = {name: erasure.get(name) for name in authority["erasure"]}
+ geometry = layout["value"]["xl"]
+ slot = geometry["sets"][0].index(geometry["this"])
+ require(logical == authority and type(erasure.get("index")) is int
+ and erasure["index"] == erasure["distribution"][slot]
+ and version.get("role") == ("data" if erasure["index"] <= erasure["data_blocks"] else "parity"),
+ "MRF replay changed logical authority or its actual target set role")
+ parts = version.get("parts", [])
+ require(version.get("inline") is None and len(parts) == 1 and parts[0].get("number") == 1
+ and parts[0].get("size") == parts[0].get("actualSize") > 1024**2,
+ "MRF progress requires its reviewed external plaintext part")
+ path = f"{bucket}/{item['key']}/{version['dataDir']}/part.1"
+ entry = current.get((target, disk, path))
+ if entry:
+ require(type(entry.get("size")) is int and entry["size"] > 32
+ and bool(re.fullmatch(r"[0-9a-f]{64}", entry.get("sha256", ""))),
+ "MRF progress lacks complete part-file identity")
+ restored += 1
+ if restored == 4: complete.append(list(mrf_replay.identity(item)))
+ return {"completeACKs": complete, "pendingACKs": len(obligations) - len(complete),
+ "partial": 0 < len(complete) < len(obligations), "finalStreamingProofClaimed": False}
+
+
+def active_mrf_task(events, obligations, bindings, after, target):
+ candidates = []
+ for item in obligations:
+ for owner in bindings[mrf_replay.identity(item)]:
+ node = owner["node"]
+ if node == target: continue
+ rows = events.get(node, [])
+ for start in rows:
+ token, stamp = start.get("task_id"), event_time(start)
+ if not (start.get("target") == "rustfs::heal::task" and start.get("stage") == "start"
+ and start.get("message") == "Heal object started" and start.get("bucket") == item["bucket"]
+ and start.get("object") == item["key"] and "version_id" in start
+ and mrf_replay.source_version(start["version_id"], item["versionId"])
+ and isinstance(token, str) and token and stamp is not None and stamp >= after): continue
+ accepted = any(row.get("target") == "rustfs::heal::manager" and row.get("source") == "mrf"
+ and row.get("message") == "Heal queue request accepted" and row.get("result") == "accepted"
+ and row.get("request_id") == token and event_time(row) is not None
+ and after <= event_time(row) <= stamp for row in rows)
+ terminal = any(row.get("target") == "rustfs::heal::manager" and row.get("task_id") == token
+ and row.get("state") == "task_completed" and event_time(row) is not None
+ and event_time(row) >= stamp for row in rows)
+ if accepted and not terminal: candidates.append((node, token, item))
+ require(len(candidates) == len({(node, token) for node, token, _ in candidates}),
+ "MRF source duplicated an active task identity")
+ return candidates
+
+
+def interrupt_mrf(probe, fleet, window, binding, bucket, expected, obligations, bindings, rejoin_time,
+ before, authorities, target):
+ fleet.record("MRF-crash-support-table", REFERENCE)
+ fleet.record("MRF-pre-registered-crash-window", {"objects": MRF_COUNT, "windowSeconds": WINDOW_SECONDS,
+ "maxConcurrentHeals": 1, "maxConcurrentPerSet": 1, "replayBatch": 1,
+ "actualProcessConfiguration": {node: value["backgroundConfig"] for node, value in before.items()},
+ "partialPhysicalProgressRequired": True, "fixedSleepUsedAsWindow": False})
+ deadline = time.monotonic() + WINDOW_SECONDS
+ chosen = None
+ while chosen is None:
+ events = window.capture()
+ scene = fleet.snapshot("MRF-coordinator-running-window")
+ progress = mrf_progress(scene, authorities, bucket, obligations, target)
+ for owner, token, obligation in active_mrf_task(events, obligations, bindings, rejoin_time, target):
+ if not progress["partial"] or list(mrf_replay.identity(obligation)) in progress["completeACKs"]: continue
+ endpoint = "node" + str(fleet.nodes.index(owner) + 1)
+ path = "/rustfs/admin/v3/heal/" + quote(bucket, safe="") + "/" + quote(obligation["key"], safe="/")
+ observed = fleet.record("MRF-original-volatile-task-running", Probe(binding, endpoint).admin(path, [("clientToken", token)], b""))
+ require(observed.get("httpStatus") == 200 and isinstance(observed.get("response"), dict),
+ "MRF active source task lost its exact status before freeze")
+ if observed["response"].get("summary") == "running":
+ chosen = owner, token, obligation, endpoint, path
+ break
+ require(chosen is not None or time.monotonic() < deadline, "NOT_OBSERVED: no active MRF coordinator with partial physical ACK replay")
+ if chosen is None: time.sleep(.1)
+ owner, token, obligation, endpoint, path = chosen
+ peers = processes(fleet, "MRF-processes-before-coordinator-freeze")
+ process_identity = freeze(fleet, owner, peers)
+ frozen = fleet.snapshot("MRF-frozen-coordinator-partial-scene")
+ progress = mrf_progress(frozen, authorities, bucket, obligations, target)
+ require(progress["partial"] and list(mrf_replay.identity(obligation)) not in progress["completeACKs"],
+ "NOT_OBSERVED: original MRF task finished before its physical crash boundary")
+ require(any(node == owner and active == token for node, active, _ in
+ active_mrf_task(window.capture(), obligations, bindings, rejoin_time, target)),
+ "NOT_OBSERVED: MRF task became terminal before the coordinator freeze")
+ raw = fleet.record("MRF-frozen-committed-records", fleet.remote(owner, "mrf-records"))
+ committed = checkpoint(raw)
+ # The original binding already checked the actual enqueue time. Require
+ # that exact intent digest below, rather than re-dating it at rejoin.
+ bound = mrf_replay.pending_bindings({owner: committed}, [obligation], 0)
+ original_owner = next(item for item in bindings[mrf_replay.identity(obligation)] if item["node"] == owner)
+ recovered_owner = bound[mrf_replay.identity(obligation)][0]
+ require(all(recovered_owner[name] == original_owner[name] for name in
+ ("node", "checkpointOwner", "responsibilityId", "bucketIncarnationId"))
+ and recovered_owner["sequence"] >= original_owner["sequence"], "MRF pending crash checkpoint changed its original ACK responsibility")
+ exit_proof = kill(fleet, owner, process_identity, peers)
+ after = fleet.record("MRF-committed-records-after-SIGKILL", fleet.remote(owner, "mrf-records", quiescent=True))
+ require(canonical(raw) == canonical(after), "SIGKILL changed the frozen committed MRF record bytes")
+ fleet.record("same-MRF-coordinator-restarted", fleet.remote(owner, "start"))
+ fleet.wait_ready()
+ restarted = processes(fleet, "MRF-processes-after-coordinator-restart")
+ continuity_proof = continuity(peers, restarted, owner)
+ unknown = fleet.record("lost-volatile-MRF-token-refused", Probe(binding, endpoint).admin(path, [("clientToken", token)], b""))
+ validate_unknown_token(unknown, token)
+ return {"branch": "mrf-coordinator-kill", "coordinator": owner, "targetKeptRunning": target,
+ "volatileOriginalTaskId": token, "ackIdentity": list(mrf_replay.identity(obligation)),
+ "originalResponsibilityId": original_owner["responsibilityId"], "partialPhysicalWindow": progress,
+ "originalIntentDigest": original_owner["intentDigest"], "frozenIntentDigest": recovered_owner["intentDigest"],
+ "actualExit": exit_proof, "processContinuity": continuity_proof,
+ "oldTokenCompletedClaim": False, "newAutomaticTaskRequired": True, "manualCompensationTasks": 0}
+
+
+def root_observer(*args, **kwargs):
+ return root_restart.observe_task(*args, **kwargs, interruption="sigkill")
+
+
+def run_replacement(args, nodes):
+ directory = args.evidence / "replacement-coordinator-kill"
+ directory.mkdir()
+ run_id = f"gh-{os.environ['GITHUB_RUN_ID']}-{os.environ.get('GITHUB_RUN_ATTEMPT', '1')}-h2"
+ fleet = Fleet(nodes, directory, run_id, args.tool_directory, runtime_profile="replacement", resource_profile="full-restart")
+ fleet.node_runtime_profiles = {node: "replacement-target" for node in nodes[1:]}
+ candidate = json.loads(args.candidate.read_text())
+ require(candidate["source_sha"] == REFERENCE["candidateCommit"], "unreviewed coordinator durability boundary")
+ fleet.record("inspector-build", verify_inspector(args.tool_directory, candidate))
+ fleet.record("registered-coordinator-support-table", REFERENCE)
+ fleet.record("registered-replacement-crash-budget", {"objects": REPLACEMENT_COUNT,
+ "objectBytes": mrf_replay.SIZE, "rebuildSeconds": replacement.REBUILD_SECONDS,
+ "requiresNonzeroAndUnfinishedPhysicalProgress": True})
+ bucket = "rv-heal-" + hashlib.sha256(run_id.encode()).hexdigest()[:16]
+ access, secret = secrets.token_hex(16), secrets.token_hex(24)
+ os.environ.update(HEAL_ACCESS_KEY=access, HEAL_SECRET_KEY=secret)
+ binding = lab_binding(args, candidate, nodes, run_id, bucket)
+ binding.update(maxInventoryObjects=REPLACEMENT_COUNT, maxInventoryBytes=2 * 1024**3,
+ leaseExpiresAt=(datetime.now(timezone.utc) + timedelta(minutes=45)).isoformat())
+ fleet.record("binding", binding)
+ probe, active = Probe(binding), []
+ result = {"topology": "H2", "branch": "replacement-coordinator-kill", "verdict": "RUNNING", "cleanup": "PENDING"}
+ try:
+ fleet.deploy(args.binary_sha, bucket, access, secret)
+ fleet.record("actual-topology", validate_topology(fleet.snapshot("replacement-H2-topology"), "H2"))
+ recorder = Recorder(probe, directory / "fixture")
+ expected = seed(recorder, bucket, "DB", REPLACEMENT_COUNT, [mrf_replay.SIZE])
+ baseline = fleet.record("replacement-full-S3-baseline", probe.inventory(bucket, unversioned_current_tags=True))
+ fleet.record("replacement-independent-ACK-manifest", {"expected": expected, **verify_seed(expected, baseline)})
+ fleet.stop()
+ proof = replacement.run_branch(recorder, fleet, bucket, "replacement-coordinator-kill", expected, baseline,
+ active, [], coordinator_interruptor=interrupt_replacement)
+ require(proof["responsibility"]["coordinatorInterruption"]["targetKeptRunning"] == nodes[-1],
+ "replacement coordinator branch did not keep its actual target running")
+ result.update(verdict="PASS", proof=proof)
+ except (Exception, KeyboardInterrupt) as exc:
+ result.update(verdict="FAIL", failure={"type": type(exc).__name__, "message": str(exc)})
+ if fleet.reserved:
+ try: fleet.stop(); fleet.snapshot("replacement-coordinator-failure", quiescent=True)
+ except Exception as capture: result["failureEvidenceError"] = str(capture)
+ finally:
+ result["cleanup"] = "PASS" if fleet.cleanup() else "FAIL"
+ fleet.record("branch-result", result)
+ return result
+
+
+def main():
+ def interrupted(signum, frame): raise KeyboardInterrupt(f"Heal workflow interrupted by signal {signum}")
+ signal.signal(signal.SIGTERM, interrupted)
+ parser = argparse.ArgumentParser(description=__doc__)
+ for name in ("candidate", "rc-bin", "evidence", "tool-directory"): parser.add_argument("--" + name, type=Path, required=True)
+ parser.add_argument("--case-id", choices=(CASE_ID,), required=True)
+ parser.add_argument("--testing-commit", required=True)
+ parser.add_argument("--binary-sha", required=True)
+ args = parser.parse_args()
+ nodes = os.environ.get("RUSTFS_NODES", "vm000 vm001 vm002 vm003").split()
+ require(len(nodes) >= 4 and len(set(nodes)) == len(nodes) and all(re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", node) for node in nodes),
+ "F04 requires the actual four current Heal nodes")
+ require(bool(re.fullmatch(r"[0-9a-f]{64}", args.binary_sha))
+ and bool(re.fullmatch(r"[1-9][0-9]*", os.environ.get("GITHUB_RUN_ID", ""))), "verified workflow candidate required")
+ require(json.loads(args.candidate.read_text())["source_sha"] == REFERENCE["candidateCommit"], "unreviewed coordinator persistence support")
+ recipe = select(CASE_ID)[0]
+ require(recipe["variants"] == list(BRANCHES) and recipe["contract"]["probes"]["topology"] == "H2",
+ "F04 omitted its source branches or topology")
+ results = []
+ try:
+ root_args = copy.copy(args)
+ root_args.evidence = args.evidence / "root-coordinator-kill"
+ root_args.evidence.mkdir()
+ root = dx_topology(root_args, "H2", nodes[:4], observer=root_observer, branches=(BRANCHES[0],),
+ source_branches=BRANCHES, runtime_profile="root-restart", policy_reference=REFERENCE, target_nodes=nodes[1:4])
+ results.append({**root, "branch": BRANCHES[0]})
+ if root["cleanup"] == "PASS":
+ mrf_args = copy.copy(args)
+ mrf_args.evidence = args.evidence / "mrf-coordinator-kill"
+ mrf_args.evidence.mkdir()
+ mrf = mrf_replay.run_branch(mrf_args, "offline-put", nodes[:4], after_rejoin=interrupt_mrf,
+ put_count=MRF_COUNT, fixture="DB")
+ results.append({**mrf, "branch": BRANCHES[1], "mechanismScenario": mrf["branch"]})
+ if results[-1]["cleanup"] == "PASS": results.append(run_replacement(args, nodes[:4]))
+ finally:
+ complete = len(results) == 3 and all(item["verdict"] == "PASS" and item["cleanup"] == "PASS" for item in results)
+ save(args.evidence / "healthy-summary.json", {"caseId": CASE_ID, "results": results,
+ "cleanup": "PASS" if results and all(item["cleanup"] == "PASS" for item in results) else "FAIL", "complete": complete})
+ require(complete, "F04 requires all three H2 coordinator mechanisms, real partial SIGKILL windows and complete cleanup")
+
+
+if __name__ == "__main__":
+ try: main()
+ except Exception as exc:
+ print(f"Heal coordinator crash validation failed: {exc}", file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_heal/cursor.py b/scripts/validation_heal/cursor.py
new file mode 100644
index 00000000..79162bce
--- /dev/null
+++ b/scripts/validation_heal/cursor.py
@@ -0,0 +1,444 @@
+#!/usr/bin/env python3
+"""J03: real retained-window gaps with independent, complete DX verification."""
+
+from __future__ import annotations
+
+import argparse
+from datetime import datetime, timedelta, timezone
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import secrets
+import signal
+import sys
+import time
+from urllib.parse import quote
+
+try:
+ from .heal_probe import Probe, canonical, compare_inventories, require, save
+ from .heal_workload import Recorder, seed, verify_seed
+ from .healthy import BASE_OPTIONS, Fleet, physical_content, validate_topology
+ from .inspector import verify as verify_inspector
+ from .matrix import select
+ from .heal_fault import bounded_fault_id
+ from .shards import files, map_target, validate_repaired_physical
+ from .versions import public_version_id
+ from .fleet_node import resource_limits
+except ImportError:
+ from heal_probe import Probe, canonical, compare_inventories, require, save
+ from heal_workload import Recorder, seed, verify_seed
+ from healthy import BASE_OPTIONS, Fleet, physical_content, validate_topology
+ from inspector import verify as verify_inspector
+ from matrix import select
+ from heal_fault import bounded_fault_id
+ from shards import files, map_target, validate_repaired_physical
+ from versions import public_version_id
+ from fleet_node import resource_limits
+
+
+CASE_ID = "heal.incremental-cursor-and-truncation"
+BRANCHES = ("within-window-cursor", "past-retention-gap", "cross-node-cursor", "terminal-truncation")
+RETAINED_ITEMS = 1024
+OBJECTS = RETAINED_ITEMS + 1
+SIZE = 1024**2 + 73
+TASK_SECONDS = 600
+LIMIT_REFERENCE = {"candidateCommit": "072b57b37559fbe626aeda8abf61617fdcbdd926",
+ "path": "crates/heal/src/heal/task.rs",
+ "sourceSha256": "81e8e2ec8807d1094095e444d0aaf8913f94114836bc1bc9f7953cbeff98d855",
+ "retainedItems": RETAINED_ITEMS, "diagnosticObjects": 128,
+ "contract": "sinceSeq is exclusive; nextSeq is the next sequence to allocate",
+ "differentCandidates": "the measured terminal window must still actually exceed retention; no inferred gap"}
+
+
+def wanted_pairs(expected, *, count=OBJECTS):
+ wanted = {(entry["key"], public_version_id(entry["versionId"])): entry for entry in expected}
+ require(type(count) is int and 129 <= count <= OBJECTS
+ and len(wanted) == len(expected) == count and all(not entry["deleteMarker"]
+ and pair[1] == "null" and entry["payload"]["size"] == SIZE for pair, entry in wanted.items()),
+ "DX fixture must contain every pre-registered distinct external-size ACK")
+ return wanted
+
+
+class Window:
+ """One endpoint's cursors, within one admitted task and deployment epoch."""
+
+ def __init__(self, bucket, expected, token, endpoint, epoch):
+ self.bucket, self.wanted = bucket, wanted_pairs(expected)
+ require(isinstance(token, str) and token and isinstance(epoch, str) and epoch,
+ "cursor lacks an admitted task/deployment epoch")
+ self.token, self.endpoint, self.epoch = token, endpoint, epoch
+ self.cursor, self.next_seq, self.min_seq = 0, 1, 1
+ self.seen, self.running_windows, self.gaps = set(), 0, []
+
+ def check(self, status, cursor):
+ require(type(cursor) is int and cursor >= 0, "invalid exclusive cursor")
+ next_seq, min_seq = status.get("nextSeq"), status.get("minSeq")
+ require(type(next_seq) is int and type(min_seq) is int and 1 <= min_seq <= next_seq <= OBJECTS + 1,
+ "cursor response omitted or corrupted its actual sequence bounds")
+ require(status.get("summary") in {"running", "finished"}, "cursor task failed, stopped or disappeared")
+ truncated = status.get("truncated", False)
+ require(type(truncated) is bool, "cursor truncation flag is not boolean")
+ gap = cursor + 1 < min_seq
+ require(not gap or truncated, "past-retention gap was silently omitted")
+ items = status.get("items", [])
+ require(isinstance(items, list) and len(items) <= RETAINED_ITEMS, "invalid retained item window")
+ first = max(cursor + 1, min_seq)
+ available = max(0, next_seq - first)
+ require(len(items) <= available and (len(items) == available or truncated),
+ "cursor items silently skip sequences or exceed their bounds")
+ pairs = []
+ for item in items:
+ pair = item.get("object"), public_version_id(item.get("versionId"))
+ require(item.get("bucket") == self.bucket and pair in self.wanted
+ and item.get("objectSize") == self.wanted[pair]["payload"]["size"],
+ "retained item has the wrong bucket/key/version or acknowledged size")
+ pairs.append(pair)
+ require(len(pairs) == len(set(pairs)), "retained window duplicated an object/version")
+ return {"nextSeq": next_seq, "minSeq": min_seq, "retentionGap": gap,
+ "payloadTruncated": len(items) != available, "firstReturnedSeq": first,
+ "pairs": pairs, "returnedItems": len(items), "truncated": truncated}
+
+ def consume(self, status, cursor, *, token, endpoint, epoch):
+ require((token, endpoint, epoch) == (self.token, self.endpoint, self.epoch),
+ "cursor crossed a task, endpoint or deployment epoch")
+ require(cursor == self.cursor, "cursor jumped over an unconsumed sequence")
+ observed = self.check(status, cursor)
+ require(observed["nextSeq"] >= self.next_seq and observed["minSeq"] >= self.min_seq,
+ "same-task cursor bounds moved backwards")
+ require(not self.seen.intersection(observed["pairs"]), "incremental cursor duplicated an earlier item")
+ self.seen.update(observed["pairs"])
+ if observed["retentionGap"]:
+ self.gaps.append({"sinceSeq": cursor, "minSeq": observed["minSeq"], "truncated": True})
+ # The API may trim its serialized payload separately from item retention.
+ # Advance over received items only, preserving any unreturned suffix.
+ if observed["returnedItems"]:
+ self.cursor = observed["firstReturnedSeq"] + observed["returnedItems"] - 1
+ elif not observed["payloadTruncated"]:
+ self.cursor = max(self.cursor, observed["nextSeq"] - 1)
+ counters = (status.get("outcome") or {}).get("counters", {})
+ processed = counters.get("processed")
+ if (status["summary"] == "running" and type(processed) is int and 0 < processed < OBJECTS
+ and observed["returnedItems"] and not observed["retentionGap"] and not observed["payloadTruncated"]):
+ self.running_windows += 1
+ self.next_seq, self.min_seq = observed["nextSeq"], observed["minSeq"]
+ return {name: value for name, value in observed.items() if name != "pairs"} | {
+ "exclusiveNextCursor": self.cursor, "taskId": token, "endpoint": endpoint, "deploymentEpoch": epoch}
+
+
+def complete_outcome(status, bucket, expected, repaired):
+ wanted = wanted_pairs(expected)
+ require(repaired and set(repaired) <= set(wanted), "cursor repair plan is empty or escapes ACKs")
+ outcome = status.get("outcome") or {}
+ require(status.get("summary") == "finished" and outcome.get("execution", {}).get("state") == "completed"
+ and outcome.get("coverage") == "complete" and outcome.get("objectsTruncated") is True,
+ "terminal cursor task omitted its diagnostic truncation or complete coverage")
+ counters = outcome.get("counters", {})
+ counts = {"processed": OBJECTS, "healed": len(repaired), "unchanged": OBJECTS - len(repaired),
+ "skipped": 0, "failed": 0, "unknown": 0, "attemptFailures": 0, "overflowed": False}
+ require(all(type(counters.get(name)) is type(value) and counters[name] == value for name, value in counts.items()),
+ "terminal canonical counters do not cover every frozen object and real missing shard")
+ receipts = outcome.get("objects")
+ require(isinstance(receipts, list) and 0 < len(receipts) <= 128 < OBJECTS,
+ "diagnostic receipts were incorrectly treated as a complete responsibility manifest")
+ seen, incarnation = set(), None
+ for receipt in receipts:
+ identity = receipt.get("identity", {})
+ pair = identity.get("object"), public_version_id(identity.get("versionId"))
+ require(identity.get("bucket") == bucket and pair in wanted and pair not in seen
+ and type(identity.get("poolIndex")) is int and identity["poolIndex"] == 0
+ and type(identity.get("setIndex")) is int and identity["setIndex"] == 0
+ and isinstance(identity.get("bucketIncarnationId"), str) and identity["bucketIncarnationId"],
+ "bounded diagnostic receipt has a wrong, duplicate or incomplete identity")
+ incarnation = incarnation or identity["bucketIncarnationId"]
+ require(incarnation == identity["bucketIncarnationId"], "diagnostics mixed bucket incarnations")
+ require(receipt.get("disposition", {}).get("state") == ("repaired" if pair in repaired else "verified_healthy"),
+ "bounded diagnostics claimed false repair, unknown responsibility or incomplete integrity")
+ seen.add(pair)
+ return {"canonicalCounters": counters, "bucketIncarnationId": incarnation,
+ "diagnosticObjects": len(receipts), "diagnosticObjectsAreCompleteManifest": False}
+
+
+def terminal_proof(status, bucket, expected, repaired, window):
+ proof = complete_outcome(status, bucket, expected, repaired)
+ retained = window.check(status, 0)
+ require(retained["retentionGap"] and retained["truncated"] and retained["nextSeq"] == OBJECTS + 1,
+ "terminal task never actually exceeded its retained item window")
+ return {**proof, "retainedWindow": {name: value for name, value in retained.items() if name != "pairs"}}
+
+
+def map_cohorts(snapshot, bucket, expected, *, count=OBJECTS):
+ """Index once, then use the existing complete-set oracle on each object."""
+ wanted_pairs(expected, count=count)
+ by_key = {entry["key"]: {node: {"formats": value["formats"], "files": []}
+ for node, value in snapshot.items()} for entry in expected}
+ for node, observation in snapshot.items():
+ for entry in observation["files"]:
+ require(entry["path"].startswith(bucket + "/"), "physical manifest contains an unowned bucket")
+ suffix = entry["path"][len(bucket) + 1:]
+ key = suffix.removesuffix("/xl.meta") if suffix.endswith("/xl.meta") else suffix.rsplit("/", 2)[0]
+ require(key in by_key, "physical baseline contains an unacknowledged object")
+ by_key[key][node]["files"].append(entry)
+ return [map_target(by_key[entry["key"]], bucket, entry["key"], "data" if index % 2 == 0 else "parity", 1)
+ for index, entry in enumerate(expected)]
+
+
+def verify_all_parts(fleet, cohorts, label, *, count=OBJECTS):
+ require(type(count) is int and 129 <= count <= OBJECTS and len(cohorts) == count,
+ "complete physical verification differs from its frozen object count")
+ return verify_part_manifest(fleet, cohorts, label, count=count)
+
+
+def verify_part_manifest(fleet, cohorts, label, *, count):
+ """Stream every member of an explicitly counted external-object manifest."""
+ require(type(count) is int and 1 <= count <= OBJECTS and len(cohorts) == count,
+ "complete external-object manifest differs from its frozen count")
+ width = 4 if len(fleet.nodes) == 1 else 16
+ identities = [(entry["node"], entry["disk"], entry["relativePath"], entry["versionId"], entry["partNumber"])
+ for target in cohorts for entry in target["cohort"]]
+ require(all(len(target["cohort"]) == width for target in cohorts)
+ and len(identities) == len(set(identities)) == count * width
+ and all(entry[0] in fleet.nodes for entry in identities), "complete part manifest duplicated or omitted a frozen shard")
+ total = 0
+ for node in fleet.nodes:
+ entries = [shard for target in cohorts for shard in target["cohort"] if shard["node"] == node]
+ for offset in range(0, len(entries), 128):
+ batch = entries[offset:offset + 128]
+ observed = fleet.record(label, {"node": node, **fleet.remote(node, "verify-parts", parts=[{
+ name: entry[name] for name in ("disk", "relativePath", "versionId", "partNumber")} for entry in batch])})
+ require(observed.get("complete") is True and len(observed.get("parts", [])) == len(batch),
+ "full external-part verifier omitted a batch item")
+ for entry, proof in zip(batch, observed["parts"], strict=True):
+ require(proof.get("physicalBitrotVerified") is True
+ and all(proof.get(name) == entry[name] for name in ("disk", "relativePath", "versionId", "partNumber", "sha256"))
+ and proof.get("physicalSize") == entry["size"], "external part differs from its frozen streaming/hash oracle")
+ total += len(batch)
+ require(total == count * width, "full external-part verifier missed a disk/object")
+ return {"allExternalObjects": count, "allPartsStreamingVerified": total,
+ "sameRoleOriginalShardBytesRestored": True}
+
+
+def validate_missing_batch(before, after, targets, receipts):
+ left, right = files(before), files(after)
+ require(len(targets) == len(receipts) > 1, "cursor fault group omitted its exact receipts")
+ removed = set()
+ for target, receipt in zip(targets, receipts, strict=True):
+ identity = target["node"], target["disk"], target["relativePath"]
+ require(identity in left and identity not in right and identity not in removed
+ and receipt.get("relativePath") == target["relativePath"] and receipt.get("fault") is None
+ and receipt.get("before") == {"sha256": target["sha256"], "size": target["size"]},
+ "cursor missing shard was not physically witnessed on its frozen drive")
+ removed.add(identity)
+ require({key: value for key, value in left.items() if key not in removed} == right
+ and all(before[node]["formats"] == after[node]["formats"] for node in before),
+ "cursor fault group changed metadata, a control shard or disk format")
+ return {"exactMissingParts": len(removed), "otherFilesAndFormatsUnchanged": True}
+
+
+def observe_task(probe, fleet, binding, bucket, expected, targets, active, *, original=None):
+ path = "/rustfs/admin/v3/heal/" + quote(bucket, safe="") + "/objects/"
+ start = fleet.record("trigger", probe.admin(path, [], json.dumps({**BASE_OPTIONS, "recursive": True}).encode()))
+ require(start.get("httpStatus") == 200 and isinstance(start.get("response"), dict), "cursor task admission failed")
+ token = start["response"].get("clientToken")
+ require(isinstance(token, str) and token, "cursor task omitted its real token")
+ active.append((path, token))
+ epoch = fleet.run_id + "/" + token
+ window = Window(bucket, expected, token, "node1", epoch)
+ fleet.record("cursor-identity", {"taskId": token, "path": path, "deploymentEpoch": epoch,
+ "processRestartedDuringTask": False, "responseStartTimeIsEpoch": False,
+ "reason": "candidate renders startTime on each response; use admission token and uninterrupted deployment"})
+ deadline = time.monotonic() + TASK_SECONDS
+ while True:
+ cursor = window.cursor
+ response = fleet.record("cursor-control", probe.admin(path, [("clientToken", token), ("sinceSeq", str(cursor))], b""))
+ require(response.get("httpStatus") == 200 and isinstance(response.get("response"), dict), "incremental status failed")
+ status = response["response"]
+ fleet.record("cursor-window-proof", window.consume(status, cursor, token=token, endpoint="node1", epoch=epoch))
+ if status["summary"] == "finished" and window.cursor == window.next_seq - 1:
+ break
+ require(time.monotonic() < deadline, "incremental task/retained suffix exceeded its pre-registered deadline")
+ time.sleep(0.05)
+ active.remove((path, token))
+ repaired = {(entry["key"], entry["versionId"]) for entry in targets}
+ proofs, old_status = {}, None
+ for endpoint in binding["endpoints"]:
+ client = Probe(binding, endpoint)
+ peer_window = Window(bucket, expected, token, endpoint, epoch)
+ observed = fleet.record("terminal-old-cursor", client.admin(path, [("clientToken", token), ("sinceSeq", "0")], b""))
+ require(observed.get("httpStatus") == 200 and isinstance(observed.get("response"), dict),
+ "terminal old cursor failed on an actual bound endpoint")
+ current = observed["response"]
+ proofs[endpoint] = terminal_proof(current, bucket, expected, repaired, peer_window)
+ require(canonical(current.get("outcome")) == canonical(status.get("outcome")),
+ "terminal responsibility changed or crossed a task on another endpoint")
+ if old_status is not None:
+ require(all(canonical(current.get(name)) == canonical(old_status.get(name)) for name in
+ ("summary", "nextSeq", "minSeq", "items", "truncated", "outcome")),
+ "same-token terminal cursor window differs across nodes")
+ old_status = current
+ caught_up = fleet.record("terminal-caught-up", client.admin(path,
+ [("clientToken", token), ("sinceSeq", str(current["nextSeq"] - 1))], b""))
+ require(caught_up.get("httpStatus") == 200 and isinstance(caught_up.get("response"), dict), "caught-up cursor failed")
+ empty = peer_window.check(caught_up["response"], current["nextSeq"] - 1)
+ require(empty["returnedItems"] == 0 and empty["nextSeq"] == current["nextSeq"]
+ and empty["minSeq"] == current["minSeq"]
+ and canonical(caught_up["response"].get("outcome")) == canonical(current.get("outcome")),
+ "caught-up terminal cursor repeated data or changed responsibility")
+ return {"within-window-cursor": {"verdict": "PASS" if window.running_windows >= 2 else "NOT_OBSERVED",
+ "nonemptyRunningIncrementalWindows": window.running_windows, "duplicateItems": 0,
+ "reason": "" if window.running_windows >= 2 else "two useful running windows were not observed; no timing success claim"},
+ "past-retention-gap": {"verdict": "PASS", "oldCursor": 0, "endpointProofs": proofs, "incrementalGaps": window.gaps},
+ "cross-node-cursor": {"verdict": "PASS" if len(binding["endpoints"]) > 1 else "NOT_APPLICABLE",
+ "queriedEndpoints": list(binding["endpoints"]), "sameTaskId": token, "sameDeploymentEpoch": epoch,
+ "reason": "H1 is one physical node; actual cross-node reads are required and performed on H2"},
+ "terminal-truncation": {"verdict": "PASS", "endpointProofs": proofs,
+ "completeManifestEvidenceRequired": True}}
+
+
+def run_topology(args, topology, nodes, *, observer=observe_task, branches=BRANCHES,
+ count=OBJECTS, fixture_builder=None, missing_indices=None,
+ source_branches=None, runtime_profile="manual", policy_reference=LIMIT_REFERENCE,
+ target_nodes=None):
+ directory = args.evidence / topology
+ directory.mkdir()
+ run_id = f"gh-{os.environ['GITHUB_RUN_ID']}-{os.environ.get('GITHUB_RUN_ATTEMPT', '1')}-{topology.lower()}"
+ fleet = Fleet(nodes, directory, run_id, args.tool_directory, resource_profile="cursor", runtime_profile=runtime_profile)
+ require(type(count) is int and 129 <= count <= OBJECTS, "invalid pre-registered DX size")
+ missing_indices = list(range(0, count, 64)) if missing_indices is None else list(missing_indices)
+ require(len(missing_indices) > 1 and len(set(missing_indices)) == len(missing_indices)
+ and all(type(index) is int and 0 <= index < count for index in missing_indices), "invalid DX fault indices")
+ bucket = "rv-heal-" + hashlib.sha256(run_id.encode()).hexdigest()[:16]
+ access, secret = secrets.token_hex(16), secrets.token_hex(24)
+ os.environ.update(HEAL_ACCESS_KEY=access, HEAL_SECRET_KEY=secret)
+ candidate = json.loads(args.candidate.read_text())
+ fleet.record("inspector-build", verify_inspector(args.tool_directory, candidate))
+ binding = {"schemaVersion": 1, "runId": run_id, "jumpHostOnly": True, "exclusiveHealService": True,
+ "leaseExpiresAt": (datetime.now(timezone.utc) + timedelta(minutes=45)).isoformat(),
+ "bucketPrefix": bucket, "endpoints": {f"node{index}": f"http://rustfs-node{index}:9000" for index in range(1, len(nodes) + 1)},
+ "defaultEndpoint": "node1", "candidateCommit": candidate["source_sha"], "harnessCommit": args.testing_commit,
+ "candidatePackageSha256": candidate["package_sha256"], "candidateBinarySha256": args.binary_sha,
+ "rcBinarySha256": hashlib.sha256(args.rc_bin.read_bytes()).hexdigest(),
+ "maxInventoryObjects": count, "maxInventoryBytes": count * SIZE}
+ fleet.record("binding", binding)
+ fleet.record("pre-registered-budget", {"fixture": "DX", "objects": count, "objectBytes": SIZE,
+ "logicalBytes": count * SIZE, "taskSeconds": TASK_SECONDS, "sourceLimitReference": policy_reference,
+ "maxSnapshotFilesPerNode": resource_limits("cursor")["maxSnapshotFiles"], "snapshotSecondsPerNode": 600, "verificationBatchItems": 128,
+ "missingIndices": missing_indices, "taskScope": "frozen prefix scopes; one result per ACK",
+ "faultLeaseMinutes": 30, "serviceRuntimeSeconds": 2700, "topologyLeaseMinutes": 45})
+ recipe = select(args.case_id)[0]
+ require(recipe["variants"] == list(source_branches or branches) and set(branches) <= set(recipe["variants"]),
+ "DX executor omitted or invented a source branch")
+ result = {"topology": topology, "fixture": "DX", "verdict": "RUNNING", "branches": {}, "cleanup": "PENDING"}
+ active, probe = [], Probe(binding)
+ try:
+ fleet.deploy(args.binary_sha, bucket, access, secret)
+ fleet.record("topology-proof", validate_topology(fleet.snapshot("deployed-topology"), topology))
+ recorder = Recorder(probe, directory / "fixture")
+ expected = fixture_builder(recorder, bucket) if fixture_builder else seed(recorder, bucket, "DX", count, [SIZE])
+ wanted_pairs(expected, count=count)
+ baseline = fleet.record("s3-baseline", probe.inventory(bucket, unversioned_current_tags=True, page_size=127))
+ fleet.record("fixture-manifest", {"expected": expected, **verify_seed(expected, baseline)})
+ fleet.stop()
+ original = fleet.snapshot("physical-baseline", quiescent=True)
+ cohorts = map_cohorts(original, bucket, expected, count=count)
+ fleet.record("all-external-baseline", verify_all_parts(fleet, cohorts, "baseline-part-batch", count=count))
+ targets = [cohorts[index] for index in missing_indices]
+ if target_nodes is not None:
+ require(target_nodes and len(set(target_nodes)) == len(target_nodes) and set(target_nodes) <= set(nodes),
+ "DX fault placement escaped its actual bound fleet")
+ selected = []
+ for target in targets:
+ shard = next((entry for entry in target["cohort"] if entry["node"] in target_nodes
+ and entry["role"] == target["role"]), None)
+ require(shard is not None, "DX fault has no requested role outside the coordinator")
+ selected.append({**target, **shard})
+ targets = selected
+ fleet.record("explicit-fault-placement", {"nodes": target_nodes,
+ "targets": [{name: target[name] for name in ("node", "disk", "key", "role", "index")} for target in targets]})
+ receipts = []
+ for index, target in enumerate(targets):
+ params = {"disk": target["disk"], "relativePath": target["relativePath"], "kind": "missing",
+ "faultId": bounded_fault_id(recipe["code"], "dx-window", index), "expectedSha256": target["sha256"]}
+ fleet.faults.append((target["node"], params))
+ receipts.append(fleet.record("fault", {"node": target["node"], **fleet.remote(target["node"], "apply-fault", **params)}))
+ faulted = fleet.snapshot("physical-fault", quiescent=True)
+ fleet.record("fault-proof", validate_missing_batch(original, faulted, targets, receipts))
+ fleet.start()
+ require(physical_content(faulted) == physical_content(fleet.snapshot("physical-before-trigger")),
+ "ATTRIBUTION_NOT_OBSERVED: DX fault changed before the admitted manual task")
+ result["branches"] = observer(probe, fleet, binding, bucket, expected, targets, active, original=original)
+ require(set(result["branches"]) == set(branches), "DX observer omitted or invented a source branch")
+ fleet.stop()
+ repaired = fleet.snapshot("physical-repaired-before-get", quiescent=True)
+ fleet.record("physical-repair-proof", validate_repaired_physical(original, repaired, targets[0], repair_targets=targets))
+ fleet.record("all-external-repair-proof", verify_all_parts(fleet, cohorts, "repaired-part-batch", count=count))
+ fleet.start()
+ for endpoint in binding["endpoints"]:
+ inventory = fleet.record("s3-complete-after", {"endpoint": endpoint, **Probe(binding, endpoint).inventory(
+ bucket, unversioned_current_tags=True, page_size=127)})
+ fleet.record("independent-full-manifest-proof", {"endpoint": endpoint,
+ **verify_seed(expected, inventory), **compare_inventories(baseline, inventory)})
+ fleet.stop()
+ require(physical_content(repaired) == physical_content(fleet.snapshot("physical-after-get", quiescent=True)),
+ "final GET changed physical files; prior repair attribution was incomplete")
+ result["completePhysicalAndS3ManifestsVerified"] = True
+ if "terminal-truncation" in result["branches"]:
+ result["branches"]["terminal-truncation"]["completePhysicalAndS3ManifestsVerified"] = True
+ result["verdict"] = "PASS" if all(value["verdict"] == "PASS" or name == "cross-node-cursor"
+ and topology == "H1" and value["verdict"] == "NOT_APPLICABLE" for name, value in result["branches"].items()) else "FAIL"
+ except (Exception, KeyboardInterrupt) as exc:
+ result.update(verdict="FAIL", failure={"type": type(exc).__name__, "message": str(exc)})
+ try:
+ fleet.stop()
+ fleet.snapshot("physical-failure", quiescent=True)
+ except Exception as evidence_error:
+ result["failureEvidenceError"] = str(evidence_error)
+ finally:
+ for path, token in active:
+ try:
+ fleet.record("stop-unresolved", probe.admin(path, [("clientToken", token), ("forceStop", "true")], b""))
+ except Exception as exc:
+ fleet.record("stop-error", {"error": str(exc)})
+ result["cleanup"] = "PASS" if fleet.cleanup() else "FAIL"
+ fleet.record("topology-result", result)
+ return result
+
+
+def main(*, case_id=CASE_ID, observer=observe_task, branches=BRANCHES, runner=run_topology):
+ def interrupted(signum, frame):
+ raise KeyboardInterrupt(f"Heal workflow interrupted by signal {signum}")
+ signal.signal(signal.SIGTERM, interrupted)
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--candidate", type=Path, required=True)
+ parser.add_argument("--case-id", choices=(case_id,), required=True)
+ parser.add_argument("--testing-commit", required=True)
+ parser.add_argument("--binary-sha", required=True)
+ parser.add_argument("--rc-bin", type=Path, required=True)
+ parser.add_argument("--evidence", type=Path, required=True)
+ parser.add_argument("--tool-directory", type=Path, required=True)
+ args = parser.parse_args()
+ nodes = os.environ.get("RUSTFS_NODES", "vm000 vm001 vm002 vm003").split()
+ require(len(nodes) >= 4 and len(set(nodes)) == len(nodes)
+ and all(re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", node) for node in nodes), "four current Heal nodes required")
+ require(bool(re.fullmatch(r"[0-9a-f]{64}", args.binary_sha)), "invalid candidate binary digest")
+ require(bool(re.fullmatch(r"[1-9][0-9]*", os.environ.get("GITHUB_RUN_ID", ""))), "workflow fleet identity required")
+ results = []
+ try:
+ for topology, selected in (("H1", nodes[:1]), ("H2", nodes[:4])):
+ results.append(runner(args, topology, selected, observer=observer, branches=branches))
+ if results[-1]["cleanup"] != "PASS":
+ break
+ finally:
+ complete = len(results) == 2 and all(item["verdict"] == "PASS" and item["cleanup"] == "PASS" for item in results)
+ save(args.evidence / "healthy-summary.json", {"caseId": args.case_id, "results": results,
+ "cleanup": "PASS" if results and all(item["cleanup"] == "PASS" for item in results) else "FAIL", "complete": complete})
+ require(complete, f"{args.case_id} requires every applicable H1/H2 branch; an unobserved running window is not PASS")
+
+
+if __name__ == "__main__":
+ try:
+ main()
+ except Exception as exc:
+ print(f"Heal cursor validation failed: {exc}", file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_heal/disk_loss.py b/scripts/validation_heal/disk_loss.py
new file mode 100644
index 00000000..7430a7a7
--- /dev/null
+++ b/scripts/validation_heal/disk_loss.py
@@ -0,0 +1,272 @@
+"""B01: original member return and physical repair of degraded write ACKs."""
+
+import copy
+import hashlib
+import time
+import uuid
+
+try:
+ from .heal_probe import Probe, canonical, compare_inventories, payload, require
+ from .heal_workload import verify_seed
+ from .healthy import physical_content
+ from .shards import files, map_target, run_task
+ from .stale_rejoin import logical_metadata, identities, return_original_member
+except ImportError:
+ from heal_probe import Probe, canonical, compare_inventories, payload, require
+ from heal_workload import verify_seed
+ from healthy import physical_content
+ from shards import files, map_target, run_task
+ from stale_rejoin import logical_metadata, identities, return_original_member
+
+
+BRANCHES = ("original-drive-return", "offline-ack-repair")
+SIZE = 5 * 1024**2 + 123
+
+
+def quorum_plan(target, contract):
+ require(contract.get("recognized") is True
+ and contract.get("writeQuorumRule") == "data-plus-one-if-equal-parity",
+ "disk loss requires the recognized candidate quorum policy")
+ geometry = target["logical"]["erasure"]
+ read, parity = geometry["data_blocks"], geometry["parity_blocks"]
+ total, write = read + parity, read + int(read == parity)
+ require(total == len(target["cohort"]) and total in {4, 16} and total - 1 >= max(read, write),
+ "one member loss cannot admit the actual layout's degraded read/write ACKs")
+ return {"totalDrives": total, "readQuorum": read, "writeQuorum": write,
+ "onlineSameSetDrives": total - 1, "diskUUID": target["diskUUID"],
+ "setSlot": target["setSlot"], "node": target["node"], "disk": target["disk"],
+ "lockAdmission": "each write's actual ACK; no disk-count lock inference"}
+
+
+def info_disks(observed, total):
+ require(observed.get("httpStatus") == 200 and isinstance(observed.get("response"), dict),
+ "member-state Admin response is unavailable")
+ servers = observed["response"].get("info", {}).get("servers")
+ require(isinstance(servers, list) and servers, "Admin info omitted its actual server members")
+ disks = [disk for server in servers for disk in server.get("drives", [])]
+ require(len(disks) == total, "Admin info omitted or duplicated a set member")
+ indexed = {}
+ for disk in disks:
+ slot = tuple(disk.get(name) for name in ("pool_index", "set_index", "disk_index"))
+ require(all(type(value) is int for value in slot) and slot[:2] == (0, 0)
+ and 0 <= slot[2] < total and slot not in indexed
+ and isinstance(disk.get("endpoint"), str) and disk["endpoint"],
+ "Admin info lacks distinct exact endpoint/pool/set/disk identities")
+ indexed[slot] = disk
+ return indexed
+
+
+def member_state(observed, target, total, baseline=None):
+ indexed = info_disks(observed, total)
+ slot = (0, 0, target["setSlot"])
+ selected = indexed[slot]
+ expected_uuids = {(0, 0, shard["setSlot"]): shard["diskUUID"] for shard in target["cohort"]}
+ if baseline is None:
+ require(all(disk.get("state") == "ok" and disk.get("runtimeState") in {None, "online"}
+ and isinstance(disk.get("uuid"), str) and disk["uuid"] for disk in indexed.values()),
+ "baseline Admin members were not positively online")
+ require(selected["uuid"] == target["diskUUID"], "Admin slot differs from the original physical disk UUID")
+ require(all(indexed[member_slot]["uuid"] == expected_uuid for member_slot, expected_uuid in expected_uuids.items()),
+ "Admin topology differs from the actual formatted full-set membership")
+ return {"endpoint": selected["endpoint"], "slot": list(slot), "uuid": selected["uuid"]}
+ require(selected["endpoint"] == baseline["endpoint"] and selected.get("uuid") in {"", baseline["uuid"]},
+ "offline Admin witness changed the original member's endpoint/UUID")
+ for other_slot, disk in indexed.items():
+ if other_slot == slot: continue
+ require(disk.get("state") == "ok" and disk.get("runtimeState") in {None, "online"}
+ and disk.get("uuid") == expected_uuids[other_slot],
+ "another Admin member is unavailable; single-drive admission is unproven")
+ # The pinned candidate counts these explicit disk errors as offline. An
+ # unknown/initializing server or aggregate count never witnesses this disk.
+ require(selected.get("state") in {"disk not found", "drive access denied", "disk access denied", "file access denied",
+ "volume access denied", "offline"} or selected.get("runtimeState") == "offline",
+ "NOT_OBSERVED: exact gated member is not explicitly offline in Admin info")
+ return {**baseline, "reportedUUID": selected.get("uuid"), "state": selected.get("state"),
+ "runtimeState": selected.get("runtimeState"), "exactOriginalMemberOffline": True}
+
+
+def offline_ack(recorder, fleet, bucket, branch, expected):
+ require(branch in BRANCHES, "unknown disk loss branch")
+ manifest, new = copy.deepcopy(expected), []
+ # Each branch admits new keys while the original member is genuinely
+ # inaccessible; neither overwrites controls nor reuses another branch ACK.
+ for index in range(1 if branch == "original-drive-return" else 6):
+ key = f"disk-loss/{branch}/new-{index:02d}.bin"
+ require(not any(entry["key"] == key for entry in manifest), "offline key already has a fixture authority")
+ body = payload(f"{bucket}/{key}/0", SIZE + index)
+ response = recorder.call("put_object", {"Bucket": bucket, "Key": key,
+ "ContentType": "application/octet-stream", "Metadata": {"heal-fixture": "DB", "branch": branch},
+ "Tagging": "suite=heal&fixture=DB"}, body)
+ require(response.get("VersionId") in {None, "null"}, "degraded unique PUT created a foreign version")
+ entry = {"key": key, "versionId": "null", "deleteMarker": False, "isLatest": True,
+ "payload": {"size": len(body), "sha256": hashlib.sha256(body).hexdigest()}}
+ manifest.append(entry); new.append(entry)
+ # GET every new ACK plus the complete original manifest below. The gate
+ # remains active and its final witness brackets all operations.
+ observed = fleet.record("disk-loss-degraded-get", recorder.probe.s3("get_object", {"Bucket": bucket, "Key": key}))
+ require(observed.get("acknowledged") is True and observed.get("payload") == entry["payload"],
+ "degraded GET differs from the exact unique-key ACK")
+ return manifest, new
+
+
+def authority_witness(before, authority, bucket, target, new):
+ left, right = files(before), files(authority)
+ member = target["node"], target["disk"]
+ require({node: value["formats"] for node, value in before.items()} ==
+ {node: value["formats"] for node, value in authority.items()}, "offline ACKs changed original disk UUID/slot")
+ require(all(right.get(identity) == entry for identity, entry in left.items()),
+ "offline ACKs changed an existing original object/shard/control")
+ require({identity: entry for identity, entry in left.items() if identity[:2] == member} ==
+ {identity: entry for identity, entry in right.items() if identity[:2] == member},
+ "offline member received writes during degraded ACKs")
+ additions = [identity for identity in right if identity not in left]
+ prefixes = [f"{bucket}/{entry['key']}/" for entry in new]
+ require(new and len({entry["key"] for entry in new}) == len(new)
+ and all(identity[:2] != member and any(identity[2].startswith(prefix) for prefix in prefixes)
+ for identity in additions), "degraded ACKs wrote outside their exact new-key responsibility")
+ frozen = {}
+ for entry in new:
+ logical = None
+ for shard in target["cohort"]:
+ peer = shard["node"], shard["disk"]
+ if peer == member: continue
+ path = f"{bucket}/{entry['key']}/xl.meta"
+ metadata = right.get((*peer, path), {}).get("metadata")
+ require(metadata and metadata.get("bucket") == bucket and metadata.get("key") == entry["key"]
+ and identities(metadata) == {("null", False, True)} and len(metadata["versions"]) == 1,
+ "online member lacks the exact unique-key ACK authority")
+ normalized = logical_metadata(metadata)
+ require(logical is None or canonical(logical) == canonical(normalized),
+ "degraded new-key metadata lacks one full online authority")
+ logical = normalized
+ version = metadata["versions"][0]
+ require(version.get("inline") is None and version.get("size") == entry["payload"]["size"]
+ and version.get("dataDir") and version.get("usesLegacyChecksum") is False,
+ "degraded new-key ACK lacks a protected external part")
+ part = right.get((*peer, f"{bucket}/{entry['key']}/{version['dataDir']}/part.1"))
+ require(part and part["size"] > 32, "online ACK authority omitted its actual payload shard")
+ frozen[entry["key"]] = logical
+ return {"logicalAuthorities": frozen, "originalMemberUnchangedDuringAllACKs": True,
+ "originalControlsAndFormatsUnchanged": True, "onlineAuthorities": len(target["cohort"]) - 1}
+
+
+def repaired_physical(authority, after, bucket, target, new, witness):
+ left, right = files(authority), files(after)
+ member = target["node"], target["disk"]
+ require({node: value["formats"] for node, value in authority.items()} ==
+ {node: value["formats"] for node, value in after.items()}, "return changed original disk UUID/slot")
+ metadata_paths = {f"{bucket}/{entry['key']}/xl.meta": entry["key"] for entry in new}
+ for identity, entry in left.items():
+ if identity[2] in metadata_paths:
+ metadata = right.get(identity, {}).get("metadata")
+ require(metadata and canonical(logical_metadata(metadata)) == canonical(
+ witness["logicalAuthorities"][metadata_paths[identity[2]]]), "repair changed online ACK metadata authority")
+ else:
+ require(right.get(identity) == entry, "repair changed an original control or surviving payload shard")
+ cohorts, allowed = [], set()
+ for entry in new:
+ cohort = map_target(after, bucket, entry["key"], "data", 1)
+ require(canonical(logical_metadata(cohort["metadata"])) == canonical(witness["logicalAuthorities"][entry["key"]]),
+ "rejoined key differs from its frozen degraded ACK authority")
+ shard = next(item for item in cohort["cohort"] if (item["node"], item["disk"]) == member)
+ part = (*member, shard["relativePath"])
+ metadata = (*member, f"{bucket}/{entry['key']}/xl.meta")
+ require(part not in left and metadata not in left and part in right and metadata in right,
+ "NOT_OBSERVED: original member lacks newly reconstructed ACK metadata/payload")
+ require(shard["diskUUID"] == target["diskUUID"] and shard["setSlot"] == target["setSlot"],
+ "repair used a replacement disk or different member slot")
+ allowed.update((part, metadata)); cohorts.append(cohort)
+ protected = cohort["logical"]["parts"][0].get("integrityDescriptorPresent") is True
+ sidecars = [identity for identity in right if identity[:2] == member
+ and identity[2].startswith(part[2] + ".integrity.")]
+ require(len(sidecars) == int(protected), "rejoined protected part has missing or ambiguous integrity sidecars")
+ for identity in sidecars:
+ generation = identity[2].removeprefix(part[2] + ".integrity.")
+ require(str(uuid.UUID(generation)) == generation and uuid.UUID(generation).int != 0
+ and 0 < right[identity]["size"] <= 65536,
+ "rejoined integrity sidecar has an invalid generation or size")
+ allowed.add(identity)
+ require(set(right) - set(left) == allowed, "repair created unexpected or missing files outside the original member's ACKs")
+ return cohorts, {"sameDiskUUIDAndSetSlot": True, "allDegradedACKTargetPartsCreated": True,
+ "originalAndOnlineShardBytesUnchanged": True, "backupRestorationUsed": False,
+ "newShardOracle": "frozen protected online metadata, actual streaming checksums, independent ACK plaintext"}
+
+
+def run_branch(recorder, fleet, bucket, branch, expected, active, admissions, contract):
+ try:
+ from .versions import validate_version_receipts, verify_version_s3
+ except ImportError:
+ from versions import validate_version_receipts, verify_version_s3
+ before = fleet.snapshot("disk-loss-original-" + branch, quiescent=True)
+ target = map_target(before, bucket, "objects/0008.bin", "data", 1)
+ target = {**target, **next(shard for shard in reversed(target["cohort"]) if shard["node"] == fleet.nodes[-1])}
+ plan = fleet.record("disk-loss-quorum-plan", quorum_plan(target, contract))
+ fleet.start()
+ baseline = member_state(fleet.record("disk-loss-admin-baseline", recorder.probe.admin(
+ "/rustfs/admin/v3/info", [], b"", method="GET")), target, plan["totalDrives"])
+ fleet.stop()
+ node = target["node"]
+ fleet.drive_gates.add(node)
+ gate = fleet.record("disk-loss-gate-armed", fleet.remote(node, "drive-gate", disk=target["disk"], mode="permission-denied"))
+ fleet.start()
+ def witness_offline(label):
+ denial = fleet.record("disk-loss-service-read-refusal-" + label, fleet.remote(node, "drive-gate-probe"))
+ require(denial.get("readProbe", {}).get("errno") == 13 and denial.get("errno") == 13
+ and denial.get("dacBypassRemoved") is True, "member gate lacks actual same-service read/write refusal")
+ deadline = time.monotonic() + 90
+ while True:
+ observed = fleet.record("disk-loss-admin-offline-" + label, recorder.probe.admin(
+ "/rustfs/admin/v3/info", [], b"", method="GET"))
+ try:
+ proof = member_state(observed, target, plan["totalDrives"], baseline)
+ break
+ except ValueError as exc:
+ if not str(exc).startswith("NOT_OBSERVED:"): raise
+ require(time.monotonic() < deadline, str(exc))
+ time.sleep(2)
+ require(gate["dataPath"] == denial["dataPath"], "service denial moved to a different logical root")
+ return fleet.record("disk-loss-double-witness-" + label, {"service": denial, "admin": proof})
+ first = witness_offline("before-acks")
+ manifest, new = offline_ack(recorder, fleet, bucket, branch, expected)
+ baseline_s3 = fleet.record("disk-loss-degraded-full-inventory", recorder.probe.inventory(bucket, unversioned_current_tags=True))
+ fleet.record("disk-loss-degraded-full-proof", {"expected": manifest, **verify_seed(manifest, baseline_s3)})
+ last = witness_offline("after-acks")
+ # A restart with the original disk still inaccessible leaves its unclean
+ # marker intact. Returning it through startup can repair ACKs before the
+ # manual token; reuse the original-process rejoin used by stale versions.
+ authority = fleet.snapshot("disk-loss-online-authorities")
+ require(physical_content(authority) == physical_content(fleet.snapshot("disk-loss-online-authorities-stable")),
+ "NOT_OBSERVED: offline ACK authority is not physically stable before live rejoin")
+ witness = fleet.record("disk-loss-ack-authority", authority_witness(before, authority, bucket, target, new))
+ return_original_member(fleet, node, "disk-loss")
+ require(physical_content(authority) == physical_content(fleet.snapshot("disk-loss-before-manual")),
+ "ATTRIBUTION_NOT_OBSERVED: original member gained ACK shards before the manual token")
+ receipts = []
+ for entry in new:
+ status = run_task(recorder.probe, fleet, {"bucket": bucket, "key": entry["key"]}, 2, active,
+ option_overrides={"recursive": True}, admissions=admissions)
+ receipts.append(validate_version_receipts(status, bucket, [entry], 2,
+ required_repairs=[(entry["key"], "null")]))
+ fleet.stop()
+ after = fleet.snapshot("disk-loss-repaired", quiescent=True)
+ cohorts, physical = repaired_physical(authority, after, bucket, target, new, witness)
+ streaming = []
+ for cohort in cohorts:
+ for shard in cohort["cohort"]:
+ proof = fleet.record("disk-loss-protected-streaming-check", {"node": shard["node"],
+ **fleet.remote(shard["node"], "verify-part", disk=shard["disk"], relativePath=shard["relativePath"],
+ versionId="null", partNumber=1)})
+ require(proof.get("physicalBitrotVerified") is True and proof.get("sha256") == shard["sha256"]
+ and proof.get("physicalSize") == shard["size"], "rejoined shard lacks a matching actual protected checksum")
+ streaming.append(proof)
+ fleet.start()
+ s3 = verify_version_s3(recorder.probe, fleet, bucket, manifest, baseline_s3, "disk-loss-full-s3-after", fixture="DB")
+ peers = []
+ for endpoint in recorder.probe.binding["endpoints"]:
+ inventory = fleet.record("disk-loss-all-peer-s3", {"endpoint": endpoint,
+ **Probe(recorder.probe.binding, endpoint).inventory(bucket, unversioned_current_tags=True)})
+ peers.append({"endpoint": endpoint, **verify_seed(manifest, inventory),
+ "fullPublicManifest": compare_inventories(baseline_s3, inventory)})
+ return manifest, baseline_s3, {"verdict": "PASS", "quorums": plan, "beforeACKs": first, "afterACKs": last,
+ "degradedUniqueACKs": len(new), "responsibility": receipts, "physical": physical,
+ "streaming": streaming, "s3": s3, "allPeerS3": peers}
diff --git a/scripts/validation_heal/drive_gate.py b/scripts/validation_heal/drive_gate.py
new file mode 100644
index 00000000..c013787a
--- /dev/null
+++ b/scripts/validation_heal/drive_gate.py
@@ -0,0 +1,269 @@
+"""B02 gates in run-owned units; the host's data mounts are never remounted."""
+
+import errno
+import json
+import os
+from pathlib import Path
+import re
+import stat
+import subprocess
+
+
+DENY_DAC_CAPS = (1 << 1) | (1 << 2) # CAP_DAC_OVERRIDE, CAP_DAC_READ_SEARCH
+PROBE_SOURCE = '''import errno,json,os
+path = __import__('sys').argv[1]
+result = {"uid": os.geteuid(), "gid": os.getegid()}
+try:
+ fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
+except OSError as exc:
+ result["errno"] = exc.errno
+else:
+ os.close(fd)
+ os.unlink(path)
+ result["errno"] = 0
+print(json.dumps(result))
+'''
+READ_PROBE_SOURCE = '''import json,os
+result = {"uid": os.geteuid(), "gid": os.getegid()}
+try:
+ fd = os.open(__import__('sys').argv[1], os.O_RDONLY | os.O_NOFOLLOW)
+except OSError as exc:
+ result["errno"] = exc.errno
+else:
+ os.close(fd)
+ result["errno"] = 0
+print(json.dumps(result))
+'''
+
+
+def require(value, message):
+ if not value: raise ValueError(message)
+
+
+def gate_folder(state, roots, disk, scope):
+ folder = roots[disk] / "data"
+ if scope == "disk": return folder
+ require(scope in {"bucket", "durable-bucket"}, "invalid drive gate scope")
+ marker = state / "owner.json"
+ info = marker.lstat()
+ require(stat.S_ISREG(info.st_mode) and info.st_uid == 0 and info.st_nlink == 1
+ and info.st_size < 16384 and info.st_mode & 0o022 == 0, "invalid bucket gate owner marker")
+ owner = json.loads(marker.read_text())
+ expected_profile = "queue-pressure" if scope == "durable-bucket" else "mrf-crash"
+ require(owner.get("runtimeProfile") == expected_profile
+ and isinstance(owner.get("bucket"), str) and re.fullmatch(r"rv-heal-[0-9a-f]{16}", owner["bucket"]),
+ "bucket gate requires the dedicated owned MRF fixture")
+ info = folder.lstat()
+ require(stat.S_ISDIR(info.st_mode) and info.st_uid == 0 and stat.S_IMODE(info.st_mode) == 0o700,
+ "bucket gate private data root changed")
+ return folder / (owner["bucket"] + ("-durable" if scope == "durable-bucket" else ""))
+
+
+def checked_gate(state, roots):
+ path = state / "drive-gate.json"
+ if not path.exists(): return None
+ info = path.lstat()
+ require(stat.S_ISREG(info.st_mode) and info.st_uid == 0 and info.st_nlink == 1
+ and info.st_size < 4096 and info.st_mode & 0o077 == 0, "invalid drive gate ownership")
+ value = json.loads(path.read_text())
+ require(value.get("schemaVersion") == 1 and value.get("mode") in {"read-only", "permission-denied"}
+ and value.get("phase", "active") in {"armed", "active"}
+ and type(value.get("disk")) is int and 0 <= value["disk"] < len(roots)
+ and type(value.get("originalMode")) is int,
+ "drive gate no longer matches the run-owned data root")
+ scope = value.get("scope", "disk")
+ require(value.get("dataPath") == str(gate_folder(state, roots, value["disk"], scope))
+ and (value["originalMode"] == 0o700 if scope == "disk" else
+ value["mode"] == "permission-denied" and value["originalMode"] in {0o700, 0o755, 0o777}),
+ "drive gate no longer matches its exact owned scope or original permissions")
+ return value
+
+
+def apply_gate(state, roots, disk, mode, *, scope="disk"):
+ require(type(disk) is int and 0 <= disk < len(roots) and mode in {"read-only", "permission-denied"}, "invalid drive gate")
+ require(checked_gate(state, roots) is None, "a drive gate is already active")
+ folder = gate_folder(state, roots, disk, scope)
+ info = folder.lstat()
+ original_mode = stat.S_IMODE(info.st_mode)
+ require(stat.S_ISDIR(info.st_mode) and info.st_uid == 0
+ and (original_mode == 0o700 if scope == "disk" else
+ mode == "permission-denied" and original_mode in {0o700, 0o755, 0o777}),
+ "expected owned gate directory with supported original permissions")
+ if scope in {"bucket", "durable-bucket"}:
+ objects = (folder / "objects").lstat()
+ probe = (folder / "objects/0000.bin").lstat()
+ read_target = (folder / "objects/0000.bin/xl.meta").lstat()
+ require(stat.S_ISDIR(objects.st_mode) and objects.st_uid == 0
+ and stat.S_ISDIR(probe.st_mode) and probe.st_uid == 0
+ and stat.S_ISREG(read_target.st_mode) and read_target.st_uid == 0 and read_target.st_nlink == 1,
+ "bucket gate needs the original non-linked fixture metadata")
+ value = {"schemaVersion": 1, "disk": disk, "mode": mode, "dataPath": str(folder),
+ "originalMode": original_mode, "device": info.st_dev, "inode": info.st_ino,
+ "phase": "armed" if mode == "permission-denied" else "active"}
+ if scope != "disk": value["scope"] = scope
+ # Intent precedes chmod/start. Cleanup can restore a partially applied gate.
+ fd = os.open(state / "drive-gate.json", os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
+ with os.fdopen(fd, "w") as stream:
+ json.dump(value, stream)
+ stream.flush()
+ os.fsync(stream.fileno())
+ return {**value, "state": "ARMED", "hostMountRemounted": False, "sharedPermissionsChanged": False}
+
+
+def activate_gate(state, roots):
+ value = checked_gate(state, roots)
+ require(value and value["mode"] == "permission-denied" and value.get("phase") == "armed",
+ "permission activation requires a newly armed run-owned gate")
+ folder = Path(value["dataPath"])
+ info = folder.lstat()
+ require(stat.S_ISDIR(info.st_mode) and info.st_uid == 0 and info.st_dev == value["device"]
+ and info.st_ino == value["inode"] and stat.S_IMODE(info.st_mode) == value["originalMode"],
+ "armed permission root changed before live injection")
+ # Keep the recovery intent before chmod. A partially activated gate remains
+ # recoverable from the armed marker and original inode/mode.
+ folder.chmod(0)
+ temporary = state / "drive-gate-active.json"
+ fd = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
+ value["phase"] = "active"
+ with os.fdopen(fd, "w") as stream:
+ json.dump(value, stream)
+ stream.flush()
+ os.fsync(stream.fileno())
+ os.replace(temporary, state / "drive-gate.json")
+ return {**value, "state": "ACTIVE", "injectedAfterActualServiceStart": True,
+ "hostMountRemounted": False, "sharedPermissionsChanged": False}
+
+
+def release_gate(state, roots):
+ value = checked_gate(state, roots)
+ if value is None: return {"state": "CLEAR", "gateWasAbsent": True}
+ folder = Path(value["dataPath"])
+ info = folder.lstat()
+ require(stat.S_ISDIR(info.st_mode) and info.st_uid == 0 and info.st_dev == value["device"] and info.st_ino == value["inode"],
+ "gated data root changed; preserve namespace")
+ if value["mode"] == "permission-denied":
+ require(stat.S_IMODE(info.st_mode) in {0, value["originalMode"]}, "gated permissions drifted")
+ folder.chmod(value["originalMode"])
+ require(stat.S_IMODE(folder.stat().st_mode) == value["originalMode"], "data root permissions were not restored")
+ (state / "drive-gate.json").unlink()
+ return {"state": "CLEAR", "originalPermissionsRestored": True, "hostMountRemounted": False}
+
+
+def start_properties(value):
+ if value is None: return []
+ properties = ["--property=PrivateMounts=true"]
+ if value["mode"] == "read-only": properties.append("--property=ReadOnlyPaths=" + value["dataPath"])
+ else: properties.append("--property=CapabilityBoundingSet=~CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH")
+ return properties
+
+
+def validate_live_boundary(pid):
+ require(type(pid) is int and pid > 1, "live drive gate requires a real owned process")
+ root = Path(f"/proc/{pid}")
+ require((root / "ns/mnt").readlink() != Path("/proc/self/ns/mnt").readlink(),
+ "live drive gate requires the preconfigured private mount namespace")
+ fields = dict(line.split(":", 1) for line in (root / "status").read_text().splitlines() if ":" in line)
+ require(all(name in fields and int(fields[name].strip(), 16) & DENY_DAC_CAPS == 0
+ for name in ("CapEff", "CapPrm", "CapBnd")),
+ "live drive gate requires DAC bypass removed from effective, permitted and bounding capabilities")
+ return {"privateMountNamespace": True, "dacBypassRemoved": True}
+
+
+def process_identity(pid):
+ text = Path(f"/proc/{pid}/status").read_text()
+ fields = dict(line.split(":", 1) for line in text.splitlines() if ":" in line)
+ uid, gid = list(map(int, fields["Uid"].split())), list(map(int, fields["Gid"].split()))
+ return {"uid": uid[1], "gid": gid[1], "capEff": int(fields["CapEff"].strip(), 16)}
+
+
+def validate_probe(observed, mode, identity):
+ expected = errno.EROFS if mode == "read-only" else errno.EACCES
+ require(identity["uid"] == observed.get("uid") == 0 and identity["gid"] == observed.get("gid") == 0,
+ "write probe does not share the service identity")
+ require(observed.get("errno") == expected, "drive gate did not cause its precise Linux write refusal")
+ if mode == "permission-denied":
+ require(identity["capEff"] & DENY_DAC_CAPS == 0, "root service still bypasses filesystem permissions")
+ return {"errno": expected, "name": errno.errorcode[expected], "sameServiceIdentity": True,
+ "dacBypassRemoved": mode == "permission-denied"}
+
+
+def probe_gate(state, roots, pid):
+ value = checked_gate(state, roots)
+ require(value is not None and type(pid) is int and pid > 0, "active gated service required")
+ require(value.get("phase", "active") == "active", "permission gate is still armed; no refusal claim")
+ identity = process_identity(pid)
+ require(Path(f"/proc/{pid}/ns/mnt").readlink() != Path("/proc/self/ns/mnt").readlink(),
+ "gate requires an independent service mount namespace")
+ path = value["dataPath"] + "/.validation-write-probe"
+ args = ["nsenter", "--mount=/proc/" + str(pid) + "/ns/mnt"]
+ if value["mode"] == "permission-denied": args += ["setpriv", "--bounding-set=-dac_override,-dac_read_search"]
+ args += ["python3", "-c", PROBE_SOURCE, path]
+ result = subprocess.run(args, capture_output=True, text=True, timeout=15)
+ require(result.returncode == 0, "service-namespace write probe did not complete")
+ observed = json.loads(result.stdout)
+ read = None
+ if value["mode"] == "permission-denied":
+ # A denied write alone cannot prove a stale member cannot read.
+ relative = "objects/0000.bin/xl.meta" if value.get("scope") in {"bucket", "durable-bucket"} else ".rustfs.sys/format.json"
+ read_args = [*args[:-4], "python3", "-c", READ_PROBE_SOURCE,
+ value["dataPath"] + "/" + relative]
+ result = subprocess.run(read_args, capture_output=True, text=True, timeout=15)
+ require(result.returncode == 0, "service-namespace read probe did not complete")
+ read = json.loads(result.stdout)
+ validate_probe(read, value["mode"], identity)
+ control = None
+ if value.get("scope") in {"bucket", "durable-bucket"}:
+ system = roots[value["disk"]] / "data/.rustfs.sys"
+ info = system.lstat()
+ require(stat.S_ISDIR(info.st_mode) and info.st_uid == 0, "MRF control root is not an owned non-linked directory")
+ control = {}
+ for label, source, path in (("write", PROBE_SOURCE, system / ".validation-mrf-write-probe"),
+ ("read", READ_PROBE_SOURCE, system / "format.json")):
+ result = subprocess.run([*args[:-4], "python3", "-c", source, str(path)],
+ capture_output=True, text=True, timeout=15)
+ require(result.returncode == 0, "service-namespace MRF control probe did not complete")
+ witness = json.loads(result.stdout)
+ require(type(witness.get("errno")) is int and witness["errno"] == 0
+ and witness.get("uid") == identity["uid"] == 0 and witness.get("gid") == identity["gid"] == 0,
+ "bucket fault also blocked system metadata or changed the MRF probe identity")
+ control[label] = witness
+ control["sameServiceIdentity"] = True
+ return {**validate_probe(observed, value["mode"], identity), "disk": value["disk"], "scope": value.get("scope", "disk"),
+ "dataPath": value["dataPath"], "readProbe": read,
+ "systemMetadataControl": control, "systemMetadataAccessible": control is not None,
+ "privateMountNamespace": True, "hostMountRemounted": False, "observed": observed}
+
+
+def validate_unwritable(status, bucket, entry):
+ outcome = status.get("outcome") or {}
+ objects = outcome.get("objects", [])
+ terminal = status.get("summary") == "finished" or status.get("summary") == "stopped" \
+ and outcome.get("execution", {}).get("state") == "completed_with_errors"
+ require(terminal and status.get("truncated", False) is False
+ and outcome.get("coverage") == "complete" and outcome.get("objectsTruncated") is False
+ and len(objects) == 1, "unwritable task did not preserve complete exact responsibility")
+ receipt = objects[0]
+ identity = receipt.get("identity", {})
+ public_version = identity.get("versionId") or "null"
+ if public_version == "00000000-0000-0000-0000-000000000000": public_version = "null"
+ require(identity.get("bucket") == bucket and identity.get("object") == entry["key"]
+ and public_version == entry["versionId"] and identity.get("bucketIncarnationId")
+ and type(identity.get("poolIndex")) is int and identity["poolIndex"] == 0
+ and type(identity.get("setIndex")) is int and identity["setIndex"] == 0,
+ "unwritable target receipt has a wrong or incomplete identity")
+ disposition = receipt.get("disposition", {})
+ state, detail = disposition.get("state"), disposition.get("details")
+ require(state == "failed" and detail in {"recoverable", "retry_exhausted", "permanent"}
+ or state == "deferred" and isinstance(detail, dict)
+ and detail.get("reason") in {"deadline", "transient_existence_check"},
+ "unwritable target was claimed repaired/healthy/absent or has an unrelated pending reason")
+ failed = state == "failed"
+ require(outcome.get("execution", {}).get("state") == ("completed_with_errors" if failed else "completed"),
+ "unwritable execution hides the unresolved result")
+ wanted = {"processed": 1, "healed": 0, "unchanged": 0, "failed": int(failed),
+ "skipped": int(not failed), "unknown": 0, "overflowed": False}
+ counters = outcome.get("counters", {})
+ require(all(type(counters.get(name)) is type(value) and counters[name] == value for name, value in wanted.items())
+ and type(counters.get("attemptFailures")) is int and counters["attemptFailures"] >= int(failed),
+ "unwritable responsibility counters claim success or hide a failure")
+ return {"disposition": disposition, "repairClaimedWhileUnwritable": False, "completeResponsibility": True}
diff --git a/scripts/validation_heal/encrypted.py b/scripts/validation_heal/encrypted.py
new file mode 100644
index 00000000..600e953e
--- /dev/null
+++ b/scripts/validation_heal/encrypted.py
@@ -0,0 +1,350 @@
+#!/usr/bin/env python3
+"""D08: same-role ciphertext reconstruction and independently decrypted ACKs."""
+
+from __future__ import annotations
+
+import argparse
+import base64
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import secrets
+import signal
+import sys
+
+try:
+ from .heal_probe import Probe, compare_inventories, payload, require, save
+ from .heal_workload import Recorder, verify_seed
+ from .healthy import Fleet, physical_content, validate_topology
+ from .inspector import verify as verify_inspector
+ from .matrix import select
+ from .shards import _map_external_target, files, run_task, validate_repaired_physical, validate_fault, verify_cohort
+ from .versions import validate_version_receipts
+ from .unformatted import binding
+ from .authorization import iam_exchange
+ from .schema import exact_get
+except ImportError:
+ from heal_probe import Probe, compare_inventories, payload, require, save
+ from heal_workload import Recorder, verify_seed
+ from healthy import Fleet, physical_content, validate_topology
+ from inspector import verify as verify_inspector
+ from matrix import select
+ from shards import _map_external_target, files, run_task, validate_repaired_physical, validate_fault, verify_cohort
+ from versions import validate_version_receipts
+ from unformatted import binding
+ from authorization import iam_exchange
+ from schema import exact_get
+
+
+CASE_ID = "heal.encrypted-objects"
+BRANCHES = ("sse-s3", "sse-kms", "sse-c", "encrypted-versioned")
+COUNT = 16
+SIZE = 5 * 1024**2 + 123
+CANDIDATE = "072b57b37559fbe626aeda8abf61617fdcbdd926"
+REVIEWED_CANDIDATES = {
+ CANDIDATE: {},
+ # This source keeps the original storage/KMS geometry and fixes the
+ # customer-key PUT/HEAD/GET response projection without changing the oracle.
+ "16cf19817e0c81e24d0d787a916f734a34517ec5": {
+ "rustfs/src/app/object/put.rs": "01256c2995f04845840d28c8434cdfaca072766f88693a7d8cb8d24ed78463c7",
+ "rustfs/src/app/object/head.rs": "d3ed1e0dcb548dbcf80769fb6ea8e37912e76a59c85ae75628baea36dab542f7",
+ "rustfs/src/app/object/get.rs": "e2ce8101de7e50b4941bb6ecca36440f439cc3212d2be50f79380d212d2ed704",
+ },
+}
+REFERENCE = {"candidateCommit": CANDIDATE, "dependency": "fresh run-owned static AES-256 KMS key on every candidate node",
+ "sourceFiles": {
+ "crates/kms/src/config.rs": "7931bb4c6757e60ca3e7fdbff4ac56c5fab59d6d6df8756c9d0287ca692a26f9",
+ "crates/kms/src/backends/static_kms.rs": "a5cf1cbe4251f79221d171c58b3844b6aa7f073a4173da0783a6d9208c5b580a",
+ "rustfs/src/storage/sse.rs": "37ecffdb4dd18a0528014afd30237c8772c3fb7f27fe2c4cc018e6d475a29641",
+ "crates/e2e_test/src/kms/kms_authorization_negative_matrix_test.rs": "d64724dbec366fb54eabcfa80cd1b11448a11174f12c7c801107d37b10825643"},
+ "ciphertextPolicy": "single uncompressed encrypted part; decoded ciphertext size and original plaintext actualSize are distinct",
+ "negativeKeyPolicy": "SSE-C missing/wrong customer key; SSE-KMS absent/other-key decrypt grant; explicit per-key enforcement",
+ "managedS3ReadKey": "service-managed; S3 caller does not supply a decryption key"}
+SSEC = {"SSECustomerAlgorithm": "AES256", "SSECustomerKey": "$HEAL_SSE_C_KEY"}
+
+
+def mode_for(branch):
+ require(branch in BRANCHES, "unrecognized D08 source branch")
+ return "sse-kms" if branch == "encrypted-versioned" else branch
+
+
+def put_options(branch, key_id):
+ mode = mode_for(branch)
+ if mode == "sse-c": return dict(SSEC)
+ if mode == "sse-s3": return {"ServerSideEncryption": "AES256"}
+ return {"ServerSideEncryption": "aws:kms", "SSEKMSKeyId": key_id}
+
+
+def encryption_headers(response, branch, key_id):
+ mode = mode_for(branch)
+ if mode == "sse-c":
+ key_md5 = base64.b64encode(hashlib.md5(os.environ["HEAL_SSE_C_KEY"].encode(), usedforsecurity=False).digest()).decode()
+ require(response.get("SSECustomerAlgorithm") == "AES256" and response.get("SSECustomerKeyMD5") == key_md5
+ and not response.get("ServerSideEncryption"), "SSE-C ACK/GET did not preserve its actual encryption classification")
+ else:
+ require(response.get("ServerSideEncryption") == ("AES256" if mode == "sse-s3" else "aws:kms"),
+ "encrypted ACK/GET silently downgraded its server encryption mode")
+ if mode == "sse-kms": require(response.get("SSEKMSKeyId") == key_id, "KMS object used another key identity")
+ return {"encrypted": True, "mode": mode, "keyMaterialLogged": False}
+
+
+def cipher_part_policy(expected):
+ plaintext = expected["payload"]["size"]
+ require(type(plaintext) is int and plaintext > 1024**2 and not expected["deleteMarker"], "D08 requires its full external plaintext ACK")
+ def checked(version, part):
+ require(len(version.get("parts", [])) == 1 and part.get("number") == 1
+ and type(part.get("size")) is int and type(part.get("actualSize")) is int
+ and part["actualSize"] == plaintext and plaintext < part["size"] <= plaintext + 1024**2
+ and version.get("size") == part["size"] and version.get("inline") is None,
+ "ciphertext geometry is not the reviewed uncompressed single-part encoding of this plaintext ACK")
+ return checked
+
+
+def map_cipher_target(snapshot, bucket, expected, role):
+ target = _map_external_target(snapshot, bucket, expected["key"], role, 1, expected["versionId"],
+ latest_required=expected["isLatest"], part_policy=cipher_part_policy(expected))
+ return {**target, "encryptedPhysicalLayer": True, "plaintextACK": expected["payload"],
+ "ciphertextPartSize": target["logical"]["parts"][0]["size"]}
+
+
+def denied_key(observed, *, customer):
+ expected = (400, "InvalidRequest") if customer else (403, "AccessDenied")
+ response = observed.get("response") or {}
+ require(observed.get("acknowledged") is False and observed.get("resultUnknown") is False and "payload" not in observed
+ and response.get("ResponseMetadata", {}).get("HTTPStatusCode") == expected[0]
+ and response.get("Error", {}).get("Code") == expected[1],
+ "missing/wrong decryption authority returned data, absence or an unreviewed error")
+ return {"httpStatus": expected[0], "code": expected[1], "missingOrWrongKeyIsNotObjectAbsence": True}
+
+
+def seed_encrypted(recorder, bucket, branch, key_id):
+ recorder.call("create_bucket", {"Bucket": bucket})
+ versioned = branch == "encrypted-versioned"
+ if versioned:
+ recorder.call("put_bucket_versioning", {"Bucket": bucket, "VersioningConfiguration": {"Status": "Enabled"}})
+ require(recorder.call("get_bucket_versioning", {"Bucket": bucket}).get("Status") == "Enabled", "encrypted versioning was not enabled")
+ expected, identities = [], set()
+ for index in range(COUNT):
+ key = f"encrypted/{index:04d}.bin"
+ for generation in range(3 if versioned else 1):
+ body = payload(f"{bucket}/{key}/{generation}", SIZE + generation * 37)
+ observed = recorder.call("put_object", {"Bucket": bucket, "Key": key,
+ "ContentType": "application/octet-stream", "Metadata": {"heal-fixture": "DE", "generation": str(generation)},
+ "Tagging": "suite=heal&fixture=DE", **put_options(branch, key_id)}, body)
+ encryption_headers(observed, branch, key_id)
+ version = observed.get("VersionId", "null")
+ require((versioned and isinstance(version, str) and version not in {"", "null"})
+ or not versioned and version in {None, "null"}, "encrypted PUT omitted its exact expected version identity")
+ version = version or "null"
+ require((key, version) not in identities, "encrypted ACK reused a logical version")
+ identities.add((key, version))
+ expected.append({"key": key, "versionId": version, "isLatest": not versioned or generation == 2,
+ "deleteMarker": False, "payload": {"size": len(body), "sha256": hashlib.sha256(body).hexdigest()}})
+ return expected
+
+
+def principal_setup(probe, fleet, bucket, key_id, context):
+ suffix = hashlib.sha256(probe.binding["runId"].encode()).hexdigest()[:14]
+ for index, grant in enumerate((None, key_id + "-other")):
+ user, policy = "rvheal" + suffix + f"encrypted{index}", "rvheal" + suffix + f"keypolicy{index}"
+ secret = secrets.token_hex(24)
+ fleet.secret_values.update({user, secret})
+ item = {"user": user, "policy": policy, "attached": False, "policyCreated": False,
+ "principal": Probe(probe.binding, credential_pair=(user, secret)), "grant": grant}
+ context.append(item)
+ iam_exchange(probe, fleet, "add-user", user, secret=secret)
+ statements = [{"Effect": "Allow", "Action": ["s3:GetObject", "s3:GetObjectVersion"],
+ "Resource": [f"arn:aws:s3:::{bucket}/*", f"arn:aws:s3:::{bucket}-control/*"]}]
+ if grant:
+ statements.append({"Effect": "Allow", "Action": ["kms:Decrypt"], "Resource": ["arn:aws:kms:::key/" + grant]})
+ item["policyCreated"] = True
+ iam_exchange(probe, fleet, "add-policy", user, policy=policy, document={"Version": "2012-10-17", "Statement": statements})
+ item["attached"] = True
+ iam_exchange(probe, fleet, "attach", user, policy=policy)
+
+
+def key_negatives(probe, fleet, bucket, branch, entry, control, context, label):
+ params = {"Bucket": bucket, "Key": entry["key"], "VersionId": entry["versionId"]}
+ proofs = []
+ if mode_for(branch) == "sse-c":
+ for name, options in (("missing", {}), ("wrong", {**SSEC, "SSECustomerKey": "$HEAL_SSE_C_WRONG_KEY"})):
+ observed = fleet.record(label + "-customer-key-" + name, probe.s3("get_object", {**params, **options}))
+ proofs.append({"variant": name, **denied_key(observed, customer=True)})
+ elif mode_for(branch) == "sse-kms":
+ for item in context:
+ principal = item["principal"]
+ readable = fleet.record(label + "-same-principal-plaintext-control", principal.s3("get_object", {
+ "Bucket": bucket + "-control", "Key": control["key"]}))
+ exact_get(readable, control)
+ observed = fleet.record(label + "-KMS-grant-denied", principal.s3("get_object", params))
+ proofs.append({"variant": "missing-grant" if item["grant"] is None else "other-key-grant", **denied_key(observed, customer=False),
+ "samePrincipalS3ReadAllowed": True})
+ else:
+ # SSE-S3 does not take a customer key on GET. Test its positive public
+ # classification; inventing a missing client key would be false coverage.
+ proofs.append({"variant": "service-managed-key", "callerKeyArgumentApplicable": False})
+ return proofs
+
+
+def run_branch(args, topology, nodes, branch):
+ directory = args.evidence / f"{topology}-{branch}"
+ directory.mkdir()
+ run_id = f"gh-{os.environ['GITHUB_RUN_ID']}-{os.environ.get('GITHUB_RUN_ATTEMPT', '1')}-{topology.lower()}"
+ fleet = Fleet(nodes, directory, run_id, args.tool_directory)
+ candidate = json.loads(args.candidate.read_text())
+ require(candidate["source_sha"] in REVIEWED_CANDIDATES, "unreviewed encrypted geometry/KMS policy contract")
+ fleet.record("inspector-build", verify_inspector(args.tool_directory, candidate))
+ fleet.record("encryption-contract", {**REFERENCE, "candidateCommit": candidate["source_sha"],
+ "reviewedResponseSourceFiles": REVIEWED_CANDIDATES[candidate["source_sha"]]})
+ bucket = "rv-heal-" + hashlib.sha256(run_id.encode()).hexdigest()[:16]
+ key_id, material = "rv-heal-key-" + run_id, base64.b64encode(secrets.token_bytes(32)).decode()
+ access, secret = secrets.token_hex(16), secrets.token_hex(24)
+ customer, wrong = secrets.token_hex(16), secrets.token_hex(16)
+ require(customer != wrong, "negative customer key duplicated the valid key")
+ fleet.secret_values.update({material, key_id + ":" + material, customer, wrong,
+ base64.b64encode(customer.encode()).decode(), base64.b64encode(wrong.encode()).decode()})
+ os.environ.update(HEAL_ACCESS_KEY=access, HEAL_SECRET_KEY=secret, HEAL_SSE_C_KEY=customer, HEAL_SSE_C_WRONG_KEY=wrong)
+ lab = binding(args, candidate, nodes, run_id, bucket)
+ fleet.record("binding", lab)
+ probe, active, context = Probe(lab), [], []
+ fleet.snapshot_buckets = [bucket, bucket + "-control"]
+ result = {"topology": topology, "branch": branch, "verdict": "RUNNING", "cleanup": "PENDING"}
+ iam_dirty = False
+ try:
+ def configure():
+ for node in nodes:
+ fleet.record("dedicated-KMS-configured", {"node": node, **fleet.remote(node, "encryption-configure", keyId=key_id, keyBase64=material)})
+ fleet.deploy(args.binary_sha, bucket, access, secret, before_start=configure)
+ recorder = Recorder(probe, directory / "fixture")
+ expected = seed_encrypted(recorder, bucket, branch, key_id)
+ read_options = SSEC if mode_for(branch) == "sse-c" else {}
+ baseline = fleet.record("decrypted-S3-baseline", probe.inventory(bucket,
+ unversioned_current_tags=branch != "encrypted-versioned", object_read_options=read_options))
+ fleet.record("independent-plaintext-ACKs", {"expected": expected, **verify_seed(expected, baseline)})
+ for entry in baseline["entries"]: encryption_headers(entry["properties"], branch, key_id)
+ control_bucket = bucket + "-control"
+ recorder.call("create_bucket", {"Bucket": control_bucket})
+ control_body = payload(control_bucket + "/unaffected", SIZE)
+ recorder.call("put_object", {"Bucket": control_bucket, "Key": "unaffected.bin"}, control_body)
+ control = {"key": "unaffected.bin", "versionId": "null", "deleteMarker": False, "isLatest": True,
+ "payload": {"size": len(control_body), "sha256": hashlib.sha256(control_body).hexdigest()}}
+ control_baseline = fleet.record("plaintext-control-baseline", probe.inventory(control_bucket, unversioned_current_tags=True))
+ verify_seed([control], control_baseline)
+ require(all(not any(name in entry["properties"] for name in
+ ("ServerSideEncryption", "SSEKMSKeyId", "SSECustomerAlgorithm")) for entry in control_baseline["entries"]),
+ "key-authority control is not an independently readable plaintext object")
+ if mode_for(branch) == "sse-kms": principal_setup(probe, fleet, bucket, key_id, context)
+ selected = [next(entry for entry in expected if entry["key"] == key and (entry["isLatest"] if index else True))
+ for index, key in enumerate(("encrypted/0002.bin", "encrypted/0004.bin"))]
+ result["baselineKeyNegatives"] = key_negatives(probe, fleet, bucket, branch, selected[0], control, context, "baseline")
+ # Each peer must already serve the correct decrypted version before a
+ # shard fault. This proves actual key propagation and service readiness.
+ for endpoint in lab["endpoints"]:
+ observed = fleet.record("all-peer-dependency-readiness", Probe(lab, endpoint).s3("get_object", {
+ "Bucket": bucket, "Key": selected[0]["key"], "VersionId": selected[0]["versionId"], **read_options}))
+ exact_get(observed, selected[0]); encryption_headers(observed["response"], branch, key_id)
+ fleet.stop()
+ before = fleet.snapshot("original-ciphertext-full-physical", quiescent=True)
+ fleet.record("actual-topology", validate_topology(before, topology))
+ require(all(before[node]["encryptionConfigAttestation"]["runOwnedSecretFileUnchanged"] for node in nodes), "dedicated KMS identity changed")
+ targets = [map_cipher_target(before, bucket, entry, "data" if index == 0 else "parity") for index, entry in enumerate(selected)]
+ for target in targets: fleet.record("original-ciphertext-streaming", verify_cohort(fleet, target, "original-ciphertext-part"))
+ prior = before
+ registrations = []
+ for index, target in enumerate(targets):
+ params = {"disk": target["disk"], "faultId": f"D08-{branch}-{index}"}
+ registrations.append((target["node"], params)); fleet.faults.append(registrations[-1])
+ receipt = fleet.record("ciphertext-fault", fleet.remote(target["node"], "apply-fault", **params,
+ relativePath=target["relativePath"], kind="missing" if index == 0 else "bitflip", expectedSha256=target["sha256"],
+ offset=None if index == 0 else target["size"] // 2))
+ changed = fleet.snapshot("witnessed-ciphertext-fault", quiescent=True)
+ validate_fault(prior, changed, target, receipt); prior = changed
+ fleet.start()
+ require(physical_content(prior) == physical_content(fleet.snapshot("ciphertext-before-manual-token")),
+ "ciphertext repaired before its attributed manual token")
+ status = run_task(probe, fleet, {"bucket": bucket, "key": ""}, 2, active, option_overrides={"recursive": True},
+ status_endpoints=tuple(lab["endpoints"])[1:])
+ result["responsibility"] = validate_version_receipts(status, bucket, expected, 2,
+ required_repairs={(entry["key"], entry["versionId"]) for entry in selected})
+ fleet.stop()
+ repaired = fleet.snapshot("ciphertext-repaired-before-plaintext-GET", quiescent=True)
+ result["physical"] = validate_repaired_physical(before, repaired, targets[0], repair_targets=targets)
+ for target in targets: fleet.record("reconstructed-ciphertext-streaming", verify_cohort(fleet, target, "repaired-ciphertext-part"))
+ for registration in registrations: fleet.faults.remove(registration)
+ fleet.start()
+ result["postRepairKeyNegatives"] = key_negatives(probe, fleet, bucket, branch, selected[0], control, context, "post-repair")
+ for endpoint in lab["endpoints"]:
+ entry_probe = Probe(lab, endpoint)
+ inventory = fleet.record("all-peer-full-decrypted-manifest", {"endpoint": endpoint, **entry_probe.inventory(bucket,
+ unversioned_current_tags=branch != "encrypted-versioned", object_read_options=read_options)})
+ fleet.record("all-peer-plaintext-and-properties", {**verify_seed(expected, inventory), **compare_inventories(baseline, inventory)})
+ for entry in inventory["entries"]: encryption_headers(entry["properties"], branch, key_id)
+ controls = fleet.record("all-peer-plaintext-controls", entry_probe.inventory(control_bucket, unversioned_current_tags=True))
+ compare_inventories(control_baseline, controls)
+ fleet.stop()
+ require(physical_content(repaired) == physical_content(fleet.snapshot("ciphertext-after-decryption-and-negatives", quiescent=True)),
+ "decryption verification changed or downgraded the proven ciphertext repair")
+ result.update(verdict="PASS", allPlaintextACKsAndVersionsPreserved=True, sameRoleCiphertextRestored=True,
+ keyMaterialLogged=False, sharedKmsTouched=False, backupRestorationUsed=False)
+ except (Exception, KeyboardInterrupt) as exc:
+ result.update(verdict="FAIL", failure={"type": type(exc).__name__, "message": str(exc)})
+ if fleet.reserved:
+ try: fleet.stop(); fleet.snapshot("D08-failure-ciphertext-scene", quiescent=True)
+ except Exception as capture: result["failureEvidenceError"] = str(capture)
+ finally:
+ for path, token in active:
+ try: fleet.record("stop-unresolved", probe.admin(path, [("clientToken", token), ("forceStop", "true")], b""))
+ except Exception as exc: fleet.record("stop-error", {"error": str(exc)})
+ if context:
+ try: fleet.stop(); fleet.start()
+ except Exception as exc: iam_dirty = True; fleet.record("IAM-recovery-start-failed", {"error": str(exc)})
+ if not iam_dirty:
+ for item in context:
+ for operation, needed in (("detach", item["attached"]), ("remove-user", True), ("remove-policy", item["policyCreated"])):
+ if not needed: continue
+ try: iam_exchange(probe, fleet, operation, item["user"], policy=item["policy"] if operation != "remove-user" else None)
+ except Exception as exc: iam_dirty = True; fleet.record("IAM-recovery-DIRTY", {"operation": operation, "error": str(exc)})
+ if iam_dirty:
+ fleet.stop()
+ fleet.record("retained-key-authority-environment", {"state": "DIRTY", "namespacesRetained": True})
+ result["cleanup"] = "PASS" if not iam_dirty and fleet.cleanup() else "FAIL"
+ fleet.record("branch-result", result)
+ return result
+
+
+def main():
+ def interrupted(signum, frame): raise KeyboardInterrupt(f"Heal workflow interrupted by signal {signum}")
+ signal.signal(signal.SIGTERM, interrupted)
+ parser = argparse.ArgumentParser(description=__doc__)
+ for name in ("candidate", "rc-bin", "evidence", "tool-directory"): parser.add_argument("--" + name, type=Path, required=True)
+ parser.add_argument("--case-id", choices=(CASE_ID,), required=True)
+ parser.add_argument("--testing-commit", required=True)
+ parser.add_argument("--binary-sha", required=True)
+ args = parser.parse_args()
+ nodes = os.environ.get("RUSTFS_NODES", "vm000 vm001 vm002 vm003").split()
+ require(len(nodes) >= 4 and len(set(nodes)) == len(nodes)
+ and all(re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", node) for node in nodes), "four current Heal nodes required")
+ require(bool(re.fullmatch(r"[0-9a-f]{64}", args.binary_sha))
+ and bool(re.fullmatch(r"[1-9][0-9]*", os.environ.get("GITHUB_RUN_ID", ""))), "verified workflow candidate required")
+ require(select(CASE_ID)[0]["variants"] == list(BRANCHES), "D08 executor omitted a canonical encryption branch")
+ results = []
+ try:
+ for topology, selected in (("H1", nodes[:1]), ("H2", nodes[:4])):
+ for branch in BRANCHES:
+ results.append(run_branch(args, topology, selected, branch))
+ if results[-1]["cleanup"] != "PASS": break
+ if results[-1]["cleanup"] != "PASS": break
+ finally:
+ complete = len(results) == 8 and all(item["verdict"] == "PASS" and item["cleanup"] == "PASS" for item in results)
+ save(args.evidence / "healthy-summary.json", {"caseId": CASE_ID, "results": results,
+ "cleanup": "PASS" if results and all(item["cleanup"] == "PASS" for item in results) else "FAIL", "complete": complete})
+ require(complete, "D08 requires all four encryption branches on H1/H2 with exact ciphertext/plaintext/key-negative proofs")
+
+
+if __name__ == "__main__":
+ try: main()
+ except Exception as exc:
+ print(f"Heal encrypted object validation failed: {exc}", file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_heal/encryption_runtime.py b/scripts/validation_heal/encryption_runtime.py
new file mode 100644
index 00000000..fa685976
--- /dev/null
+++ b/scripts/validation_heal/encryption_runtime.py
@@ -0,0 +1,68 @@
+"""D08 static KMS material only inside the stopped run-owned service namespace."""
+
+import base64
+import hashlib
+import json
+import os
+import re
+import stat
+
+
+def settings_for(state, key_id):
+ return {"RUSTFS_KMS_ENABLE": "true", "RUSTFS_KMS_BACKEND": "static", "RUSTFS_KMS_DEFAULT_KEY_ID": key_id,
+ "RUSTFS_KMS_ENFORCE_SSE_KEY_POLICY": "true", "RUSTFS_KMS_STATIC_SECRET_KEY_FILE": str(state / "static-kms-secret")}
+
+
+def require(value, message):
+ if not value: raise ValueError(message)
+
+
+def configure(state, owner, request, fault):
+ key_id = request.get("keyId")
+ material = request.get("keyBase64")
+ require(key_id == "rv-heal-key-" + owner["runId"] and isinstance(material, str), "encryption key is outside its exact run")
+ decoded = base64.b64decode(material, validate=True)
+ require(len(decoded) == 32 and len(set(decoded)) > 1, "fresh AES-256 material required")
+ value = {"schemaVersion": 1, "runId": owner["runId"], "backend": "static", "keyId": key_id,
+ "secretPath": str(state / "static-kms-secret"), "keyMaterialLogged": False, "sharedKmsTouched": False}
+ with fault.directory(state) as fd:
+ original = fault.read_file(fd, "service.env", 65536)
+ require(not re.search(rb"(?m)^RUSTFS_KMS_", original), "run already has an encryption dependency configuration")
+ secret = (key_id + ":" + material + "\n").encode()
+ fault.new_file(fd, "static-kms-secret", secret)
+ value["secretSha256"] = hashlib.sha256(secret).hexdigest()
+ settings = settings_for(state, key_id)
+ updated = original + "".join(f"{name}={item}\n" for name, item in settings.items()).encode()
+ value.update(settings=settings, serviceEnvSha256=hashlib.sha256(updated).hexdigest())
+ fault.new_file(fd, "encryption-intent.json", fault.encode(value))
+ fault.new_file(fd, "service-encryption.env", updated)
+ os.replace("service-encryption.env", "service.env", src_dir_fd=fd, dst_dir_fd=fd)
+ os.fsync(fd)
+ fault.new_file(fd, "encryption-config.json", fault.encode(value))
+ return {name: value[name] for name in ("schemaVersion", "runId", "backend", "keyId", "keyMaterialLogged", "sharedKmsTouched")}
+
+
+def attest(state, owner, checked, digest, environment=None):
+ marker = checked(state / "encryption-config.json", state)
+ intent = checked(state / "encryption-intent.json", state)
+ if not marker.exists():
+ require(not intent.exists(), "partial encryption configuration remains unresolved")
+ return None
+ require(marker.stat().st_size < 8192, "encryption attestation exceeds its bound")
+ value = json.loads(marker.read_text())
+ secret = checked(state / "static-kms-secret", state)
+ require(stat.S_ISREG(secret.stat().st_mode) and stat.S_IMODE(secret.stat().st_mode) == 0o600
+ and secret.stat().st_nlink == 1 and secret.stat().st_uid == marker.stat().st_uid,
+ "dedicated key material no longer has its private file ownership")
+ require(value.get("schemaVersion") == 1 and value.get("runId") == owner["runId"]
+ and value.get("backend") == "static" and value.get("keyId") == "rv-heal-key-" + owner["runId"]
+ and value.get("secretPath") == str(state / "static-kms-secret")
+ and value.get("settings") == settings_for(state, value["keyId"])
+ and digest(checked(state / "static-kms-secret", state)) == value["secretSha256"]
+ and digest(checked(state / "service.env", state)) == value["serviceEnvSha256"],
+ "run-owned KMS configuration or key material changed")
+ if environment is not None:
+ require(all(environment.get(name) == item for name, item in value["settings"].items()),
+ "candidate process did not load the exact dedicated KMS configuration")
+ return {"backend": "static", "keyId": value["keyId"], "runOwnedSecretFileUnchanged": True,
+ "loadedByCandidate": environment is not None, "sharedKmsTouched": False, "keyMaterialLogged": False}
diff --git a/scripts/validation_heal/environment.py b/scripts/validation_heal/environment.py
new file mode 100644
index 00000000..bb9081ab
--- /dev/null
+++ b/scripts/validation_heal/environment.py
@@ -0,0 +1,107 @@
+#!/usr/bin/env python3
+"""Read the current Heal fleet's capabilities; never execute or score a case."""
+
+import argparse
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import subprocess
+import sys
+
+try:
+ from .heal_probe import require, save
+ from .healthy import Fleet
+except ImportError:
+ from heal_probe import require, save
+ from healthy import Fleet
+
+
+def eligible_mounts(value):
+ rows = value.get('mounts')
+ require(value.get('inspectionOnly') is True and value.get('mutated') is False
+ and value.get('canonicalPassClaimed') is False and isinstance(rows, list) and len(rows) == 8,
+ 'incomplete or mutating environment inspection')
+ require({row.get('target') for row in rows} == {f'/data/rustfs{number}' for number in range(1, 9)},
+ 'environment inspection duplicated or omitted a mount endpoint')
+ eligible = {}
+ for row in rows:
+ if not row['mounted']: continue
+ mount = row.get('mount') or {}
+ if (row.get('independentBlockDeviceVerified') is True and row.get('writableMount') is True
+ and mount.get('uuid') and mount.get('target') == row['target']
+ and mount.get('fstype') in {'xfs', 'ext4'} and row.get('availableBytes', 0) >= 2 * 1024**3):
+ eligible[row['target']] = row
+ # Two partitions from the same disk are one failure domain. Aliases or
+ # duplicated filesystem IDs cannot be counted as independent endpoints.
+ physical = [row['physicalDevice'] for row in eligible.values()]
+ identifiers = [row['mount']['uuid'] for row in eligible.values()]
+ require(len(physical) == len(set(physical)) and len(identifiers) == len(set(identifiers)),
+ 'environment inspection found aliased physical failure domains')
+ return eligible
+
+
+def summarize(observed):
+ require(len(observed) == 4 and len({value.get('hostname') for value in observed.values()}) == 4
+ and all(value.get('hostname') for value in observed.values()), 'inspection requires four actual distinct current Heal hosts')
+ mounts = {node: eligible_mounts(value) for node, value in observed.items()}
+ identifiers = [row['mount']['uuid'] for rows in mounts.values() for row in rows.values()]
+ require(len(identifiers) == len(set(identifiers)), 'environment inspection found shared or cloned filesystem identities')
+ nodes = list(observed)
+ available = lambda selected, count: all({f'/data/rustfs{number}' for number in range(1, count + 1)} <= set(mounts[node])
+ for node in selected)
+ return {'independentEligibleEndpoints': sum(len(rows) for rows in mounts.values()),
+ 'eligibleEndpointsPerNode': {node: sorted(rows) for node, rows in mounts.items()},
+ 'resourceShapes': {'H1': {'mountsAvailable': available(nodes[:1], 4), 'requiredEndpoints': 4},
+ 'H2': {'mountsAvailable': available(nodes, 4), 'requiredEndpoints': 16},
+ 'H3': {'mountsAvailable': available(nodes[:3], 4), 'requiredEndpoints': 12},
+ 'H4': {'mountsAvailable': available(nodes[:3], 8), 'requiredEndpoints': 24},
+ 'H6': {'platformRestartAuthorityVerified': False}},
+ 'runtimeObservationsComplete': all(value.get('runtime', {}).get('stableObservation') is True
+ for value in observed.values()),
+ 'cleanStateCertified': False, 'globalWorkflowIdleCertified': False,
+ 'actualErasureLayoutVerified': False, 'faultBackendVerified': False,
+ 'compatibleReleasePairVerified': False, 'canonicalPassClaimed': False}
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--candidate', type=Path, required=True)
+ parser.add_argument('--testing-commit', required=True)
+ parser.add_argument('--evidence', type=Path, required=True)
+ args = parser.parse_args()
+ root = Path(__file__).resolve().parents[2]
+ require(subprocess.check_output(['git', '-C', str(root), 'rev-parse', 'HEAD'], text=True).strip() == args.testing_commit
+ and re.fullmatch(r'[0-9a-f]{40}', args.testing_commit), 'environment inspection requires an exact harness checkout')
+ candidate = json.loads(args.candidate.read_text())
+ require(re.fullmatch(r'[0-9a-f]{40}', candidate['source_sha']) and re.fullmatch(r'[0-9a-f]{64}', candidate['package_sha256']),
+ 'environment inspection has an invalid candidate identity')
+ workflow = os.environ.get('GITHUB_RUN_ID', '')
+ attempt = os.environ.get('GITHUB_RUN_ATTEMPT', '1')
+ require(re.fullmatch(r'[1-9][0-9]*', workflow) and re.fullmatch(r'[1-9][0-9]*', attempt), 'environment inspection requires workflow provenance')
+ nodes = (os.environ.get('RUSTFS_NODES') or 'vm000 vm001 vm002 vm003').split()
+ require(len(nodes) >= 4 and len(set(nodes)) == len(nodes) and all(re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9_.-]*', node) for node in nodes),
+ 'invalid current Heal node binding')
+ user = os.environ.get('RUSTFS_SSH_USER') or 'azureuser'
+ require(re.fullmatch(r'[A-Za-z_][A-Za-z0-9_-]*', user), 'invalid current Heal SSH user')
+ os.environ['RUSTFS_SSH_USER'] = user
+ args.evidence.mkdir(parents=True, exist_ok=False)
+ fleet = Fleet(nodes[:4], args.evidence, f'gh-{workflow}-{attempt}-h2')
+ result = {'schemaVersion': 1, 'runId': workflow, 'runAttempt': attempt, 'testingCommit': args.testing_commit,
+ 'candidate': candidate, 'inspectionSourceSha256': hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
+ 'inspectionOnly': True, 'mutated': False, 'canonicalPassClaimed': False, 'complete': False}
+ try:
+ observed = {node: fleet.record('node-capabilities', {'node': node, **fleet.remote(node, 'environment-capabilities')})
+ for node in fleet.nodes}
+ result.update(nodes=observed, resources=summarize(observed), complete=True)
+ finally:
+ save(args.evidence / 'environment-summary.json', result)
+ print(json.dumps(result['resources'], sort_keys=True))
+
+
+if __name__ == '__main__':
+ try: main()
+ except Exception as exc:
+ print(f'environment inspection failed: {exc}', file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_heal/environment_cases.py b/scripts/validation_heal/environment_cases.py
new file mode 100644
index 00000000..d4351e60
--- /dev/null
+++ b/scripts/validation_heal/environment_cases.py
@@ -0,0 +1,765 @@
+#!/usr/bin/env python3
+"""Disabled-by-default extended Heal scenarios on assigned auto-testing resources.
+
+S3 fixtures, ACKs, Admin tasks, physical and data oracles are owned here. An
+assigned resource adapter supplies lifecycle operations that the ordinary Heal
+fleet cannot provide (power control, IO devices, extra pools and dependencies).
+It receives JSON on stdin and returns raw, run-bound observations. It cannot
+supply a PASS verdict. No release-validation checkout is used.
+"""
+from __future__ import annotations
+
+from concurrent.futures import ThreadPoolExecutor
+from dataclasses import dataclass
+from datetime import datetime, timezone
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import subprocess
+import tempfile
+import threading
+import time
+from urllib.parse import quote
+
+try:
+ from .execution_policy import ENVIRONMENT_GATED_CASES
+ from .heal_probe import Probe, canonical, compare_inventories, payload, redact, require, save, validate_binding
+ from .heal_workload import Recorder, seed, verify_seed
+ from .healthy import BASE_OPTIONS
+ from .matrix import select, variant_topologies
+ from .shards import files, validate_fault, validate_repaired_physical
+except ImportError:
+ from execution_policy import ENVIRONMENT_GATED_CASES
+ from heal_probe import Probe, canonical, compare_inventories, payload, redact, require, save, validate_binding
+ from heal_workload import Recorder, seed, verify_seed
+ from healthy import BASE_OPTIONS
+ from matrix import select, variant_topologies
+ from shards import files, validate_fault, validate_repaired_physical
+
+RUNTIME_ENV = "VALIDATION_HEAL_ENVIRONMENT_RUNTIME"
+SHA = re.compile(r"[0-9a-f]{64}\Z")
+SOAK_SECONDS = {"24h": 24 * 3600, "72h": 72 * 3600}
+NATIVE_BRANCHES = {"native-ilm-fence", "native-replication-fence"}
+
+
+class Blocked(ValueError):
+ pass
+
+
+class NotObserved(ValueError):
+ pass
+
+
+@dataclass(frozen=True)
+class Scenario:
+ code: str
+ family: str
+ # Actions run after actual Heal admission. Each has a raw evidence oracle.
+ actions: tuple[str, ...]
+ fault_kind: str = "missing"
+ role: str = "data"
+ interrupted: bool = False
+
+
+def scenario(code: str, branch: str) -> Scenario:
+ """Executable branches; strings are adapter operations, never shell text."""
+ if code == "F08":
+ actions = {
+ "rolling-old-new": ("upgrade-next", "wait-mixed", "upgrade-rest"),
+ "mixed-peer-status": ("upgrade-next", "mixed-status", "upgrade-rest"),
+ "rollback-new-old": ("upgrade-all", "rollback-next", "wait-mixed", "rollback-rest"),
+ "mixed-restart": ("upgrade-next", "restart-mixed", "mixed-status", "upgrade-rest"),
+ }[branch]
+ return Scenario(code, "upgrade", actions)
+ if code == "F09":
+ return Scenario(code, "host", ("replace-drive", "replacement-window", branch, "recover-host"), interrupted=True)
+ if code == "G07":
+ return Scenario(code, "ilm", ("ilm-" + branch, "ilm-window", "ilm-settle"))
+ if code == "G08":
+ return Scenario(code, "replication", (branch, "replication-window", "replication-settle"))
+ if code == "H01":
+ return Scenario(code, "pool", ("expand-pool", "rebalance-start", "pool-window", "pool-settle"),
+ role="parity" if "parity" in branch else "data")
+ if code == "H02":
+ actions = ["rebalance-start", "pool-window", "rebalance-stop"]
+ if branch == "rebalance-service-restart": actions.append("restart-service")
+ actions.extend(("rebalance-resume", "pool-window", "pool-settle"))
+ return Scenario(code, "pool", tuple(actions))
+ if code == "H03":
+ return Scenario(code, "pool", ("decommission-start", "pool-window", "decommission-write-guard", "pool-settle", "source-offline"))
+ if code == "H04":
+ return Scenario(code, "pool", ("decommission-start", "pool-window", "decommission-cancel", "wait-inflight-exit",
+ "decommission-clear", "decommission-resume", "capacity-lock-window"
+ if branch == "capacity-lock-retry-window" else "pool-window", "pool-settle"))
+ if code == "H05":
+ return Scenario(code, "retired-pool", ("survivor-window",),
+ fault_kind="metadata-missing" if branch == "survivor-missing-metadata" else "missing")
+ if code == "H07":
+ return Scenario(code, "ec84", ("replace-drive", "replacement-window",
+ "crash-service" if branch.endswith("crash") else "restart-service", "replacement-settle"), interrupted=True)
+ if code == "I01":
+ return Scenario(code, "io", ("io-" + branch, "io-window", "io-recover"))
+ if code == "I06":
+ return Scenario(code, "dependency", ("dependency-" + branch, "dependency-window", "dependency-recover"))
+ if code == "J05":
+ require(branch in SOAK_SECONDS, "unregistered soak duration")
+ return Scenario(code, "soak", ())
+ raise ValueError(f"unknown environment scenario: {code}/{branch}")
+
+
+def required_operations(case_id: str) -> set[str]:
+ item = select(case_id)[0]
+ common = {"preflight", "prepare", "stop", "start", "snapshot", "apply-fault", "recover", "cleanup", "verify-parts", "checkpoint", "metrics"}
+ for branch in item["variants"]:
+ if branch in NATIVE_BRANCHES: continue
+ spec = scenario(item["code"], branch)
+ common.update(spec.actions)
+ if spec.family == "retired-pool": common.add("retire-pool-prepare")
+ if spec.family in {"ilm", "replication", "dependency"}: common.add(spec.family + "-prepare")
+ if spec.family == "soak": common.update(("soak-fault", "soak-recover", "metrics"))
+ return common
+
+
+def runtime_binding(value: dict, case_id: str, candidate: dict, testing_commit: str) -> dict:
+ """Reject unassigned resources before the first command or mutation."""
+ require(value.get("schemaVersion") == 1 and value.get("provider") == "auto-testing", "auto-testing runtime manifest required")
+ binding = value["binding"]
+ validate_binding(binding)
+ require(binding["candidateCommit"] == candidate["source_sha"]
+ and binding["candidatePackageSha256"] == candidate["package_sha256"]
+ and binding["harnessCommit"] == testing_commit, "runtime is bound to a different candidate/checkout")
+ if candidate.get("binary_sha256"):
+ require(binding["candidateBinarySha256"] == candidate["binary_sha256"], "runtime binary differs from candidate")
+ rc = Path(value["rcBinary"])
+ require(rc.is_absolute() and rc.is_file() and not rc.is_symlink()
+ and hashlib.sha256(rc.read_bytes()).hexdigest() == binding["rcBinarySha256"], "runtime rc identity differs")
+ configured_nodes = os.environ.get("RUSTFS_NODES", "vm000 vm001 vm002 vm003").split()
+ require(value.get("sshNodes") == configured_nodes, "runtime must reuse the current auto-testing Heal fleet")
+ required = set(ENVIRONMENT_GATED_CASES[case_id]["requiredCapabilities"])
+ require(required <= set(value.get("capabilities", [])), "runtime lacks required environment capabilities")
+ selected = value.get("topologies", [])
+ wanted = set(select(case_id)[0]["contract"]["probes"]["topology"].split(","))
+ require(isinstance(selected, list) and len(set(selected)) == len(selected) and set(selected) == wanted,
+ "runtime must bind every required topology")
+ adapter = value.get("adapter", {})
+ executable = Path(adapter.get("executable", ""))
+ require(executable.is_absolute() and executable.is_file() and not executable.is_symlink()
+ and os.access(executable, os.X_OK) and hashlib.sha256(executable.read_bytes()).hexdigest() == adapter.get("sha256"),
+ "assigned resource adapter is absent, non-executable or changed")
+ require(required_operations(case_id) <= set(adapter.get("operations", [])), "resource adapter omits a required operation")
+ code = ENVIRONMENT_GATED_CASES[case_id]["code"]
+ require(isinstance(value.get("inspectorSha256"), str) and SHA.fullmatch(value["inspectorSha256"]),
+ "candidate-bound inspector digest required")
+ resource_fields = {"F09": ("hostResourceId",), "G07": ("kmsKeyId", "tierEndpoint"),
+ "G08": ("replicaEndpoint",), "I01": ("faultDeviceId",),
+ "I06": ("kmsKeyId", "tierEndpoint", "tenantId", "dependencyNamespaceId")}
+ require(all(isinstance(value.get(field), str) and value[field] for field in resource_fields.get(code, ())),
+ "runtime omits a required dedicated resource binding")
+ if code in {"G07", "G08", "I06"}:
+ field = "replicaEndpoint" if code == "G08" else "tierEndpoint"
+ require(value[field] in binding["endpoints"] and value[field] != binding["defaultEndpoint"],
+ "remote dependency/replica must bind a separate endpoint")
+ if code == "F08":
+ pair = value.get("releasePair", {})
+ releases = pair.get("releases", [])
+ require(pair.get("rollbackCompatible") is True and SHA.fullmatch(pair.get("compatibilityEvidenceSha256", ""))
+ and len(releases) == 2 and len({r.get("packageSha256") for r in releases}) == 2
+ and all(SHA.fullmatch(r.get("packageSha256", "")) and SHA.fullmatch(r.get("binarySha256", ""))
+ and re.fullmatch(r"[0-9a-f]{40}", r.get("sourceCommit", "")) and r.get("packageUrl") for r in releases)
+ and any(r["binarySha256"] == binding["candidateBinarySha256"] for r in releases),
+ "F08 requires two immutable compatible releases including the candidate")
+ if code == "J05":
+ expiry = datetime.fromisoformat(binding["leaseExpiresAt"].replace("Z", "+00:00"))
+ # Both source windows run sequentially; include final verification/recovery.
+ require((expiry - datetime.now(timezone.utc)).total_seconds() >= len(wanted) * sum(SOAK_SECONDS.values()) + 3600,
+ "soak lease cannot cover all actual 24h/72h windows and recovery")
+ limits = value.get("soakLimits", {})
+ require(all(type(limits.get(k)) is int and limits[k] >= 0 for k in
+ ("rssBytes", "openFds", "queuedTasks", "unresolvedRecords", "temporaryBytes")), "soak resource limits required")
+ return binding
+
+
+class Adapter:
+ """A resource operation protocol, similar to the existing Pool suite hooks."""
+ def __init__(self, runtime, evidence):
+ self.runtime, self.evidence = runtime, evidence
+ self.sequence = 0
+ self.reserved = False
+ self.calls = []
+
+ def record(self, label, value):
+ self.sequence += 1
+ save(self.evidence / f"{self.sequence:06d}-{label}.json", value)
+ return value
+
+ def call(self, operation, *, scope, **parameters):
+ binding = self.runtime["binding"]
+ if operation not in {"recover", "cleanup", "soak-recover"}:
+ validate_binding(binding)
+ require(operation in self.runtime["adapter"]["operations"], "unregistered resource operation")
+ executable = Path(self.runtime["adapter"]["executable"])
+ require(hashlib.sha256(executable.read_bytes()).hexdigest() == self.runtime["adapter"]["sha256"], "resource adapter changed during run")
+ request = {"schemaVersion": 1, "runId": binding["runId"], "sequence": len(self.calls) + 1,
+ "candidateCommit": binding["candidateCommit"], "scope": scope, "operation": operation,
+ "bucketPrefix": binding["bucketPrefix"], "leaseExpiresAt": binding["leaseExpiresAt"],
+ "parameters": parameters}
+ digest = hashlib.sha256(canonical(request).encode()).hexdigest()
+ self.calls.append(request)
+ self.record("request-" + operation, request)
+ if operation == "prepare": self.reserved = True # A lost ACK can leave partial setup.
+ # Files retain partial stdout/stderr even on timeout. Never emit credentials
+ # from adapter diagnostics to workflow stdout.
+ timeout = 600 if operation in {"prepare", "cleanup", "pool-settle", "replacement-settle"} else 180
+ def safe_text(raw):
+ text = redact(raw.decode(errors="replace"))
+ for name in self.runtime.get("redactionEnvironmentVariables", []):
+ secret = os.environ.get(name)
+ if secret: text = text.replace(secret, "[REDACTED]")
+ return text
+ # Raw adapter output lives outside the uploaded evidence directory.
+ # Preserve bounded, redacted diagnostics even when the child times out.
+ with tempfile.TemporaryFile() as out, tempfile.TemporaryFile() as err:
+ try:
+ result = subprocess.run([str(executable)], input=json.dumps(request).encode(), stdout=out, stderr=err, timeout=timeout)
+ finally:
+ out.seek(0); raw = out.read(16 * 1024**2 + 1)
+ err.seek(0); diagnostic = err.read(1024**2)
+ self.record("adapter-diagnostic", {"operation": operation, "stdout": safe_text(raw), "stderr": safe_text(diagnostic)})
+ require(result.returncode == 0, f"resource adapter {operation} failed; inspect retained diagnostics")
+ require(len(raw) <= 16 * 1024**2, "adapter observation exceeds budget")
+ response = json.loads(raw)
+ require(response.get("schemaVersion") == 1 and response.get("requestSha256") == digest and response.get("runId") == binding["runId"]
+ and response.get("sequence") == request["sequence"] and response.get("scope") == scope
+ and response.get("operation") == operation and isinstance(response.get("observation"), dict)
+ and "verdict" not in response["observation"], "resource observation is not bound to the original operation")
+ return self.record("response-" + operation, response)["observation"]
+
+
+def assert_topology(observed, topology, candidate_sha, *, allow_old=None):
+ disks = observed.get("disks", [])
+ expected_min = {"H1": 4, "H2": 16, "H3": 12, "H4": 24, "H5": 16, "H6": 4}[topology]
+ require(len(disks) >= expected_min, "insufficient independent endpoints")
+ identities = set()
+ slots = set()
+ sets = {}
+ for disk in disks:
+ require(all(type(disk.get(k)) is int and disk[k] >= 0 for k in ("poolIndex", "setIndex", "setSlot")),
+ "invalid physical pool/set/slot")
+ identity = (disk["hostname"], disk["mountDevice"])
+ slot = (disk["poolIndex"], disk["setIndex"], disk["setSlot"])
+ require(identity not in identities and slot not in slots and disk.get("diskUUID"), "directories alias a device or erasure slot")
+ identities.add(identity); slots.add(slot)
+ sets.setdefault(slot[:2], []).append(disk)
+ require(topology != "H5" or len({pool for pool, _ in sets}) >= 2, "H5 omitted separate pools")
+ if topology in {"H3", "H4"}:
+ require(len(sets) >= (2 if topology == "H4" else 1), "EC8+4 omitted required sets")
+ require(all(len(group) == 12 and all(d["dataShards"] == 8 and d["parityShards"] == 4 for d in group)
+ for group in sets.values()), "actual set is not EC8+4")
+ processes = observed.get("processes", [])
+ require(processes and {p["hostname"] for p in processes} == {d["hostname"] for d in disks}, "process attestation omitted hosts")
+ allowed = {candidate_sha} | ({allow_old} if allow_old else set())
+ require(all(p.get("binarySha256") in allowed and type(p.get("pid")) is int and p["pid"] > 0 for p in processes),
+ "service process is not a verified immutable candidate")
+ return sets
+
+
+def target_from_snapshot(snapshot, bucket, entry, role, *, set_index=None):
+ indexed = files(snapshot)
+ cohort = []
+ for node, observation in snapshot.items():
+ for form in observation["formats"]:
+ disk = form["disk"]
+ meta = indexed.get((node, disk, f"{bucket}/{entry['key']}/xl.meta"), {}).get("metadata", {})
+ versions = [v for v in meta.get("versions", []) if (v.get("versionId") or "null") == entry["versionId"]]
+ if not versions: continue
+ require(len(versions) == 1, "ambiguous acknowledged version")
+ version = versions[0]
+ erasure = version["erasure"]
+ layout = form["value"]["xl"]
+ group = next(i for i, ids in enumerate(layout["sets"]) if layout["this"] in ids)
+ if set_index is not None and group != set_index: continue
+ pool = form.get("poolIndex", 0)
+ slot = layout["sets"][group].index(layout["this"])
+ require(erasure["index"] == erasure["distribution"][slot] and version.get("deleted") is False,
+ "shard slot/role does not match actual format")
+ part = version["parts"][0]
+ relative = f"{bucket}/{entry['key']}/{version['dataDir']}/part.{part['number']}"
+ physical = indexed.get((node, disk, relative))
+ if not physical: continue
+ actual_role = "data" if erasure["index"] <= erasure["data_blocks"] else "parity"
+ cohort.append({"node": node, "disk": disk, "relativePath": relative, "sha256": physical["sha256"],
+ "size": physical["size"], "role": actual_role, "index": erasure["index"],
+ "versionId": entry["versionId"], "bucket": bucket, "key": entry["key"],
+ "partNumber": part["number"], "poolIndex": pool, "setIndex": group,
+ "diskUUID": layout["this"], "dataDir": version["dataDir"], "setSlot": slot,
+ "logical": version, "metadata": meta})
+ require(cohort, "no external physical target for the ACK fixture")
+ first = cohort[0]["logical"]["erasure"]
+ require(len(cohort) == first["data_blocks"] + first["parity_blocks"]
+ and len({(c["poolIndex"], c["setIndex"]) for c in cohort}) == 1
+ and len({c["index"] for c in cohort}) == len(cohort), "target lacks one complete erasure authority")
+ target = next((c for c in cohort if c["role"] == role), None)
+ require(target is not None, "requested shard role is absent")
+ return {**target, "cohort": cohort}
+
+
+def assert_overlap(observed, *, task_id, kind):
+ # Server events share one provider clock, and must intersect a task running
+ # interval. A broad wall-clock client request duration is not a witness.
+ intervals = observed.get("intervals", [])
+ require(observed.get("clockDomain") and intervals, "overlap lacks a common server observation clock")
+ heal = [v for v in intervals if v.get("kind") == "heal" and v.get("taskId") == task_id]
+ other = [v for v in intervals if v.get("kind") == kind and v.get("taskId") != task_id]
+ for row in intervals:
+ require(type(row.get("startNs")) is int and type(row.get("endNs")) is int
+ and row["endNs"] > row["startNs"] and row.get("evidenceId"), "invalid activity interval")
+ if not any(max(a["startNs"], b["startNs"]) < min(a["endNs"], b["endNs"]) for a in heal for b in other):
+ raise NotObserved("actual Heal/" + kind + " overlap not observed")
+
+
+def assert_terminal(status, bucket, expected, *, allowed_extra=(), allowed_absent=()):
+ outcome = status.get("outcome", {})
+ require(status.get("summary") == "finished" and not status.get("truncated", False)
+ and outcome.get("coverage") == "complete" and outcome.get("objectsTruncated") is False
+ and outcome.get("execution", {}).get("state") == "completed", "task did not finish with complete canonical coverage")
+ wanted = {(x["key"], x["versionId"]) for x in expected}
+ objects = outcome.get("objects", [])
+ actual = set()
+ repaired = healthy = 0
+ for row in objects:
+ identity = row.get("identity", {})
+ pair = (identity.get("object"), identity.get("versionId") or "null")
+ require(pair not in actual and identity.get("bucket") == bucket and identity.get("bucketIncarnationId")
+ and type(identity.get("poolIndex")) is int and type(identity.get("setIndex")) is int,
+ "receipt lacks exact bucket/version/pool/set responsibility")
+ actual.add(pair)
+ state = row.get("disposition", {}).get("state")
+ valid = {"repaired", "verified_healthy", "delete_marker_healthy"}
+ if pair in set(allowed_absent): valid.add("already_absent")
+ require(state in valid, "unknown/deferred/failed/skipped is not repair success")
+ if state == "delete_marker_healthy":
+ require(any(e["key"] == pair[0] and e["versionId"] == pair[1] and e["deleteMarker"]
+ for e in [*expected, *allowed_extra]), "data version received marker disposition")
+ repaired += state == "repaired"
+ healthy += state != "repaired"
+ permitted = wanted | {(e["key"], e["versionId"]) for e in allowed_extra}
+ require(wanted <= actual <= permitted, "receipt identities differ from the complete ACK fixture")
+ counters = outcome.get("counters", {})
+ expected_counts = {"processed": len(actual), "healed": repaired, "unchanged": healthy,
+ "skipped": 0, "failed": 0, "unknown": 0, "attemptFailures": 0, "overflowed": False}
+ require(all(type(counters.get(k)) is type(v) and counters[k] == v for k, v in expected_counts.items()),
+ "canonical counters disagree with complete per-object receipts")
+
+
+def assert_action(action, observed, *, task_id, runtime):
+ """Raw infrastructure evidence oracles. An adapter's booleans cannot pass."""
+ if action == "survivor-window":
+ interval = observed.get("taskInterval", {})
+ require(interval.get("taskId") == task_id and type(interval.get("startNs")) is int
+ and type(interval.get("endNs")) is int and interval["endNs"] > interval["startNs"]
+ and observed.get("sourceReachable") is False, "survivor Heal interval or retired-source isolation is missing")
+ elif action.endswith("window"):
+ kind = {"replacement-window": "replacement", "ilm-window": "ilm", "replication-window": "replication",
+ "pool-window": "pool", "survivor-window": "heal", "io-window": "io", "dependency-window": "dependency",
+ "capacity-lock-window": "capacity-lock"}[action]
+ assert_overlap(observed, task_id=task_id, kind=kind)
+ if action == "capacity-lock-window":
+ raise Blocked("H04 exact capacity-lock owner/commit barrier still needs reviewed native evidence")
+ if action in {"dedicated-vm-forced-stop", "dedicated-host-power-cycle"}:
+ wanted = "platform-forced-stop" if action.endswith("forced-stop") else "physical-power-cycle"
+ require(observed.get("eventKind") == wanted and observed.get("platformActivityId")
+ and observed.get("resourceId") == runtime["hostResourceId"]
+ and observed.get("bootIdBefore") and observed.get("bootIdAfter")
+ and observed["bootIdBefore"] != observed["bootIdAfter"]
+ and observed.get("recoveryChannel") == "out-of-band", "power event is not the assigned H6 platform event")
+ if action in {"restart-service", "crash-service", "restart-mixed"}:
+ require(observed.get("pidBefore") != observed.get("pidAfter") and observed.get("rootBefore")
+ and observed.get("rootBefore") == observed.get("rootAfter")
+ and observed.get("signal") == ("SIGKILL" if action == "crash-service" else "SIGTERM"),
+ "restart did not preserve the original data root/process interruption")
+ if action.startswith("upgrade-") or action.startswith("rollback-"):
+ pair = runtime["releasePair"]
+ require(pair.get("rollbackCompatible") is True and pair.get("compatibilityEvidenceSha256")
+ and len({p["packageSha256"] for p in pair["releases"]}) == 2, "immutable release pair/compatibility proof absent")
+ allowed = {p["binarySha256"] for p in pair["releases"]}
+ processes = observed.get("processes", [])
+ require(processes and all(p.get("binarySha256") in allowed and p.get("rootBefore") == p.get("rootAfter")
+ and p.get("rootBefore") for p in processes), "upgrade used unverified bytes or replaced data roots")
+ if action in {"mixed-status", "wait-mixed"}:
+ peers = observed.get("peers", [])
+ require(len({p.get("binarySha256") for p in peers}) == 2 and len(peers) >= 2, "no real mixed-release peers observed")
+ require(all(p.get("coverage") in {"partial", "unsupported"} for p in peers if not p.get("capabilitySupported")),
+ "unsupported peer reported complete status")
+ if action.startswith("io-") and action not in {"io-recover", "io-window"}:
+ fault = action[3:]
+ probe = observed.get("probe", {})
+ require(probe.get("uid") == observed.get("serviceUid") and probe.get("mountNamespace") == observed.get("serviceMountNamespace")
+ and probe.get("path") and observed.get("deviceId") == runtime["faultDeviceId"], "IO fault is not in the service namespace/device")
+ if fault == "bounded-latency":
+ require(type(probe.get("elapsedMs")) is int and probe["elapsedMs"] >= runtime["ioLatencyMs"], "bounded latency was not witnessed")
+ elif fault == "short-read":
+ require(type(probe.get("bytesRead")) is int and 0 <= probe["bytesRead"] < probe.get("bytesRequested", 0), "short read was not witnessed")
+ else:
+ require(probe.get("errno") == 5, "EIO was not witnessed")
+ if action.startswith("dependency-") and action not in {"dependency-window", "dependency-recover"}:
+ wanted = "AccessDenied" if action.endswith("denied") else "NoSuchVersion" if action.endswith("version-unavailable") else "Unavailable"
+ require(observed.get("errorCode") == wanted and observed.get("tenantId") == runtime["tenantId"]
+ and observed.get("namespaceId") == runtime["dependencyNamespaceId"], "dependency fault lacks isolated tenant/namespace/error evidence")
+ outcomes = observed.get("objectDispositions", [])
+ require(outcomes and all(o.get("state") in {"failed", "deferred", "unknown"} and o.get("reason") for o in outcomes),
+ "dependency error was treated as absent/healthy/removable")
+ if action in {"pool-settle", "replication-settle", "ilm-settle", "replacement-settle"}:
+ tasks = observed.get("tasks", [])
+ require(tasks and all(t.get("taskId") and t.get("state") == "completed" and not t.get("error") for t in tasks),
+ "background operation has an incomplete/failed task")
+ if action in {"decommission-stop", "decommission-cancel", "rebalance-stop"}:
+ require(observed.get("taskId") and observed.get("state") in {"stopped", "cancelled"}
+ and observed.get("healTaskId") == task_id and observed.get("healState") == "running", "pool cancellation also cancelled Heal or lost task ownership")
+ if action == "wait-inflight-exit":
+ require(observed.get("inflight") == 0 and observed.get("cancelCommitNs", 0) < observed.get("lastExitNs", 0),
+ "inflight work did not exit after durable cancellation")
+ if action == "decommission-clear":
+ require(observed.get("oldGeneration") and observed.get("inflight") == 0 and observed.get("state") == "cleared",
+ "clear ran before old inflight exit")
+ if action in {"decommission-resume", "rebalance-resume"}:
+ require(observed.get("newGeneration") and observed.get("oldGeneration")
+ and observed["newGeneration"] != observed["oldGeneration"], "resume reused an old generation")
+ if action == "decommission-write-guard":
+ require(observed.get("httpStatus") in {405, 409, 503} and observed.get("errorCode") in {"MethodNotAllowed", "OperationAborted", "SlowDown"},
+ "decommission did not enforce the frozen write restriction")
+ if action == "source-offline":
+ require(observed.get("sourceEndpoints") and observed.get("reachableEndpoints") == []
+ and observed.get("survivorEndpoints"), "retired source remains reachable")
+
+
+class Runner:
+ def __init__(self, runtime, candidate, testing_commit, evidence, *, adapter=None, probe_factory=Probe,
+ clock=time.monotonic, sleep=time.sleep):
+ self.runtime, self.candidate, self.testing_commit, self.evidence = runtime, candidate, testing_commit, evidence
+ self.adapter = adapter or Adapter(runtime, evidence)
+ self.probe_factory, self.clock, self.sleep = probe_factory, clock, sleep
+ self.active = []
+
+ def admit(self, probe, bucket):
+ path = "/rustfs/admin/v3/heal/" + quote(bucket, safe="")
+ options = {**BASE_OPTIONS, "recursive": True, "pool": None, "set": None}
+ row = self.adapter.record("heal-admission", probe.admin(path, [], json.dumps(options).encode()))
+ token = row.get("response", {}).get("clientToken")
+ require(row.get("httpStatus") == 200 and isinstance(token, str) and token, "Heal was not admitted with an owned token")
+ self.active.append((probe, path, token))
+ return path, token
+
+ def wait_task(self, probe, path, token):
+ deadline = self.clock() + 180
+ while True:
+ row = self.adapter.record("heal-status", probe.admin(path, [("clientToken", token)], b""))
+ require(row.get("httpStatus") == 200 and isinstance(row.get("response"), dict), "Heal status unavailable")
+ status = row["response"]
+ if status.get("summary") != "running": break
+ require(self.clock() < deadline, "Heal task exceeded its registered budget")
+ self.sleep(.5)
+ for name in probe.binding["endpoints"]:
+ other = self.probe_factory(probe.binding, name)
+ repeated = self.adapter.record("heal-terminal-peer", other.admin(path, [("clientToken", token)], b""))
+ require(repeated.get("httpStatus") == 200 and repeated.get("response", {}).get("summary") == status.get("summary")
+ and canonical(repeated["response"].get("outcome")) == canonical(status.get("outcome")), "peer terminal responsibility differs")
+ self.active.remove((probe, path, token))
+ return status
+
+ def fixture(self, probe, directory, bucket, spec, branch):
+ recorder = Recorder(probe, directory / "ACKs")
+ fixture = "DB" if spec.code == "I01" else "DV"
+ count = 257 if spec.interrupted else 8
+ expected = seed(recorder, bucket, fixture, count, [5 * 1024**2 + 123])
+ if spec.code == "H03" and branch == "versioned-multipart":
+ key = "multipart/0000.bin"
+ upload = recorder.call("create_multipart_upload", {"Bucket": bucket, "Key": key})
+ parts, full = [], b""
+ for number, size in enumerate((5 * 1024**2 + 17, 1024**2 + 47), 1):
+ body = payload(f"{bucket}/{key}/{number}", size); full += body
+ response = recorder.call("upload_part", {"Bucket": bucket, "Key": key, "UploadId": upload["UploadId"], "PartNumber": number}, body)
+ parts.append({"PartNumber": number, "ETag": response["ETag"]})
+ response = recorder.call("complete_multipart_upload", {"Bucket": bucket, "Key": key, "UploadId": upload["UploadId"], "MultipartUpload": {"Parts": parts}})
+ expected.append({"key": key, "versionId": response["VersionId"], "isLatest": True, "deleteMarker": False,
+ "payload": {"size": len(full), "sha256": hashlib.sha256(full).hexdigest()}})
+ if spec.code in {"I06", "G07"}:
+ key = "encrypted.bin"; body = payload(bucket + "/encrypted", 5 * 1024**2 + 73)
+ response = recorder.call("put_object", {"Bucket": bucket, "Key": key, "ServerSideEncryption": "aws:kms",
+ "SSEKMSKeyId": self.runtime["kmsKeyId"]}, body)
+ require(response.get("ServerSideEncryption") == "aws:kms" and response.get("SSEKMSKeyId"), "encrypted fixture was not encrypted")
+ expected.append({"key": key, "versionId": response["VersionId"], "isLatest": True, "deleteMarker": False,
+ "payload": {"size": len(body), "sha256": hashlib.sha256(body).hexdigest()}})
+ baseline = self.adapter.record("baseline-inventory", probe.inventory(bucket, unversioned_current_tags=fixture == "DB"))
+ verify_seed(expected, baseline)
+ return recorder, expected, baseline
+
+ def soak(self, scope, recorder, bucket, expected, topology):
+ duration = SOAK_SECONDS[scope["branch"]]
+ started = self.clock(); deadline = started + duration
+ writes = 0
+ metrics = []
+ schedule = ("drive-outage", "service-restart", "dependency-outage")
+ while self.clock() < deadline:
+ body = payload(f"{bucket}/soak/{writes}", 1024)
+ response = recorder.call("put_object", {"Bucket": bucket, "Key": "soak.bin"}, body)
+ require(response.get("VersionId") not in {None, "null"}, "soak write lost a version ACK")
+ for entry in expected:
+ if entry["key"] == "soak.bin": entry["isLatest"] = False
+ expected.append({"key": "soak.bin", "versionId": response["VersionId"], "isLatest": True, "deleteMarker": False,
+ "payload": {"size": len(body), "sha256": hashlib.sha256(body).hexdigest()}})
+ old = [e for e in expected if e["key"] == "soak.bin" and not e["isLatest"]]
+ if len(old) > 64:
+ entry = old[0]
+ deleted = recorder.call("delete_object", {"Bucket": bucket, "Key": entry["key"], "VersionId": entry["versionId"]})
+ require(deleted.get("VersionId") == entry["versionId"], "soak delete ACK differs from the exact version")
+ expected.remove(entry)
+ kind = schedule[writes % len(schedule)]
+ # The control operation must acknowledge the real fault and undo,
+ # separately from the data ACK. Both are retained on failure.
+ witness = self.adapter.call("soak-fault", scope=scope, kind=kind, iteration=writes, bucket=bucket)
+ require(witness.get("faultId") and witness.get("kind") == kind and witness.get("injectedAtNs"), "soak fault has no actual witness")
+ restored = self.adapter.call("soak-recover", scope=scope, faultId=witness["faultId"])
+ require(restored.get("faultId") == witness["faultId"] and restored.get("remainingFaults") == [],
+ "soak fault was not recovered")
+ sample = self.adapter.call("metrics", scope=scope)
+ for field in ("rssBytes", "openFds", "queuedTasks", "unresolvedRecords", "temporaryBytes"):
+ limit = self.runtime["soakLimits"][field]
+ require(type(sample.get(field)) is int and 0 <= sample[field] <= limit, "soak resource threshold exceeded: " + field)
+ require(sample.get("panicCount") == 0 and sample.get("busyLoopCount") == 0, "soak observed a panic or busy loop")
+ metrics.append(sample); writes += 1
+ self.sleep(min(120, max(0, deadline - self.clock())))
+ elapsed = self.clock() - started
+ require(elapsed >= duration and writes > 0, "short run cannot satisfy a 24h/72h window")
+ return self.adapter.record("soak-window", {"durationSeconds": duration, "actualElapsedSeconds": elapsed,
+ "acknowledgedWrites": writes, "samples": len(metrics), "topology": topology})
+
+ def branch(self, case_id, topology, branch):
+ recipe = select(case_id)[0]
+ scope = {"caseId": case_id, "topology": topology, "branch": branch}
+ if branch in NATIVE_BRANCHES:
+ return {**scope, "verdict": "BLOCKED", "cleanup": "NOT_RUN", "reason": "reviewed candidate-bound " + branch + " is unavailable"}
+ spec = scenario(recipe["code"], branch)
+ directory = self.evidence / f"{topology}-{branch}"
+ directory.mkdir()
+ bucket = self.runtime["binding"]["bucketPrefix"] + "-" + hashlib.sha256(canonical(scope).encode()).hexdigest()[:8]
+ binding = self.runtime["binding"]
+ probe = self.probe_factory(binding)
+ result = {**scope, "verdict": "RUNNING", "cleanup": "PENDING"}
+ reserved = False
+ def control(operation, **params):
+ return self.adapter.call(operation, scope=scope, bucket=bucket, **params)
+ try:
+ preflight = control("preflight")
+ assert_topology(preflight, topology, binding["candidateBinarySha256"], allow_old=self.runtime.get("oldBinarySha256"))
+ reserved = True
+ control("prepare", candidate=self.candidate)
+ recorder, expected, baseline = self.fixture(probe, directory, bucket, spec, branch)
+ if spec.family in {"ilm", "replication", "dependency"}:
+ dependency_prepared = control(spec.family + "-prepare", fixture=expected, branch=branch)
+ if spec.family == "retired-pool":
+ prepared = control("retire-pool-prepare", fixture=expected)
+ assert_action("source-offline", prepared, task_id="", runtime=self.runtime)
+ if spec.family == "soak":
+ self.soak(scope, recorder, bucket, expected, topology)
+ baseline = None # The final oracle uses every newly ACKed version.
+ control("stop")
+ original = control("snapshot", quiescent=True)["snapshot"]
+ faulted = original
+ targets = []
+ groups = sorted({disk["setIndex"] for disk in preflight["disks"]}) if spec.code == "H07" else [None]
+ for group in groups:
+ target = None
+ for entry in expected:
+ if entry["deleteMarker"]: continue
+ try: target = target_from_snapshot(original, bucket, entry, spec.role, set_index=group)
+ except ValueError: continue
+ break
+ require(target is not None, "fixture does not cover a required erasure set")
+ request = {"target": target, "kind": "missing" if spec.fault_kind == "metadata-missing" else spec.fault_kind,
+ "faultId": recipe["code"] + "-" + str(len(targets))}
+ if spec.fault_kind == "metadata-missing":
+ request["target"] = {**target, "relativePath": f"{bucket}/{target['key']}/xl.meta"}
+ physical = files(original)[(target["node"], target["disk"], request["target"]["relativePath"])]
+ request["target"].update(sha256=physical["sha256"], size=physical["size"])
+ before_fault = faulted
+ targets.append(request["target"])
+ receipt = control("apply-fault", **request)
+ faulted = control("snapshot", quiescent=True)["snapshot"]
+ validate_fault(before_fault, faulted, request["target"], receipt)
+ control("start")
+ witness = control("snapshot", quiescent=False)["snapshot"]
+ require(files(witness) == files(faulted), "NOT_OBSERVED: target changed before Heal admission")
+ preliminary = {}
+ if spec.family in {"io", "dependency"}:
+ action = spec.actions[0]
+ preliminary[action] = control(action, targets=targets, fixture=expected)
+ assert_action(action, preliminary[action], task_id="", runtime=self.runtime)
+ admitted_fixture = list(expected)
+ path, task_id = self.admit(probe, bucket)
+ durable = control("checkpoint", taskId=task_id, fixture=expected)
+ records = durable.get("records", [])
+ require(records and all(r.get("taskId") == task_id and r.get("bucketIncarnationId")
+ and SHA.fullmatch(r.get("sha256", "")) and r.get("durableProgress", 0) > 0 for r in records),
+ "original durable responsibility was not witnessed")
+ # Keep S3 foreground writes in our ACK ledger during pool/upgrade IO.
+ if spec.family in {"pool", "upgrade", "replication"}:
+ body = payload(bucket + "/foreground", 1024)
+ response = recorder.call("put_object", {"Bucket": bucket, "Key": "foreground.bin"}, body)
+ version = response.get("VersionId", "null")
+ expected.append({"key": "foreground.bin", "versionId": version, "isLatest": True, "deleteMarker": False,
+ "payload": {"size": len(body), "sha256": hashlib.sha256(body).hexdigest()}})
+ # Add a new task after the first one if its snapshot predates this
+ # ACK; never pretend it had already included the new object.
+ observations = dict(preliminary)
+ if spec.code == "G08" and branch == "replicate-delete":
+ key = next(e["key"] for e in expected if e["isLatest"] and not e["deleteMarker"])
+ deleted = recorder.call("delete_object", {"Bucket": bucket, "Key": key})
+ require(deleted.get("DeleteMarker") is True and deleted.get("VersionId") not in {None, "null"},
+ "replicated delete has no exact marker ACK")
+ for entry in expected:
+ if entry["key"] == key: entry["isLatest"] = False
+ marker = {"key": key, "versionId": deleted["VersionId"], "deleteMarker": True, "isLatest": True}
+ expected.append(marker)
+ stop_writes = threading.Event()
+ def foreground():
+ number = 0
+ while not stop_writes.is_set():
+ body = payload(f"{bucket}/live/{number}", 1024)
+ key = f"live/{number:06d}.bin"
+ response = recorder.call("put_object", {"Bucket": bucket, "Key": key}, body)
+ expected.append({"key": key, "versionId": response.get("VersionId", "null"), "isLatest": True,
+ "deleteMarker": False, "payload": {"size": len(body), "sha256": hashlib.sha256(body).hexdigest()}})
+ number += 1
+ stop_writes.wait(2)
+ return number
+ with ThreadPoolExecutor(max_workers=1) as workers:
+ future = workers.submit(foreground) if spec.family in {"host", "upgrade", "pool", "replication"} else None
+ try:
+ for action in spec.actions:
+ if action in preliminary: continue
+ observation = control(action, taskId=task_id, targets=targets, fixture=list(expected))
+ observations[action] = observation
+ assert_action(action, observation, task_id=task_id, runtime=self.runtime)
+ finally:
+ stop_writes.set()
+ if future is not None:
+ require(future.result() > 0, "no foreground ACK was recorded during lifecycle operations")
+ if spec.code == "G07" and branch in {"current-expiry", "noncurrent-expiry"}:
+ expired = observations["ilm-settle"].get("expiredVersions", [])
+ require(expired and all(e.get("eventId") and e.get("ruleId") and e.get("versionId") for e in expired), "expiration has no actual version/rule event")
+ gone = {(e["key"], e["versionId"]) for e in expired}
+ require(gone <= {(e["key"], e["versionId"]) for e in expected}, "ILM event refers to an unacknowledged version")
+ if branch == "current-expiry":
+ require(all(e.get("deleteMarkerVersionId") for e in expired), "current expiry lacks the created marker identity")
+ for event in expired:
+ for entry in expected:
+ if entry["key"] == event["key"]: entry["isLatest"] = False
+ expected.append({"key": event["key"], "versionId": event["deleteMarkerVersionId"], "deleteMarker": True, "isLatest": True})
+ gone = set() # Current expiration creates a marker; old versions survive.
+ else:
+ require(all(not e["isLatest"] for e in expected if (e["key"], e["versionId"]) in gone),
+ "noncurrent expiration selected a current version")
+ expected = [e for e in expected if (e["key"], e["versionId"]) not in gone]
+ terminal = self.wait_task(probe, path, task_id)
+ # Keep the complete pre-admission denominator. A task cannot
+ # choose its own expected identities by returning fewer receipts.
+ assert_terminal(terminal, bucket, admitted_fixture, allowed_extra=expected,
+ allowed_absent=gone if spec.code == "G07" and branch in {"current-expiry", "noncurrent-expiry"} else ())
+ control("stop")
+ repaired = control("snapshot", quiescent=True)["snapshot"]
+ if spec.family in {"io", "upgrade", "dependency", "retired-pool"} and not any(e["key"] == "foreground.bin" for e in expected):
+ for target in targets:
+ if spec.fault_kind != "metadata-missing": validate_repaired_physical(original, repaired, target)
+ proof = control("verify-parts", fixture=expected, targets=targets, beforeAnyTargetGET=True)
+ require(proof.get("toolSha256") == self.runtime["inspectorSha256"] and proof.get("candidateCommit") == binding["candidateCommit"],
+ "part verification tool differs from the candidate-bound inspector")
+ verified = proof.get("parts", [])
+ require(verified and all(p.get("sha256") and p.get("checksumVerified") is True for p in verified), "physical shard verification is incomplete")
+ physical_ids = {(p["key"], p["versionId"]) for p in verified}
+ require({(e["key"], e["versionId"]) for e in expected if not e["deleteMarker"]} <= physical_ids,
+ "physical proof omitted an acknowledged version")
+ if spec.family in {"pool", "retired-pool"}:
+ movements = proof.get("movements", [])
+ require(movements and all(m.get("sourcePool") != m.get("targetPool") and m.get("targetCommitNs", 0) > 0
+ and m.get("sourceDeleteNs", 0) >= m["targetCommitNs"] for m in movements), "source removed before target authority committed")
+ if spec.code in {"H03", "H05"}:
+ require(proof.get("sourceReachable") is False and proof.get("retiredRootHashesBefore")
+ and proof["retiredRootHashesBefore"] == proof.get("retiredRootHashesAfter"), "retired pool changed or remains reachable")
+ control("start")
+ final = self.adapter.record("final-inventory", probe.inventory(bucket, unversioned_current_tags=spec.code == "I01"))
+ verify_seed(expected, final)
+ if baseline is not None and spec.family not in {"pool", "upgrade", "replication", "ilm"}: compare_inventories(baseline, final)
+ if spec.family in {"replication", "ilm", "dependency"}:
+ endpoint = self.runtime["replicaEndpoint"] if spec.family == "replication" else self.runtime["tierEndpoint"]
+ remote = self.probe_factory(binding, endpoint)
+ remote_inventory = self.adapter.record("remote-inventory", remote.inventory(bucket))
+ mapping = observations.get("replication-settle", observations.get("ilm-settle", observations.get("dependency-recover", {}))).get("versionMapping", [])
+ require(mapping, "remote versions have no exact mapping")
+ remote_ids = {(e["key"], e["versionId"]): e for e in remote_inventory["entries"]}
+ local_ids = {(e["key"], e["versionId"]): e for e in final["entries"]}
+ for row in mapping:
+ left = local_ids[(row["key"], row["sourceVersionId"])]; right = remote_ids[(row["key"], row["targetVersionId"])]
+ require(left.get("payload") == right.get("payload") and left["deleteMarker"] == right["deleteMarker"], "remote payload or marker differs")
+ require({(r["key"], r["sourceVersionId"]) for r in mapping} == set(local_ids), "remote mapping omits local acknowledged versions")
+ if spec.code == "H04" and branch == "capacity-lock-retry-window":
+ raise Blocked("exact capacity-lock retry barrier requires reviewed candidate native evidence")
+ result.update(verdict="BLOCKED", checksCompleted=True,
+ reason="environment scenario checks completed; assigned adapter evidence requires source-assertion review before canonical PASS")
+ except (Exception, KeyboardInterrupt) as exc:
+ result.update(verdict="BLOCKED" if isinstance(exc, Blocked) else "NOT_OBSERVED" if isinstance(exc, NotObserved) else "FAIL",
+ reason=str(exc))
+ finally:
+ if reserved:
+ try:
+ for owner, path, token in tuple(self.active):
+ self.adapter.record("cancel-original-task", owner.admin(path, [("clientToken", token), ("forceStop", "true")], b""))
+ self.active.remove((owner, path, token))
+ control("recover", lastACKsOnly=True, originalFailureRetained=result["verdict"] != "PASS")
+ cleaned = control("cleanup")
+ require(cleaned.get("remainingOwnedPaths") == [] and cleaned.get("activeOwnedServices") == []
+ and cleaned.get("remainingFaults") == [] and cleaned.get("pendingMutations") == [], "cleanup left a dirty environment")
+ result["cleanup"] = "PASS"
+ except (Exception, KeyboardInterrupt) as exc:
+ result.update(cleanup="FAIL", recoveryError=str(exc))
+ else: result["cleanup"] = "NOT_RUN"
+ self.adapter.record("branch-result", result)
+ return result
+
+ def run(self, case_id):
+ recipe = select(case_id)[0]
+ mapping = variant_topologies(recipe)
+ results = []
+ for topology in sorted(self.runtime["topologies"]):
+ for branch in recipe["variants"]:
+ if topology not in mapping[branch]: continue
+ results.append(self.branch(case_id, topology, branch))
+ if results[-1]["cleanup"] == "FAIL": return results
+ return results
+
+
+def run(runtime_path, case_id, candidate_path, testing_commit, artifacts):
+ runtime = json.loads(runtime_path.read_text())
+ candidate = json.loads(candidate_path.read_text())
+ runtime_binding(runtime, case_id, candidate, testing_commit)
+ evidence = artifacts / "evidence" / "execution"
+ evidence.mkdir(parents=True)
+ runner = Runner(runtime, candidate, testing_commit, evidence)
+ rows = runner.run(case_id)
+ checks_complete = bool(rows) and all(row.get("checksCompleted") and row["cleanup"] == "PASS" for row in rows)
+ verdict = "INFRA_ERROR" if any(r["cleanup"] == "FAIL" for r in rows) else "FAIL" if any(r["verdict"] == "FAIL" for r in rows) else "BLOCKED"
+ recipe = select(case_id)[0]
+ result = {"schema_version": 1, "case_id": case_id, "source_status": recipe["contract"]["source_status"],
+ "migration_status": "unverified", "profile": ENVIRONMENT_GATED_CASES[case_id]["profile"],
+ "verdict": verdict, "cleanup_status": "PASS" if all(r["cleanup"] in {"PASS", "NOT_RUN"} for r in rows) else "FAIL",
+ "evidence": ["evidence/execution/summary.json"],
+ "reason": "; ".join(r.get("reason", r.get("recoveryError", "")) for r in rows) or "required branch evidence is incomplete"}
+ binding = runtime["binding"]
+ result["provenance"] = {"candidate_url": candidate["package_url"], "candidate_sha256": candidate["package_sha256"],
+ "binary_sha256": binding["candidateBinarySha256"], "source_commit": candidate["source_sha"], "testing_commit": testing_commit,
+ "rc_path": str(Path(runtime["rcBinary"]).resolve()), "rc_sha256": binding["rcBinarySha256"]}
+ save(evidence / "summary.json", {"caseId": case_id, "results": rows, "checksCompleted": checks_complete,
+ "canonicalPassClaimed": False, "reviewRequired": True})
+ save(artifacts / "cases" / (case_id + ".json"), result)
+ return result
diff --git a/scripts/validation_heal/environment_gate.py b/scripts/validation_heal/environment_gate.py
new file mode 100644
index 00000000..e80685c9
--- /dev/null
+++ b/scripts/validation_heal/environment_gate.py
@@ -0,0 +1,224 @@
+#!/usr/bin/env python3
+"""Record a safe, disabled-by-default result for environment-gated Heal cases.
+
+The first migration batch has complete procedures in ``recipes.json`` but the
+current auto-testing fleet does not expose all required resources. This
+default path never mutates the fleet. It records missing capabilities and
+requires an explicit switch, capability manifest and candidate-bound runtime
+before invoking the extended scenario driver.
+"""
+
+from __future__ import annotations
+
+import argparse
+import hashlib
+import json
+import os
+from pathlib import Path
+import tempfile
+from typing import Mapping, Sequence
+
+try:
+ from .execution_policy import CAPABILITIES_ENV, ENABLE_ENV, ENVIRONMENT_GATED_CASES, enabled
+except ImportError:
+ from execution_policy import CAPABILITIES_ENV, ENABLE_ENV, ENVIRONMENT_GATED_CASES, enabled
+
+ROOT = Path(__file__).resolve().parents[2]
+CATALOG = ROOT / "validation-cases.json"
+
+
+def _catalog_case(case_id: str) -> dict:
+ document = json.loads(CATALOG.read_text(encoding="utf-8"))
+ for module in document["modules"]:
+ for case in module["cases"]:
+ if case["id"] == case_id:
+ return case
+ raise ValueError(f"unknown validation case: {case_id}")
+
+
+def load_capabilities(path: Path | None) -> set[str]:
+ if path is None:
+ return set()
+ if not path.is_file() or path.is_symlink():
+ return set()
+ value = json.loads(path.read_text(encoding="utf-8"))
+ if isinstance(value, list):
+ values = value
+ elif isinstance(value, dict):
+ values = value.get("capabilities", [])
+ else:
+ raise ValueError("capability manifest must be a list or an object with capabilities")
+ if not isinstance(values, list) or any(not isinstance(item, str) or not item for item in values):
+ raise ValueError("capability manifest contains an invalid capability")
+ return set(values)
+
+
+def decision(case_id: str, *, environ: Mapping[str, str] | None = None,
+ capabilities: Sequence[str] = ()) -> dict:
+ policy = ENVIRONMENT_GATED_CASES[case_id]
+ values = environ if environ is not None else os.environ
+ requested = set(capabilities)
+ if not enabled(values):
+ return {
+ "state": "disabled",
+ "reasonCode": "ENV_DISABLED_BY_DEFAULT",
+ "enabled": False,
+ "missingCapabilities": list(policy["requiredCapabilities"]),
+ "reason": (
+ f"{policy['reason']} 默认关闭;不会启动真实测试。只有设置 "
+ f"{ENABLE_ENV}=1 并提供 {CAPABILITIES_ENV},且清单满足全部能力后才允许进入准备阶段。"
+ ),
+ }
+ missing = sorted(set(policy["requiredCapabilities"]) - requested)
+ if missing:
+ return {
+ "state": "blocked",
+ "reasonCode": "ENV_CAPABILITY_MISSING",
+ "enabled": True,
+ "missingCapabilities": missing,
+ "reason": (
+ f"已显式开启 {ENABLE_ENV}=1,但 auto-testing Heal 环境能力清单仍缺少:"
+ f"{', '.join(missing)}。原始阻断原因:{policy['reason']}"
+ ),
+ }
+ return {
+ "state": "ready",
+ "reasonCode": "ENV_RUNTIME_REQUIRED",
+ "enabled": True,
+ "missingCapabilities": [],
+ "reason": (
+ "已显式开启且环境能力清单满足;还需 VALIDATION_HEAL_ENVIRONMENT_RUNTIME 指定候选绑定的资源与控制适配器。"
+ "缺少实际资源绑定仍为 BLOCKED,能力声明本身不记 PASS。"
+ ),
+ }
+
+
+def _write_json(path: Path, value: dict) -> None:
+ path.parent.mkdir(parents=True, exist_ok=True)
+ with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=path.parent, delete=False) as stream:
+ temporary = Path(stream.name)
+ json.dump(value, stream, ensure_ascii=False, indent=2)
+ stream.write("\n")
+ os.replace(temporary, path)
+
+
+def write_blocked(case_id: str, artifacts: Path, *, candidate: Path | None,
+ testing_commit: str | None, environ: Mapping[str, str] | None = None,
+ override: tuple[str, str] | None = None) -> dict:
+ if case_id not in ENVIRONMENT_GATED_CASES:
+ raise ValueError(f"case is not environment-gated: {case_id}")
+ policy = ENVIRONMENT_GATED_CASES[case_id]
+ capabilities_path = None
+ values = environ if environ is not None else os.environ
+ raw_capabilities = values.get(CAPABILITIES_ENV, "").strip()
+ if raw_capabilities:
+ capabilities_path = Path(raw_capabilities).expanduser()
+ capabilities_error = None
+ try:
+ capabilities = load_capabilities(capabilities_path) if enabled(values) else set()
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
+ capabilities = set()
+ capabilities_error = str(exc)
+ result_decision = decision(case_id, environ=values, capabilities=capabilities)
+ if capabilities_error:
+ result_decision.update(state="blocked", reasonCode="ENV_CAPABILITY_MANIFEST_INVALID",
+ reason=f"能力清单无效:{capabilities_error};原始阻断原因:{policy['reason']}")
+ if override:
+ result_decision.update(state="blocked", reasonCode=override[0], reason=override[1])
+
+ evidence_dir = artifacts / "evidence"
+ evidence_path = evidence_dir / "environment-gate.json"
+ evidence = {
+ "schemaVersion": 1,
+ "caseId": case_id,
+ "code": policy["code"],
+ "policy": policy,
+ "enabled": result_decision["enabled"],
+ "state": result_decision["state"],
+ "reasonCode": result_decision["reasonCode"],
+ "reason": result_decision["reason"],
+ "missingCapabilities": result_decision["missingCapabilities"],
+ "enableVariable": ENABLE_ENV,
+ "capabilitiesVariable": CAPABILITIES_ENV,
+ "capabilitiesPath": str(capabilities_path) if capabilities_path else None,
+ "capabilities": sorted(capabilities),
+ "candidateManifest": str(candidate.resolve()) if candidate and candidate.exists() else None,
+ "candidateManifestSha256": hashlib.sha256(candidate.read_bytes()).hexdigest()
+ if candidate and candidate.is_file() and not candidate.is_symlink() else None,
+ "testingCommit": testing_commit,
+ "fleetTouched": False,
+ "canonicalPassClaimed": False,
+ }
+ _write_json(evidence_path, evidence)
+ case = _catalog_case(case_id)
+ result = {
+ "schema_version": 1,
+ "case_id": case_id,
+ "source_status": case["source_status"],
+ "migration_status": "unverified",
+ "profile": policy["profile"],
+ "verdict": "BLOCKED",
+ "cleanup_status": "NOT_RUN",
+ "reason": f"[{policy['code']}:{result_decision['reasonCode']}] {result_decision['reason']}",
+ "evidence": ["evidence/environment-gate.json"],
+ "execution_policy": {
+ "kind": policy["kind"],
+ "disabledByDefault": policy["disabledByDefault"],
+ "enableVariable": policy["enableVariable"],
+ "capabilitiesVariable": policy["capabilitiesVariable"],
+ "requiredCapabilities": policy["requiredCapabilities"],
+ "reasonCode": policy["reasonCode"],
+ },
+ }
+ result_path = artifacts / "cases" / f"{case_id}.json"
+ _write_json(result_path, result)
+ return result
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--case-id", required=True)
+ parser.add_argument("--candidate-manifest", type=Path)
+ parser.add_argument("--testing-commit")
+ parser.add_argument("--validation-artifacts-dir", type=Path, required=True)
+ args = parser.parse_args()
+ # The default path imports no live test dependencies and never reads or
+ # invokes a resource adapter. A capability declaration alone cannot run.
+ capabilities_error = None
+ try:
+ capabilities = load_capabilities(Path(os.environ[CAPABILITIES_ENV])) if enabled() and os.environ.get(CAPABILITIES_ENV) else set()
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
+ capabilities = set(); capabilities_error = str(exc)
+ gate = decision(args.case_id, capabilities=capabilities)
+ if capabilities_error:
+ gate.update(state="blocked", reasonCode="ENV_CAPABILITY_MANIFEST_INVALID", reason=capabilities_error)
+ if gate["state"] == "ready" and os.environ.get("VALIDATION_HEAL_ENVIRONMENT_RUNTIME"):
+ try:
+ from .environment_cases import run, runtime_binding, RUNTIME_ENV
+ except ImportError:
+ from environment_cases import run, runtime_binding, RUNTIME_ENV
+ runtime_path = Path(os.environ[RUNTIME_ENV])
+ try:
+ runtime_binding(json.loads(runtime_path.read_text()), args.case_id,
+ json.loads(args.candidate_manifest.read_text()), args.testing_commit)
+ except (OSError, ValueError, KeyError, TypeError) as exc:
+ write_blocked(args.case_id, args.validation_artifacts_dir,
+ candidate=args.candidate_manifest, testing_commit=args.testing_commit,
+ override=("ENV_RUNTIME_INVALID", str(exc)))
+ return 0
+ result = run(runtime_path, args.case_id, args.candidate_manifest, args.testing_commit,
+ args.validation_artifacts_dir)
+ print(f"{args.case_id}: {result['verdict']}")
+ return 0 if result["verdict"] == "PASS" else 1
+ write_blocked(args.case_id, args.validation_artifacts_dir,
+ candidate=args.candidate_manifest, testing_commit=args.testing_commit)
+ print(f"{args.case_id}: BLOCKED by environment gate; fleet untouched")
+ return 0
+
+
+if __name__ == "__main__":
+ try:
+ raise SystemExit(main())
+ except (OSError, ValueError, KeyError, json.JSONDecodeError) as exc:
+ print(f"environment gate error: {exc}", file=os.sys.stderr)
+ raise SystemExit(2)
diff --git a/scripts/validation_heal/execution_policy.py b/scripts/validation_heal/execution_policy.py
new file mode 100644
index 00000000..f007b366
--- /dev/null
+++ b/scripts/validation_heal/execution_policy.py
@@ -0,0 +1,189 @@
+#!/usr/bin/env python3
+"""Execution policy for Heal cases whose live proof needs special handling.
+
+The recipe remains the source of the operator procedure. This module only
+classifies whether the dispatcher may enter that procedure and records the
+reason when it cannot make a canonical PASS claim.
+"""
+
+from __future__ import annotations
+
+import os
+from typing import Mapping
+
+
+ENABLE_ENV = "AUTO_TESTING_ENABLE_ENV_GATED_HEAL"
+CAPABILITIES_ENV = "VALIDATION_HEAL_ENVIRONMENT_CAPABILITIES"
+
+
+def _environment(
+ code: str,
+ reason_code: str,
+ reason: str,
+ capabilities: tuple[str, ...],
+ *,
+ profile: str = "cluster-1pool",
+) -> dict:
+ return {
+ "kind": "environment-gated",
+ "disabledByDefault": True,
+ "dispatchable": True,
+ "code": code,
+ "reasonCode": reason_code,
+ "reason": reason,
+ "requiredCapabilities": list(capabilities),
+ "enableVariable": ENABLE_ENV,
+ "capabilitiesVariable": CAPABILITIES_ENV,
+ "profile": profile,
+ }
+
+
+def _native(code: str, reason_code: str, reason: str, capabilities: tuple[str, ...]) -> dict:
+ return {
+ "kind": "evidence-blocked",
+ "disabledByDefault": False,
+ "dispatchable": False,
+ "code": code,
+ "reasonCode": reason_code,
+ "reason": reason,
+ "requiredCapabilities": list(capabilities),
+ "enableVariable": None,
+ "capabilitiesVariable": None,
+ }
+
+
+# These cases are procedure-complete, but the current auto-testing fleet does
+# not provide the resources needed to run them safely. They are dispatchable
+# only through the explicit environment gate and are disabled by default.
+ENVIRONMENT_GATED_CASES = {
+ "heal.mixed-version-upgrade-rollback": _environment(
+ "F08", "ENV_IMMUTABLE_RELEASE_PAIR",
+ "需要两个已固定且相互兼容的 RustFS release、混合版本 peer 状态和经过审核的回滚兼容矩阵;当前 auto-testing Heal 节点没有这组不可变双版本资源。",
+ ("immutable_release_pair", "mixed_version_peer_status", "rolling_upgrade_rollback", "rollback_compatibility_matrix"),
+ ),
+ "heal.dedicated-host-forced-restart": _environment(
+ "F09", "ENV_DEDICATED_H6_HOST",
+ "需要 H6 专用主机、平台强制停启权限、带外恢复和 boot_id/平台事件证据;共享 Heal 节点不能证明真实电源循环。",
+ ("dedicated_h6_host", "platform_power_cycle", "out_of_band_recovery", "boot_id_evidence"),
+ ),
+ "heal.race-ilm-expiry-and-transition": _environment(
+ "G07", "ENV_DEDICATED_ILM_TIER_WINDOW",
+ "需要隔离 ILM/tier 租户、真实 Expire/Transition/Restore 窗口和远端版本映射;共享 ILM 环境不能安全制造交叠窗口。",
+ ("dedicated_ilm_tier", "real_expire_transition_restore_window", "remote_version_mapping", "isolated_rule_cleanup"),
+ ),
+ "heal.race-replication-resync": _environment(
+ "G08", "ENV_ISOLATED_REPLICATION_PAIR",
+ "需要隔离的复制两端、resync 控制、跨端版本映射和 Heal/resync 实际交叠窗口;当前固定 fleet 没有独占复制对。",
+ ("isolated_replication_pair", "resync_control", "cross_endpoint_version_mapping", "replication_overlap_window"),
+ ),
+ "heal.with-expansion-rebalance": _environment(
+ "H01", "ENV_H5_EXPANSION_REBALANCE",
+ "需要 H5 pool 扩容、rebalance 和 Heal 同时活动的完整拓扑,并逐对象保存 pool/set 迁移证据;当前 auto-testing 未绑定扩容前后布局与专用操作窗口。",
+ ("h5_pool_expansion", "rebalance_task", "per_object_pool_set_mapping", "retired_scanner_evidence"),
+ ),
+ "heal.with-rebalance-stop-resume": _environment(
+ "H02", "ENV_H5_REBALANCE_LIFECYCLE",
+ "需要可观测的 H5 rebalance、停止/恢复/服务重启控制及独立 task 身份;当前 auto-testing 未绑定这两类任务的独立身份和操作窗口。",
+ ("h5_rebalance", "rebalance_stop_resume", "independent_task_identity", "service_restart_control"),
+ ),
+ "heal.with-decommission": _environment(
+ "H03", "ENV_H5_DECOMMISSION",
+ "需要 H5 pool decommission、survivor 目标、写入限制和移动 ACK/物理权威证据;当前 auto-testing 未绑定本例退役前后拓扑和恢复方案。",
+ ("h5_decommission", "survivor_pool", "decommission_write_guard", "movement_ack_physical_proof"),
+ ),
+ "heal.with-decommission-cancel-clear": _environment(
+ "H04", "ENV_H5_DECOMMISSION_CANCEL",
+ "需要退役取消/清理/新代次恢复以及 #2423 所需容量锁重试窗口;普通取消结果不能替代这个临界点。",
+ ("h5_decommission", "cancel_clear_resume", "capacity_lock_retry_window", "generation_bound_ack_ledger"),
+ ),
+ "heal.after-retired-pool-removal": _environment(
+ "H05", "ENV_H5_RETIRED_POOL_REMOVAL",
+ "需要完整迁移后移除 retired pool、源保持离线和 survivor-only 访问;当前 auto-testing 未提供本例源持续离线和 survivor-only 的绑定证据。",
+ ("h5_retired_pool_removal", "source_offline_proof", "survivor_authority", "retired_pool_write_guard"),
+ ),
+ "heal.ec84-multi-set-restart": _environment(
+ "H07", "ENV_EC84_MULTI_SET_TOPOLOGY",
+ "需要 H3 的 12 个独立盘和 H4 的至少 24 个独立 endpoint,并按 set 分别记录重启/崩溃窗口;当前 H4 资源不可用。",
+ ("h3_ec84_12_independent_disks", "h4_ec84_24_independent_endpoints", "per_set_identity", "per_set_restart_crash_evidence"),
+ ),
+ "heal.slow-io-and-eio": _environment(
+ "I01", "ENV_DEDICATED_FAULT_IO",
+ "需要专用 faultfs 或故障设备、可恢复的 slow/short-read/EIO 注入和 namespace 隔离;共享盘模拟会污染其他运行。",
+ ("dedicated_faultfs_or_device", "slow_io_short_read_eio", "namespace_isolation", "fault_layer_restore"),
+ ),
+ "heal.kms-and-tier-outage": _environment(
+ "I06", "ENV_DEDICATED_KMS_TIER_TENANT",
+ "需要专用 KMS/tier 租户、依赖网络隔离和安全的 key/tier 版本证据;不能在共享依赖上全局断网。",
+ ("dedicated_kms_tier_tenant", "dependency_network_isolation", "key_redaction", "tier_version_mapping"),
+ ),
+ "heal.long-soak": _environment(
+ "J05", "ENV_DEDICATED_LONG_SOAK_LANE",
+ "必须在独占 H2/H5 lane 真实运行 24 小时和 72 小时,包含限速、监控、到期回收和完整资源预算;短跑不能替代 soak 证据。",
+ ("dedicated_soak_lane", "wall_clock_24h_72h", "resource_budget_monitoring", "scheduled_fault_recovery"),
+ ),
+}
+
+
+# These cases still lack complete orchestration/evidence, and I05 also lacks
+# an exact service-side ownership barrier. More nodes alone do not close them.
+EVIDENCE_BLOCKED_CASES = {
+ "heal.bucket-incarnation-recreation": _native(
+ "C11", "EVIDENCE_INCARNATION_LIVE_ORCHESTRATION",
+ "已实现 H1/H2 旧元数据重返、真实 checkpoint/resume 回放、ACK 绑定的 committed MRF 回放和精确 native 证据门禁;尚缺全部现场分支的独立审计,诊断执行保持 FAIL/BLOCKED,原生前置通过不能替代 canonical 验收。",
+ ("bucket_incarnation_isolation", "checkpoint_mrf_replay_guard", "incarnation_native_evidence_binding", "complete_live_orchestrator"),
+ ),
+ "heal.checkpoint-and-mrf-corruption": _native(
+ "F07", "EVIDENCE_DURABLE_JOURNAL_ORCHESTRATION",
+ "已实现 H1/H2 checkpoint 截断、未来 schema、真实较早代次回放、committed MRF CRC 故障、私有限额卷的真实 journal ENOSPC 和独立安全重扫;10 项原生回归仅为前置证据。仍缺全部所需 fsync/commit 临界点审核和完整现场分支的独立 live 审计,诊断执行不能提升 canonical 验收。",
+ ("durable_checkpoint_mrf_fault", "write_failure_retention", "publication_boundary_evidence_binding", "complete_live_orchestrator"),
+ ),
+ "heal.lock-timeout-and-caller-abort": _native(
+ "I05", "NATIVE_LOCK_OWNER_ABORT_BARRIER",
+ "已有同候选源码的 lock owner、取消物理 unlink 与 stale read-repair 原生子集;H1/H2 timeout 与 queued-successor 诊断均已通过独立 live 审计(39767336741/1、39817606025/1),包括原始 ACK、历史代次、真实锁事件、各 peer inventory、停机后新分片完整性和有界 FD 收尾。仍缺实际断连边界的精确所有权、完整 native timeout/stale 屏障、stale-generation controller,以及 H1/H2 DL/DV 完整对象/ACK/物理/锁/FD 闭环。精确源码 cfg(test) 原生证据独立保留;诊断子集和原生子集不能充作完整 canonical PASS。",
+ ("server_lock_owner", "lock_timeout_fence", "caller_abort_sequence", "stale_read_repair_fence", "exact_source_native_evidence", "complete_live_orchestrator", "final_object_lock_fd_closure"),
+ ),
+ "heal.outcome-counters-and-error-truth": _native(
+ "J02", "EVIDENCE_MIXED_OUTCOME_ORCHESTRATION",
+ "已有 mixed healthy/repaired/failed、独立 dry-run、依赖 deferral、真实 SDK legacy Unknown、原始全副本损坏元数据 Unknown、只读永久失败/新 token 补偿及版本级 grace Deferred/新 token 清理执行流程,均按逐对象 manifest/attemptFailures 对账;仍需完整串行 live 证据及独立验收,故保持禁止默认 dispatch 和 canonical admission;本地检查不能作为完整 PASS。",
+ ("mixed_outcome_fixture", "per_object_canonical_receipts", "complete_live_orchestrator", "attempt_failure_visibility"),
+ ),
+}
+
+FIRST_BATCH = frozenset(ENVIRONMENT_GATED_CASES)
+SECOND_BATCH = frozenset(EVIDENCE_BLOCKED_CASES)
+DISPATCHABLE_CASES = FIRST_BATCH | frozenset(
+ {
+ "heal.queue-pressure-and-admission", "heal.admin-option-contract", "heal.admin-nolock-safety",
+ "heal.race-unversioned-overwrite", "heal.race-versioned-put", "heal.race-version-delete",
+ "heal.race-markers-and-batch-delete", "heal.race-multipart-complete-abort",
+ "heal.race-copy-and-metadata-update", "heal.dangling-and-orphan-safety",
+ "heal.conflicting-subquorum-authority", "heal.multi-pool-target-isolation",
+ "heal.network-partition-and-lock-quorum", "heal.enospc-inodes-and-quota", "heal.disk-and-node-flapping",
+ "heal.coordinator-process-crash", "heal.encrypted-objects", "heal.foreign-format-and-unknown-schema",
+ "heal.below-write-quorum", "heal.bucket-metadata-integrity", "heal.admin-scope-isolation",
+ "heal.admin-healthy-noop", "heal.admin-invalid-request", "heal.admin-token-cross-node-status",
+ "heal.admin-dry-run", "heal.missing-part-file", "heal.data-shard-bitrot", "heal.parity-shard-bitrot",
+ "heal.truncated-and-zeroed-shard", "heal.completed-multipart-object", "heal.range-head-and-conditions",
+ "heal.manual-normal-attribution", "heal.manual-deep-attribution", "heal.performance-abba",
+ "heal.historical-versions", "heal.repeat-idempotency", "heal.versioning-suspended-null",
+ "heal.below-read-quorum", "heal.missing-or-corrupt-xlmeta", "heal.object-metadata-and-tags",
+ "heal.multiple-shard-loss", "heal.special-keys-and-pagination", "heal.object-size-boundaries",
+ "heal.admin-authorization", "heal.node-outage-rejoin", "heal.object-lock-retention",
+ "heal.metadata-payload-mismatch", "heal.read-repair-attribution", "heal.disk-readonly-and-permission",
+ "heal.scanner-cold-object-repair", "heal.delete-marker-semantics", "heal.status-partial-disabled-unknown",
+ "heal.incremental-cursor-and-truncation", "heal.admin-stop-and-stale-token", "heal.cancelled-task-restart",
+ "heal.admin-overlap-and-force-start", "heal.root-graceful-coordinator-restart", "heal.read-quorum-boundary",
+ "heal.stale-version-rejoin", "heal.disk-loss-and-repair", "heal.disk-replacement-formatted",
+ "heal.full-cluster-restart", "heal.replacement-auto-rebuild", "heal.target-graceful-restart",
+ "heal.target-process-crash", "heal.mrf-offline-write-replay", "heal.disk-replacement-unformatted",
+ }
+)
+
+
+def policy_for(case_id: str) -> dict | None:
+ return ENVIRONMENT_GATED_CASES.get(case_id) or EVIDENCE_BLOCKED_CASES.get(case_id)
+
+
+def enabled(environ: Mapping[str, str] | None = None) -> bool:
+ values = environ if environ is not None else os.environ
+ return values.get(ENABLE_ENV, "").strip().lower() in {"1", "true", "yes", "on"}
diff --git a/scripts/validation_heal/flapping.py b/scripts/validation_heal/flapping.py
new file mode 100644
index 00000000..cd1d0106
--- /dev/null
+++ b/scripts/validation_heal/flapping.py
@@ -0,0 +1,554 @@
+#!/usr/bin/env python3
+"""B10: three bounded member-loss cycles and an observed rebuild interruption."""
+
+from __future__ import annotations
+
+import argparse
+from concurrent.futures import ThreadPoolExecutor
+import copy
+from datetime import datetime, timedelta, timezone
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import secrets
+import signal
+import sys
+import time
+from urllib import request
+from urllib.error import URLError
+from urllib.parse import quote
+
+try:
+ from .heal_probe import Probe, canonical, compare_inventories, payload, require, save
+ from .heal_workload import Recorder, seed, verify_seed
+ from .healthy import Fleet, physical_content, validate_topology
+ from .inspector import verify as verify_inspector
+ from .matrix import select
+ from .fleet_node import PROCESS_FIELDS
+ from .disk_loss import quorum_plan as drive_quorum, member_state
+ from .node_outage import quorum_plan as node_quorum, put_ack, replica_census
+ from .mrf_records import verify_reference
+ from .mrf_replay import identity, pending_bindings, read_checkpoints, retirement, wait_replay, source_version
+ from .scanner_repair import JournalWindow, event_time
+ from .shards import files, map_target, verify_cohort
+ from .stale_rejoin import logical_metadata
+ from .versions import verify_quorum_contract
+except ImportError:
+ from heal_probe import Probe, canonical, compare_inventories, payload, require, save
+ from heal_workload import Recorder, seed, verify_seed
+ from healthy import Fleet, physical_content, validate_topology
+ from inspector import verify as verify_inspector
+ from matrix import select
+ from fleet_node import PROCESS_FIELDS
+ from disk_loss import quorum_plan as drive_quorum, member_state
+ from node_outage import quorum_plan as node_quorum, put_ack, replica_census
+ from mrf_records import verify_reference
+ from mrf_replay import identity, pending_bindings, read_checkpoints, retirement, wait_replay, source_version
+ from scanner_repair import JournalWindow, event_time
+ from shards import files, map_target, verify_cohort
+ from stale_rejoin import logical_metadata
+ from versions import verify_quorum_contract
+
+
+CASE_ID = "heal.disk-and-node-flapping"
+BRANCHES = ("drive-flap-3", "node-flap-3", "interrupt-rebuild-again")
+SIZE = 1024**2 + 73
+PROTOCOL = {"cycles": 3, "uniqueACKsPerCycle": [16, 16, 64], "foregroundConcurrency": 4,
+ "faultWindowSeconds": 120, "persistenceSeconds": 30, "replaySeconds": 240,
+ "partialWindowSeconds": 90, "branchDeadlineSeconds": 1200,
+ "extraThirdCycleInterruption": 1, "maxActiveTasks": 4, "maxQueuedTasks": 512,
+ "maxAcceptedTasksPerACKPerOwner": 64, "maxSystemTasksPerOwner": 256,
+ "maxFDs": 4096, "maxRssBytes": 4 * 1024**3,
+ "maxSampledCpuCores": 8, "maxForegroundErrorRate": 0,
+ "adminCompensationTasks": 0, "backupRestorationUsed": False}
+
+
+def same_process(before, after):
+ require(all(before.get(name) == after.get(name) for name in PROCESS_FIELDS),
+ "flapping changed an unrelated process or a live-gated service birth")
+
+
+def node_return(before, after):
+ require(before.get("pid", 0) > 1 and after.get("pid", 0) > 1
+ and (before["pid"], before["startTicks"]) != (after["pid"], after["startTicks"])
+ and all(before.get(name) == after.get(name) for name in
+ ("bootId", "unit", "executable", "binarySha256", "commandSha256", "environmentFileSha256", "settings")),
+ "node flap did not restart the exact original unit, candidate, boot and runtime")
+
+
+def untouched(before, after, members):
+ left, right = files(before), files(after)
+ require({node: value["formats"] for node, value in before.items()} ==
+ {node: value["formats"] for node, value in after.items()}, "flap changed original disk UUIDs or set slots")
+ require(all(right.get(key) == value for key, value in left.items()),
+ "flap changed an original acknowledged shard or control")
+ require(all(key[:2] not in members for key in set(right) - set(left)),
+ "offline target received a new ACK shard while inaccessible")
+ return {"originalShardAndControlBytesUnchanged": True, "offlineMembersReceivedNoACKFiles": True}
+
+
+def replay_progress(scene, authorities, bucket, obligations, members):
+ """Observe exact original member roles; this is not final streaming verification."""
+ validate_topology(scene, "H2")
+ require(members and len(members) in {1, 4}, "flap progress requires the exact original lost members")
+ current, original, complete = files(scene), files(authorities), []
+ layouts = {(node, row["disk"]): row["value"]["xl"] for node, value in scene.items() for row in value["formats"]}
+ require(members <= set(layouts), "flap target members escaped the original topology")
+ for item in obligations:
+ require(item["bucket"] == bucket and item["versionId"] == "null", "flap progress crossed an ACK identity")
+ path = f"{bucket}/{item['key']}/xl.meta"
+ sources = [entry["metadata"] for (node, disk, name), entry in original.items()
+ if name == path and (node, disk) not in members and entry.get("metadata")]
+ require(len(sources) == 16 - len(members) and
+ all(canonical(logical_metadata(value)) == canonical(logical_metadata(sources[0])) for value in sources),
+ "flap has no complete surviving ACK authority")
+ authority = logical_metadata(sources[0])
+ versions = authority.get("versions", [])
+ require(len(versions) == 1 and versions[0].get("versionId") == "null"
+ and versions[0].get("isLatest") is True and versions[0].get("deleted") is False,
+ "flap authority is a different version or marker")
+ restored = 0
+ for member in members:
+ metadata = current.get((*member, path), {}).get("metadata")
+ if metadata is None: continue
+ require(canonical(logical_metadata(metadata)) == canonical(authority), "flap replay changed ACK metadata authority")
+ version = metadata["versions"][0]
+ erasure, layout = version["erasure"], layouts[member]
+ slot = layout["sets"][0].index(layout["this"])
+ require(type(erasure.get("index")) is int and erasure["index"] == erasure["distribution"][slot]
+ and version.get("role") == ("data" if erasure["index"] <= erasure["data_blocks"] else "parity"),
+ "flap replay put an ACK on the wrong original set role")
+ parts = version.get("parts", [])
+ require(version.get("inline") is None and version.get("dataDir") and len(parts) == 1
+ and parts[0].get("number") == 1 and parts[0].get("size") == parts[0].get("actualSize") > 1024**2,
+ "flap progress requires one actual external plaintext ACK part")
+ part = current.get((*member, f"{bucket}/{item['key']}/{version['dataDir']}/part.1"))
+ if part is not None:
+ require(type(part.get("size")) is int and part["size"] > 32
+ and re.fullmatch(r"[0-9a-f]{64}", part.get("sha256", "")), "flap replay lacks an actual complete part identity")
+ restored += 1
+ if restored == len(members): complete.append(list(identity(item)))
+ return {"completeACKs": complete, "pendingACKs": len(obligations) - len(complete),
+ "partial": 0 < len(complete) < len(obligations), "finalStreamingProofClaimed": False}
+
+
+def retained_pending(checkpoints, obligations, bindings, pending):
+ require(pending and pending <= {identity(item) for item in obligations}, "interruption has no exact incomplete ACKs")
+ for wanted in pending:
+ for owner in bindings[wanted]:
+ value = checkpoints[owner["node"]]
+ rows = [row for row in value["intents"] if identity(row) == wanted]
+ require(value["present"] and value["owner"] == owner["checkpointOwner"]
+ and value["sequence"] >= owner["sequence"] and len(rows) == 1
+ and rows[0]["lifecycle"]["responsibility_id"] == owner["responsibilityId"]
+ and rows[0]["lifecycle"]["source_bucket_incarnation_id"] == owner["bucketIncarnationId"]
+ and rows[0]["lifecycle"]["state"] == {"state": "active"}
+ and rows[0]["lifecycle"].get("last_operator_acceptance") is None,
+ "incomplete interrupted ACK was retired, rebound, rolled back or operator-accepted")
+ return {"pendingOriginalResponsibilities": len(pending), "ownerIncarnationAndResponsibilityStable": True}
+
+
+def bounded_admissions(events, obligations):
+ lookup = {(item["bucket"], item["key"], item["versionId"]): identity(item) for item in obligations}
+ tokens, counts, system_counts = {}, {}, {}
+ for node, rows in events.items():
+ accepted = {row.get("request_id") for row in rows if row.get("target") == "rustfs::heal::manager"
+ and row.get("message") == "Heal queue request accepted" and row.get("source") == "mrf" and row.get("result") == "accepted"}
+ for row in rows:
+ if row.get("target") != "rustfs::heal::task" or row.get("message") != "Heal object started": continue
+ wanted = (row.get("bucket"), row.get("object"), "null")
+ require((wanted in lookup or wanted[0] == ".rustfs.sys" and isinstance(wanted[1], str) and wanted[1])
+ and "version_id" in row and source_version(row["version_id"], "null"),
+ "flapping source task escaped owned ACKs or the internal system bucket")
+ bound = lookup[wanted] if wanted in lookup else (*wanted, "internal-system")
+ token = row.get("task_id")
+ require(isinstance(token, str) and token in accepted and event_time(row) is not None,
+ "flapping task has no actual accepted MRF admission")
+ previous = tokens.get(token)
+ require(previous is None or previous == (node, bound), "flap task token changed owner or ACK identity")
+ if previous is None:
+ tokens[token] = (node, bound)
+ key = node, bound
+ counts[key] = counts.get(key, 0) + 1
+ require(counts[key] <= PROTOCOL["maxAcceptedTasksPerACKPerOwner"], "flapping exceeded its registered retry/task budget")
+ if wanted not in lookup:
+ system_counts[node] = system_counts.get(node, 0) + 1
+ require(system_counts[node] <= PROTOCOL["maxSystemTasksPerOwner"], "flapping exceeded its registered internal-system task budget")
+ return {"distinctAcceptedTaskIds": len(tokens), "maxTasksForOneACKOwner": max(counts.values(), default=0),
+ "internalSystemTasks": sum(system_counts.values()), "internalSystemTasksCertifyOwnedACKs": False,
+ "duplicateTokensCountedAsIndependentRepairs": False}
+
+
+class Budget:
+ def __init__(self, fleet, binding):
+ self.fleet, self.binding = fleet, binding
+ self.deadline = time.monotonic() + PROTOCOL["branchDeadlineSeconds"]
+ self.previous = {}
+ self.samples = 0
+
+ def check(self):
+ require(time.monotonic() < self.deadline, "flapping exceeded its preregistered total deadline")
+
+ def sample(self, nodes):
+ self.check()
+ for node in nodes:
+ value = self.fleet.record("flap-process-resource-sample", {"node": node, **self.fleet.remote(node, "process-metrics")})
+ require(value.get("readOnly") is True and type(value.get("fileDescriptors")) is int
+ and 0 < value["fileDescriptors"] <= PROTOCOL["maxFDs"]
+ and type(value.get("rssBytes")) is int and 0 < value["rssBytes"] <= PROTOCOL["maxRssBytes"],
+ "flapping exceeded its frozen FD/RSS resource budget")
+ previous = self.previous.get(node)
+ if previous is not None:
+ if all(previous["identity"].get(name) == value["identity"].get(name) for name in PROCESS_FIELDS):
+ elapsed = (value["monotonicNs"] - previous["monotonicNs"]) / 1e9
+ require(elapsed > 0 and type(value["clockTicksPerSecond"]) is int and value["clockTicksPerSecond"] > 0
+ and value["clockTicksPerSecond"] == previous["clockTicksPerSecond"]
+ and value["cpuTicks"] >= previous["cpuTicks"] and value["clockDomain"] == previous["clockDomain"],
+ "flapping resource clock or counters regressed")
+ cores = (value["cpuTicks"] - previous["cpuTicks"]) / value["clockTicksPerSecond"] / elapsed
+ self.fleet.record("flap-CPU-interval", {"node": node, "cpuCores": cores, "elapsedSeconds": elapsed})
+ require(cores <= PROTOCOL["maxSampledCpuCores"], "flapping exceeded its frozen CPU budget")
+ else:
+ require(node == self.fleet.nodes[-1], "flapping changed an unrelated peer process")
+ node_return(previous["identity"], value["identity"])
+ self.previous[node] = value
+ endpoint = "node" + str(self.fleet.nodes.index(node) + 1)
+ observed = self.fleet.record("flap-queue-resource-sample", Probe(self.binding, endpoint).admin(
+ "/rustfs/admin/v3/background-heal/status", [], b""))
+ require(observed.get("httpStatus") == 200 and isinstance(observed.get("response"), dict), "flap queue status unavailable")
+ body = observed["response"]
+ require(type(body.get("healActiveTasks")) is int and 0 <= body["healActiveTasks"] <= PROTOCOL["maxActiveTasks"]
+ and type(body.get("healQueueLength")) is int and 0 <= body["healQueueLength"] <= PROTOCOL["maxQueuedTasks"],
+ "flapping exceeded its registered active/queued task budget")
+ self.samples += 1
+
+
+def foreground(recorder, bucket, cycle, expected, deadline):
+ count = PROTOCOL["uniqueACKsPerCycle"][cycle - 1]
+ def put(index):
+ require(time.monotonic() < deadline, "foreground admission exceeded the registered offline window")
+ key = f"flap/cycle-{cycle}/nonce-{index:03d}.bin"
+ require(not any(entry["key"] == key for entry in expected), "flap reused a previous ACK key")
+ body = payload(f"{bucket}/{key}/0", SIZE + index)
+ ack = recorder.call("put_object", {"Bucket": bucket, "Key": key,
+ "Metadata": {"heal-fixture": "DB-DL", "cycle": str(cycle), "nonce": str(index)},
+ "Tagging": "suite=heal&fixture=flap"}, body, admission_deadline=deadline)
+ require(ack.get("ResponseMetadata", {}).get("HTTPStatusCode") == 200, "flap PUT lacks its explicit commit ACK")
+ recorder.call("head_object", {"Bucket": bucket, "Key": key}, admission_deadline=deadline)
+ return key, body, ack
+ def control_read():
+ require(time.monotonic() < deadline, "control admission exceeded the registered offline window")
+ entry = expected[0]
+ observed = recorder.probe.s3("get_object", {"Bucket": bucket, "Key": entry["key"]})
+ save(recorder.output / f"cycle-{cycle}-degraded-control-get.json", observed)
+ require(observed.get("acknowledged") is True and observed.get("payload") == entry["payload"], "degraded flap control GET differs from ACK")
+ recorder.call("list_objects_v2", {"Bucket": bucket, "Prefix": f"flap/cycle-{cycle}/"}, admission_deadline=deadline)
+ with ThreadPoolExecutor(max_workers=PROTOCOL["foregroundConcurrency"]) as pool:
+ reads = pool.submit(control_read)
+ writes = [pool.submit(put, index) for index in range(count)]
+ rows = [future.result() for future in writes]
+ reads.result()
+ manifest, obligations = copy.deepcopy(expected), []
+ for key, body, ack in rows:
+ manifest = put_ack(manifest, key, body, ack, "DB")
+ obligations.append({"bucket": bucket, "key": key, "versionId": "null", "poolIndex": 0, "setIndex": 0,
+ "kind": 3, "authoritativelyAbsentRequired": False})
+ return manifest, obligations
+
+
+def refuse_endpoint(binding):
+ try:
+ with request.build_opener(request.ProxyHandler({})).open(binding["endpoints"]["node4"] + "/health/ready", timeout=3):
+ raise ValueError("flapped node still accepts endpoint requests")
+ except URLError as exc:
+ require(isinstance(exc.reason, ConnectionRefusedError), "node flap has no exact connection refusal")
+ return {"connectionRefused": True}
+
+
+class MemberFault:
+ def __init__(self, fleet, binding, target, drive, budget):
+ self.fleet, self.binding, self.target, self.drive, self.budget = fleet, binding, target, drive, budget
+ self.node = fleet.nodes[-1]
+ self.baseline = None
+ self.processes = None
+
+ def inject(self):
+ self.processes = {node: self.fleet.remote(node, "process") for node in self.fleet.nodes}
+ self.fleet.record("flap-processes-before-loss", self.processes)
+ if self.drive:
+ self.fleet.drive_gates.add(self.node)
+ self.fleet.record("flap-live-gate-arm", self.fleet.remote(self.node, "drive-gate-arm-live",
+ disk=self.target["disk"], expected=self.processes[self.node]))
+ self.fleet.record("flap-live-gate-active", self.fleet.remote(self.node, "drive-gate-activate"))
+ else:
+ stopped = self.fleet.record("flap-owned-node-stop", self.fleet.remote(self.node, "stop"))
+ require(stopped.get("quiescent") is True and stopped.get("service", {}).get("MainPID") == "0", "flap target unit did not stop")
+ return self.witness()
+
+ def witness(self):
+ self.budget.check()
+ if not self.drive:
+ current = self.fleet.record("flap-stopped-process", self.fleet.remote(self.node, "process"))
+ require(current["pid"] == 0 and current["bootId"] == self.processes[self.node]["bootId"], "flap target stopped witness changed")
+ return self.fleet.record("flap-endpoint-refusal", refuse_endpoint(self.binding))
+ current = self.fleet.remote(self.node, "process")
+ same_process(self.processes[self.node], current)
+ denial = self.fleet.record("flap-service-drive-refusal", self.fleet.remote(self.node, "drive-gate-probe"))
+ require(denial.get("errno") == 13 and denial.get("readProbe", {}).get("errno") == 13
+ and denial.get("dacBypassRemoved") is True and denial.get("disk") == self.target["disk"],
+ "drive flap lacks exact service read/write denial")
+ deadline = min(time.monotonic() + 90, self.budget.deadline)
+ while True:
+ observed = self.fleet.record("flap-admin-member-offline", Probe(self.binding).admin(
+ "/rustfs/admin/v3/info", [], b"", method="GET"))
+ try:
+ proof = member_state(observed, self.target, 16, self.baseline)
+ return self.fleet.record("flap-drive-double-witness", {"service": denial, "admin": proof})
+ except ValueError as exc:
+ if not str(exc).startswith("NOT_OBSERVED:"): raise
+ require(time.monotonic() < deadline, str(exc))
+ time.sleep(1)
+
+ def release(self):
+ if self.drive:
+ observed = self.fleet.record("flap-live-drive-rejoin", self.fleet.remote(self.node, "drive-gate-release-live"))
+ require(observed.get("sameOriginalProcess") is True and observed.get("serviceRestarted") is False,
+ "drive flap rejoin restarted its service")
+ self.fleet.drive_gates.remove(self.node)
+ else:
+ self.fleet.record("flap-original-node-rejoin", self.fleet.remote(self.node, "start"))
+ self.fleet.wait_ready()
+ returned = {node: self.fleet.remote(node, "process") for node in self.fleet.nodes}
+ for node in self.fleet.nodes:
+ if node == self.node and not self.drive: node_return(self.processes[node], returned[node])
+ else: same_process(self.processes[node], returned[node])
+ self.fleet.record("flap-process-continuity", returned)
+ if self.drive:
+ deadline = min(time.monotonic() + 90, self.budget.deadline)
+ while True:
+ observed = self.fleet.record("flap-admin-member-online", Probe(self.binding).admin(
+ "/rustfs/admin/v3/info", [], b"", method="GET"))
+ try:
+ online = member_state(observed, self.target, 16)
+ require(online == self.baseline, "flap original member returned with a different identity")
+ break
+ except ValueError:
+ require(time.monotonic() < deadline, "NOT_OBSERVED: original flapped drive did not return online")
+ time.sleep(1)
+
+
+def interrupt_rebuild(fleet, window, binding, bucket, obligations, bindings, authorities, members, fault, budget, after, all_obligations):
+ deadline = min(time.monotonic() + PROTOCOL["partialWindowSeconds"], budget.deadline)
+ while True:
+ budget.sample(fleet.nodes)
+ events = window.capture()
+ bounded_admissions(events, all_obligations)
+ scene = fleet.snapshot("flap-third-cycle-rebuild-window")
+ progress = fleet.record("flap-third-cycle-physical-progress", replay_progress(scene, authorities, bucket, obligations, members))
+ pending = {identity(item) for item in obligations} - {tuple(item) for item in progress["completeACKs"]}
+ active = None
+ if progress["partial"]:
+ for item in obligations:
+ if identity(item) not in pending: continue
+ for owner in bindings[identity(item)]:
+ endpoint = "node" + str(fleet.nodes.index(owner["node"]) + 1)
+ starts = [row for row in events[owner["node"]] if row.get("target") == "rustfs::heal::task"
+ and row.get("message") == "Heal object started" and row.get("bucket") == bucket
+ and row.get("object") == item["key"] and "version_id" in row and source_version(row["version_id"], "null")
+ and event_time(row) is not None and event_time(row) >= after]
+ for row in reversed(starts):
+ path = "/rustfs/admin/v3/heal/" + quote(bucket, safe="") + "/" + quote(item["key"], safe="/")
+ observed = fleet.record("flap-interrupted-task-running", Probe(binding, endpoint).admin(path,
+ [("clientToken", row["task_id"])], b""))
+ require(observed.get("httpStatus") == 200, "third-cycle original task lost its exact status")
+ if observed.get("response", {}).get("summary") == "running":
+ active = {"node": owner["node"], "taskId": row["task_id"], "ACK": list(identity(item))}
+ break
+ if active: break
+ if active: break
+ if active:
+ interrupted_at = time.monotonic()
+ fault.inject()
+ frozen = fleet.record("flap-third-cycle-offline-progress", replay_progress(
+ {node: fleet.remote(node, "snapshot", quiescent=node == fault.node) for node in fleet.nodes},
+ authorities, bucket, obligations, members))
+ require(frozen["partial"] and active["ACK"] not in frozen["completeACKs"],
+ "NOT_OBSERVED: actual rebuild completed before the third-cycle repeated loss")
+ pending = {identity(item) for item in obligations} - {tuple(item) for item in frozen["completeACKs"]}
+ retained = retained_pending(read_checkpoints(fleet, fleet.nodes[:-1], "flap-interrupted-durable-records"),
+ obligations, bindings, pending)
+ fault.witness()
+ fault.release()
+ require(time.monotonic() - interrupted_at <= PROTOCOL["faultWindowSeconds"],
+ "third-cycle repeated loss exceeded its registered window budget")
+ return {"runningOriginalTask": active, "physicalWindow": frozen, "durable": retained,
+ "extraInterruptionCount": 1, "fixedSleepUsedAsWindow": False}
+ require(time.monotonic() < deadline, "NOT_OBSERVED: no exact running MRF task with partial third-cycle physical reconstruction")
+ time.sleep(.1)
+
+
+def run_branch(args, branch, nodes):
+ directory = args.evidence / branch
+ directory.mkdir()
+ run_id = f"gh-{os.environ['GITHUB_RUN_ID']}-{os.environ.get('GITHUB_RUN_ATTEMPT', '1')}-h2"
+ fleet = Fleet(nodes, directory, run_id, args.tool_directory, runtime_profile="flapping")
+ candidate = json.loads(args.candidate.read_text())
+ tool = fleet.record("inspector-build", verify_inspector(args.tool_directory, candidate))
+ contract = verify_quorum_contract(tool, candidate)
+ fleet.record("candidate-durable-MRF-reference", verify_reference(candidate))
+ bucket = "rv-heal-" + hashlib.sha256(run_id.encode()).hexdigest()[:16]
+ access, secret = secrets.token_hex(16), secrets.token_hex(24)
+ os.environ.update(HEAL_ACCESS_KEY=access, HEAL_SECRET_KEY=secret)
+ binding = {"schemaVersion": 1, "runId": run_id, "jumpHostOnly": True, "exclusiveHealService": True,
+ "leaseExpiresAt": (datetime.now(timezone.utc) + timedelta(minutes=30)).isoformat(), "bucketPrefix": bucket,
+ "endpoints": {f"node{index}": f"http://rustfs-node{index}:9000" for index in range(1, 5)}, "defaultEndpoint": "node1",
+ "candidateCommit": candidate["source_sha"], "harnessCommit": args.testing_commit,
+ "candidatePackageSha256": candidate["package_sha256"], "candidateBinarySha256": args.binary_sha,
+ "rcBinarySha256": hashlib.sha256(args.rc_bin.read_bytes()).hexdigest(),
+ "maxInventoryObjects": 128, "maxInventoryBytes": 256 * 1024**2}
+ fleet.record("binding", binding)
+ fleet.record("preregistered-three-cycle-protocol", {**PROTOCOL, "branch": branch,
+ "lossKind": "one-original-drive" if branch == "drive-flap-3" else "one-original-node",
+ "thirdCycleExtraInterruptionRequired": branch == "interrupt-rebuild-again"})
+ probe, result = Probe(binding), {"topology": "H2", "branch": branch, "verdict": "RUNNING", "cleanup": "PENDING", "cycles": []}
+ try:
+ fleet.deploy(args.binary_sha, bucket, access, secret)
+ recorder = Recorder(probe, directory / "foreground-ACK-ledger")
+ expected = seed(recorder, bucket, "DB", 4, [SIZE])
+ fleet.record("original-S3-manifest", verify_seed(expected, probe.inventory(bucket, unversioned_current_tags=True)))
+ before = fleet.snapshot("original-physical-baseline")
+ fleet.record("topology-proof", validate_topology(before, "H2"))
+ require(all(not value["intents"] for value in read_checkpoints(fleet, nodes, "fresh-MRF-baseline").values()),
+ "fresh flapping service has preexisting durable responsibilities")
+ target = map_target(before, bucket, "objects/0001.bin", "data", 1)
+ target = {**target, **next(shard for shard in target["cohort"] if shard["node"] == nodes[-1])}
+ drive = branch == "drive-flap-3"
+ members = {(nodes[-1], target["disk"])} if drive else {(nodes[-1], disk) for disk in range(4)}
+ fleet.record("actual-loss-quorum-plan", drive_quorum(target, contract) if drive else node_quorum(target, nodes[-1], contract))
+ budget = Budget(fleet, binding)
+ fault = MemberFault(fleet, binding, target, drive, budget)
+ if drive:
+ fault.baseline = member_state(fleet.record("flap-original-admin-member", probe.admin(
+ "/rustfs/admin/v3/info", [], b"", method="GET")), target, 16)
+ all_obligations, all_bindings = [], {}
+ window = JournalWindow(fleet, "flapping-source-and-retry-window",
+ event_targets={"rustfs::heal::manager", "rustfs::heal::task"})
+ for cycle in range(1, PROTOCOL["cycles"] + 1):
+ budget.sample(nodes)
+ original = fleet.snapshot("cycle-original-physical")
+ started = time.monotonic()
+ offline = fault.inject()
+ online = nodes if drive else nodes[:-1]
+ budget.sample(online)
+ earliest_ms = int(time.time() * 1000) - 5000
+ expected, obligations = foreground(recorder, bucket, cycle, expected,
+ min(started + PROTOCOL["faultWindowSeconds"], budget.deadline))
+ all_obligations.extend(obligations)
+ deadline = min(time.monotonic() + PROTOCOL["persistenceSeconds"], budget.deadline)
+ while True:
+ try:
+ checkpoints = read_checkpoints(fleet, online, "flap-offline-committed-ACKs")
+ bindings = pending_bindings(checkpoints, obligations, earliest_ms)
+ break
+ except ValueError as exc:
+ if not str(exc).startswith("NOT_OBSERVED:"): raise
+ require(time.monotonic() < deadline, "flap durable persistence exceeded its budget: " + str(exc))
+ time.sleep(.2)
+ all_bindings.update(bindings)
+ fleet.record("cycle-durable-ACK-bindings", [{"identity": list(key), "owners": value} for key, value in bindings.items()])
+ fault.witness()
+ authorities = {node: fleet.remote(node, "snapshot", quiescent=not drive and node == nodes[-1]) for node in nodes}
+ fleet.record("cycle-offline-authorities", authorities)
+ fleet.record("cycle-original-control-proof", untouched(original, authorities, members))
+ require(time.monotonic() - started <= PROTOCOL["faultWindowSeconds"], "flap offline window exceeded its registered budget")
+ returned = datetime.now(timezone.utc)
+ fault.release()
+ interruption = None
+ if branch == "interrupt-rebuild-again" and cycle == 3:
+ interruption = interrupt_rebuild(fleet, window, binding, bucket, obligations, bindings, authorities,
+ members, fault, budget, returned, all_obligations)
+ def observe():
+ budget.sample(nodes)
+ bounded_admissions(window.capture(), all_obligations)
+ proofs, retired = wait_replay(fleet, window, binding, nodes, bucket, expected, obligations, bindings,
+ returned, original, authorities, [], observe=observe, deadline_limit=budget.deadline)
+ fleet.record("cycle-full-ACK-task-proofs", [{"identity": list(key), **value} for key, value in proofs.items()])
+ reconstructed = fleet.snapshot("cycle-complete-physical-before-target-GET")
+ cohorts = replica_census(reconstructed, bucket, expected, authorities)
+ for cohort in cohorts: verify_cohort(fleet, cohort, "cycle-full-streaming-proof")
+ inventory = fleet.record("cycle-complete-S3-manifest", probe.inventory(bucket, unversioned_current_tags=True))
+ fleet.record("cycle-ACK-content-proof", verify_seed(expected, inventory))
+ result["cycles"].append({"cycle": cycle, "offlineWitness": offline, "uniqueACKs": len(obligations),
+ "sourceTasks": len(proofs), "retirement": retired, "interruption": interruption,
+ "sameOriginalDiskUUIDs": True, "physicalAndStreamingBeforeTargetGET": True})
+ budget.sample(nodes)
+ fleet.stop()
+ reconstructed = fleet.snapshot("final-quiescent-physical", quiescent=True)
+ cohorts = replica_census(reconstructed, bucket, expected, before)
+ for cohort in cohorts: verify_cohort(fleet, cohort, "final-full-streaming-proof")
+ retired = retirement(read_checkpoints(fleet, nodes, "final-original-responsibility-retirement", quiescent=True), all_obligations, all_bindings)
+ admissions = bounded_admissions(window.capture(), all_obligations)
+ fleet.start()
+ for endpoint in binding["endpoints"]:
+ actual = fleet.record("final-full-peer-manifest", {"endpoint": endpoint,
+ **Probe(binding, endpoint).inventory(bucket, unversioned_current_tags=True)})
+ fleet.record("final-peer-ACK-proof", {**verify_seed(expected, actual), **compare_inventories(inventory, actual)})
+ fleet.stop()
+ require(physical_content(reconstructed) == physical_content(fleet.snapshot("final-after-S3-verification", quiescent=True)),
+ "target verification altered the frozen product reconstruction")
+ budget.check()
+ result.update(verdict="PASS", uniqueACKs=len(all_obligations), resourceSamples=budget.samples,
+ boundedAdmissions=admissions, durableRetirement=retired, fullPeerManifestEqual=True,
+ originalDiskUUIDsPreserved=True, adminCompensationTasks=0, backupRestorationUsed=False)
+ except (Exception, KeyboardInterrupt) as exc:
+ result.update(verdict="FAIL", failure={"type": type(exc).__name__, "message": str(exc)})
+ if fleet.reserved:
+ try:
+ fleet.stop()
+ fleet.snapshot("flapping-failure-physical", quiescent=True)
+ read_checkpoints(fleet, nodes, "flapping-failure-durable-records", quiescent=True)
+ except Exception as evidence_error:
+ result["failureEvidenceError"] = str(evidence_error)
+ finally:
+ result["cleanup"] = "PASS" if fleet.cleanup() else "FAIL"
+ fleet.record("branch-result", result)
+ return result
+
+
+def main():
+ def interrupted(signum, frame):
+ raise KeyboardInterrupt(f"Heal workflow interrupted by signal {signum}")
+ signal.signal(signal.SIGTERM, interrupted)
+ parser = argparse.ArgumentParser(description=__doc__)
+ for name in ("candidate", "rc-bin", "evidence", "tool-directory"):
+ parser.add_argument("--" + name, type=Path, required=True)
+ for name in ("testing-commit", "binary-sha"):
+ parser.add_argument("--" + name, required=True)
+ parser.add_argument("--case-id", choices=(CASE_ID,), required=True)
+ args = parser.parse_args()
+ nodes = os.environ.get("RUSTFS_NODES", "vm000 vm001 vm002 vm003").split()
+ require(len(nodes) >= 4 and len(set(nodes)) == len(nodes) and all(re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", node) for node in nodes),
+ "four current Heal nodes required")
+ require(re.fullmatch(r"[0-9a-f]{64}", args.binary_sha), "invalid candidate binary digest")
+ require(re.fullmatch(r"[1-9][0-9]*", os.environ.get("GITHUB_RUN_ID", "")), "workflow fleet identity required")
+ require(select(CASE_ID)[0]["variants"] == list(BRANCHES), "flapping executor omitted a source branch")
+ results = []
+ try:
+ for branch in BRANCHES:
+ results.append(run_branch(args, branch, nodes[:4]))
+ if results[-1]["cleanup"] != "PASS": break
+ finally:
+ complete = len(results) == len(BRANCHES) and all(item["verdict"] == "PASS" and item["cleanup"] == "PASS" for item in results)
+ save(args.evidence / "healthy-summary.json", {"caseId": CASE_ID, "results": results, "complete": complete,
+ "cleanup": "PASS" if results and all(item["cleanup"] == "PASS" for item in results) else "FAIL"})
+ require(complete, "B10 requires all three bounded H2 branches, actual third-cycle interruption and full cleanup")
+
+
+if __name__ == "__main__":
+ try:
+ main()
+ except Exception as exc:
+ print(f"Heal flapping validation failed: {exc}", file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_heal/fleet_node.py b/scripts/validation_heal/fleet_node.py
new file mode 100644
index 00000000..549e4fb8
--- /dev/null
+++ b/scripts/validation_heal/fleet_node.py
@@ -0,0 +1,2179 @@
+#!/usr/bin/env python3
+"""Run-owned Heal deployment and physical evidence on current auto-testing nodes.
+
+The runner sends this module and a structured request over SSH stdin. Paths,
+units and mounts are derived here; no shell program or fault path is accepted.
+"""
+
+from __future__ import annotations
+
+import hashlib
+import ipaddress
+import base64
+import json
+import os
+from datetime import datetime, timedelta, timezone
+from pathlib import Path
+import re
+import signal
+import shutil
+import socket
+import stat
+import subprocess
+import time
+import uuid
+
+
+MOUNTS = tuple(Path(f"/data/rustfs{index}") for index in range(1, 5))
+STATE_ROOT = Path("/var/lib/rustfs-auto-testing/heal")
+RUN_PATTERN = re.compile(r"gh-[1-9][0-9]*-[1-9][0-9]*-(?:h1|h2|h5)")
+BACKGROUND_CONFIG = {"RUSTFS_SCANNER_ENABLED": "false", "RUSTFS_HEAL_ENABLED": "true",
+ "RUSTFS_HEAL_AUTO_HEAL_ENABLE": "false", "RUSTFS_HEAL_MRF_ENABLE": "false",
+ "RUSTFS_SHARD_INTEGRITY_WRITE": "true", "RUSTFS_SHARD_INTEGRITY_FLEET_CONFIRMED": "true",
+ "RUSTFS_OBS_LOGGER_LEVEL": "info"}
+I05_BPFTOOL = Path('/usr/lib/linux-tools/7.0.0-1017-azure/bpftool')
+I05_BPFTOOL_SHA256 = 'aabc337b58169d62c42b3c8f1fccf605e5fc1e445a23feb0156638e9d9606991'
+
+
+def runtime_config(profile):
+ if profile == "lock-successor":
+ return {**runtime_config("lock-timeout"),
+ "RUSTFS_PUT_COMMIT_NAMESPACE_LOCK_ACQUIRE_TIMEOUT_MS": "60000"}
+ if profile == "lock-timeout":
+ logging_filter = "info,rustfs_ecstore::object_lock_diag=trace"
+ return {**BACKGROUND_CONFIG, "RUSTFS_OBJECT_LOCK_DIAG_ENABLE": "true",
+ # I05 needs streaming GETs to retain their lock through backpressure.
+ "RUSTFS_OBJECT_LOCK_OPTIMIZATION_ENABLE": "false",
+ "RUSTFS_OBJECT_LOCK_ACQUIRE_TIMEOUT": "1",
+ "RUSTFS_PUT_RENAME_EARLY_ACK_ENABLE": "false",
+ "RUSTFS_OBS_LOGGER_LEVEL": logging_filter, "RUST_LOG": logging_filter}
+ if profile == "frozen-race-legacy":
+ # The frozen G04/G05 fixture predates protected shard sidecars. Keep
+ # its exact file population and serial Heal scheduling explicit.
+ return {**BACKGROUND_CONFIG, "RUSTFS_SHARD_INTEGRITY_WRITE": "false",
+ "RUSTFS_SHARD_INTEGRITY_FLEET_CONFIRMED": "false",
+ "RUSTFS_PUT_RENAME_EARLY_ACK_ENABLE": "false",
+ "RUSTFS_HEAL_MAX_CONCURRENT_HEALS": "1", "RUST_LOG": "info"}
+ if profile in {"incarnation-checkpoint", "incarnation-mrf"}:
+ logging_filter = "info,rustfs::heal::manager=debug,rustfs::heal::task=debug,rustfs::heal::mrf=debug"
+ return {**BACKGROUND_CONFIG, "RUSTFS_HEAL_MRF_ENABLE": "true" if profile == "incarnation-mrf" else "false",
+ "AUTO_TESTING_C11_PHASE": "capture",
+ "RUSTFS_HEAL_PAGE_PARALLEL_ENABLE": "false", "RUSTFS_HEAL_MAX_CONCURRENT_HEALS": "1",
+ "RUSTFS_HEAL_MAX_CONCURRENT_PER_SET": "1", "RUSTFS_OBS_LOGGER_LEVEL": logging_filter,
+ "RUST_LOG": logging_filter}
+ require(profile in {"outcome-legacy", "outcome-grace", "queue-pressure", "orphan-preserve", "orphan-before", "orphan-after", "manual", "authority-split", "multi-pool", "bounded-capacity", "journal-bounded", "lock-quorum", "offline-replay", "durable-mrf", "mrf-crash", "flapping", "read-repair", "scanner", "disabled", "overlap", "root-restart", "stale-rejoin", "replacement", "replacement-target", "schema-refusal", "scope-isolation"}, "unsupported Heal runtime profile")
+ if profile == "queue-pressure":
+ logging_filter = "info,rustfs::heal::manager=debug,rustfs::heal::task=debug,rustfs::heal::mrf=debug"
+ return {**BACKGROUND_CONFIG, "RUSTFS_HEAL_MRF_ENABLE": "true",
+ "RUSTFS_OBS_LOGGER_LEVEL": logging_filter, "RUST_LOG": logging_filter,
+ "RUSTFS_HEAL_QUEUE_SIZE": "2", "RUSTFS_HEAL_MAX_CONCURRENT_HEALS": "1",
+ "RUSTFS_HEAL_MAX_CONCURRENT_PER_SET": "1", "RUSTFS_HEAL_PAGE_PARALLEL_ENABLE": "false",
+ "RUSTFS_HEAL_OVERLAP_POLICY": "merge", "RUSTFS_HEAL_LOW_PRIORITY_MERGE_ENABLE": "true",
+ "RUSTFS_HEAL_LOW_PRIORITY_DROP_WHEN_FULL": "true", "RUSTFS_HEAL_MRF_REPLAY_BATCH": "1"}
+ if profile in {"orphan-preserve", "orphan-before", "orphan-after", "outcome-grace"}:
+ return {**BACKGROUND_CONFIG, "RUSTFS_HEAL_DANGLING_DELETE_GRACE_SECS":
+ {"orphan-preserve": "0", "orphan-before": "300", "orphan-after": "30", "outcome-grace": "120"}[profile]}
+ if profile == "authority-split":
+ return {**BACKGROUND_CONFIG, "RUSTFS_STORAGE_CLASS_STANDARD": "EC:1"}
+ if profile == "multi-pool":
+ return {**BACKGROUND_CONFIG, "RUSTFS_ERASURE_SET_DRIVE_COUNT": "4", "RUSTFS_STORAGE_CLASS_STANDARD": "EC:2"}
+ if profile in {"bounded-capacity", "journal-bounded"}:
+ logging_filter = "info,rustfs::heal::manager=debug,rustfs::heal::task=debug,rustfs::heal::resume=debug"
+ return {**BACKGROUND_CONFIG, "RUSTFS_UNSAFE_BYPASS_DISK_CHECK": "true",
+ **({"RUSTFS_HEAL_PAGE_PARALLEL_ENABLE": "false", "RUSTFS_OBS_LOGGER_LEVEL": logging_filter,
+ "RUST_LOG": logging_filter}
+ if profile == "journal-bounded" else {})}
+ if profile == "lock-quorum":
+ return {**BACKGROUND_CONFIG, "RUSTFS_STORAGE_CLASS_STANDARD": "EC:8"}
+ if profile == "scope-isolation":
+ return {**BACKGROUND_CONFIG, "RUSTFS_ERASURE_SET_DRIVE_COUNT": "8"}
+ if profile in {"durable-mrf", "mrf-crash", "flapping"}:
+ # Keep actual MRF admission/start/terminal evidence without enabling
+ # unrelated HTTP/HTTP2 frame debugging on the high-volume fleet.
+ logging_filter = "info,rustfs::heal::manager=debug,rustfs::heal::task=debug,rustfs::heal::mrf=debug"
+ return {**BACKGROUND_CONFIG, "RUSTFS_HEAL_MRF_ENABLE": "true",
+ "RUSTFS_OBS_LOGGER_LEVEL": logging_filter, "RUST_LOG": logging_filter,
+ **({"RUSTFS_HEAL_MAX_CONCURRENT_HEALS": "1", "RUSTFS_HEAL_MAX_CONCURRENT_PER_SET": "1",
+ "RUSTFS_HEAL_MRF_REPLAY_BATCH": "1"} if profile in {"mrf-crash", "flapping"} else {})}
+ if profile in {"replacement", "replacement-target", "schema-refusal"}:
+ return {**BACKGROUND_CONFIG, "RUSTFS_HEAL_AUTO_HEAL_ENABLE": "true" if profile == "replacement" else "false", "RUSTFS_HEAL_INTERVAL_SECS": "10",
+ "RUSTFS_UNSAFE_BYPASS_DISK_CHECK": "true", "RUSTFS_OBS_LOGGER_LEVEL": "debug"}
+ if profile == "stale-rejoin":
+ return {**BACKGROUND_CONFIG, "RUSTFS_HEAL_DANGLING_DELETE_GRACE_SECS": "0", "RUSTFS_OBS_LOGGER_LEVEL": "debug"}
+ if profile == "root-restart":
+ return {**BACKGROUND_CONFIG, "RUSTFS_HEAL_PAGE_PARALLEL_ENABLE": "false"}
+ if profile == "overlap":
+ return {**BACKGROUND_CONFIG, "RUSTFS_HEAL_OVERLAP_POLICY": "merge",
+ "RUSTFS_HEAL_PAGE_PARALLEL_ENABLE": "false"}
+ if profile == "disabled":
+ return {**BACKGROUND_CONFIG, "RUSTFS_HEAL_ENABLED": "false"}
+ if profile == "read-repair":
+ # Admission evidence must survive journald's per-unit rate limit;
+ # global DEBUG floods it with unrelated HTTP/HTTP2 frame events.
+ logging_filter = "info,rustfs_ecstore=debug,rustfs::heal=debug"
+ return {**BACKGROUND_CONFIG, "RUSTFS_OBS_LOGGER_LEVEL": logging_filter, "RUST_LOG": logging_filter}
+ if profile == "scanner":
+ logging_filter = "info,rustfs::scanner=debug,rustfs::heal=debug"
+ return {**BACKGROUND_CONFIG, "RUSTFS_SCANNER_ENABLED": "true",
+ "RUSTFS_OBS_LOGGER_LEVEL": logging_filter, "RUST_LOG": logging_filter,
+ "RUSTFS_SCANNER_START_DELAY_SECS": "5", "RUSTFS_SCANNER_CYCLE": "5",
+ "RUSTFS_SCANNER_SPEED": "fastest", "RUSTFS_SCANNER_DELAY": "0",
+ "RUSTFS_SCANNER_BITROT_CYCLE_SECS": "0", "RUSTFS_SCANNER_DEEP_VERIFY_COOLDOWN_SECS": "0",
+ "RUSTFS_HEAL_OBJECT_SELECT_PROB": "1", "RUSTFS_DATA_USAGE_UPDATE_DIR_CYCLES": "1"}
+ return {**BACKGROUND_CONFIG, "RUSTFS_HEAL_MRF_ENABLE": "true" if profile == "offline-replay" else "false"}
+
+
+def runtime_settings(owner):
+ config = runtime_config(owner.get("runtimeProfile", "manual"))
+ if owner.get("runtimeProfile") in {"incarnation-checkpoint", "incarnation-mrf"}:
+ config["AUTO_TESTING_C11_PHASE"] = owner.get("incarnationPhase", "capture")
+ if owner.get("incarnationPhase") == "transition":
+ config.update(RUSTFS_HEAL_MRF_ENABLE="false")
+ if owner.get("runtimeProfile") == "outcome-legacy" and owner.get("outcomePhase") == "legacy":
+ config["RUSTFS_SHARD_INTEGRITY_WRITE"] = "false"
+ return config
+
+
+def runtime_configuration(state, owner):
+ env = checked(state / "service.env", state)
+ info = env.stat()
+ require(info.st_uid == 0 and info.st_nlink == 1 and stat.S_ISREG(info.st_mode) and info.st_size <= 65536,
+ "runtime configuration lost its bounded private ownership")
+ lines = env.read_text().splitlines()
+ require(all("=" in line for line in lines), "runtime configuration has an invalid entry")
+ values = dict(line.split("=", 1) for line in lines)
+ require(len(lines) == len(values) and all(values.get(key) == value for key, value in runtime_settings(owner).items()),
+ "runtime configuration drifted or its durable phase commit is incomplete")
+ return values
+
+
+def outcome_legacy_phase(request):
+ """One-way switch after a separately verified protected control task."""
+ require(set(request) == {"runId", "action"}, "J02 phase accepts no runtime override")
+ state, _, owner = owned(request["runId"])
+ stopped(owner)
+ require(owner.get("runtimeProfile") == "outcome-legacy"
+ and digest(checked(state / "rustfs", state)) == owner["binarySha256"], "J02 runtime or candidate differs")
+ fault = fault_module()
+ binding = {"runId": owner["runId"], "binarySha256": owner["binarySha256"], "phase": "legacy"}
+ with fault.directory(state) as fd:
+ info = os.stat("service.env", dir_fd=fd, follow_symlinks=False)
+ require(info.st_uid == 0 and not info.st_mode & 0o077, "J02 configuration lost its private ownership")
+ raw = fault.read_file(fd, "service.env", 65536)
+ lines = raw.decode().splitlines()
+ require(all("=" in line for line in lines), "J02 private configuration has an invalid entry")
+ values = dict(line.split("=", 1) for line in lines)
+ wanted = runtime_settings({**owner, "outcomePhase": "legacy"})
+ require(len(lines) == len(values) and values.get("RUSTFS_SHARD_INTEGRITY_WRITE") in {"true", "false"}
+ and all(values.get(key) == value for key, value in wanted.items() if key != "RUSTFS_SHARD_INTEGRITY_WRITE"),
+ "J02 configuration changed outside its write-mode switch")
+ values["RUSTFS_SHARD_INTEGRITY_WRITE"] = "false"
+ changed = ("\n".join(f"{key}={value}" for key, value in values.items()) + "\n").encode()
+ changed_sha = hashlib.sha256(changed).hexdigest()
+ try:
+ info = os.stat("J02-legacy-intent.json", dir_fd=fd, follow_symlinks=False)
+ require(info.st_uid == 0 and not info.st_mode & 0o077, "J02 phase intent lost its private ownership")
+ intent = json.loads(fault.read_file(fd, "J02-legacy-intent.json", 16384))
+ except FileNotFoundError:
+ require(owner.get("outcomePhase") is None, "J02 committed phase lacks its original intent")
+ runtime_configuration(state, owner)
+ intent = {**binding, "beforeEnvironmentSha256": hashlib.sha256(raw).hexdigest(),
+ "legacyEnvironmentSha256": changed_sha}
+ fault.new_file(fd, "J02-legacy-intent.json", fault.encode(intent))
+ require(set(intent) == {*binding, "beforeEnvironmentSha256", "legacyEnvironmentSha256"}
+ and all(intent.get(key) == value for key, value in binding.items())
+ and all(re.fullmatch(r"[0-9a-f]{64}", intent.get(key, "")) for key in
+ ("beforeEnvironmentSha256", "legacyEnvironmentSha256"))
+ and changed_sha == intent["legacyEnvironmentSha256"]
+ and hashlib.sha256(raw).hexdigest() in {intent["beforeEnvironmentSha256"], intent["legacyEnvironmentSha256"]},
+ "J02 phase configuration or intent differs")
+ require(owner.get("outcomePhase") is None or hashlib.sha256(raw).hexdigest() == intent["legacyEnvironmentSha256"],
+ "J02 committed write mode drifted")
+ if owner.get("outcomePhase") != "legacy":
+ try:
+ fault.new_file(fd, "J02-service.env.new", changed)
+ except FileExistsError:
+ require(fault.read_file(fd, "J02-service.env.new", 65536) == changed, "J02 staged configuration differs")
+ os.rename("J02-service.env.new", "service.env", src_dir_fd=fd, dst_dir_fd=fd)
+ os.fsync(fd)
+ phase = fault.encode({"runId": owner["runId"], "phase": "legacy"})
+ try:
+ fault.new_file(fd, "J02-phase.new", phase)
+ except FileExistsError:
+ require(fault.read_file(fd, "J02-phase.new", 16384) == phase, "J02 staged phase differs")
+ os.rename("J02-phase.new", "J02-phase.json", src_dir_fd=fd, dst_dir_fd=fd)
+ os.fsync(fd)
+ return {**binding, "oneWay": True, "candidateUnchanged": True, "credentialsDisclosed": False,
+ "otherRuntimeSettingsUnchanged": True}
+
+
+def incarnation_operation(request):
+ require(set(request) <= {"runId", "action", "family", "taskId", "bucketIncarnationId", "expectedRecords", "phase"},
+ "C11 adapter accepts no arbitrary paths, replacement bytes or commands")
+ state, roots, owner = owned(request["runId"])
+ stopped(owner)
+ require(owner.get("runtimeProfile") in {"incarnation-checkpoint", "incarnation-mrf"}
+ and digest(state / "rustfs") == owner["binarySha256"], "C11 replay runtime/candidate changed")
+ backend = globals()["incarnation_replay_backend"]
+ journal, fault = globals()["journal_fault_backend"], fault_module()
+ if request["action"] == "incarnation-phase":
+ require(set(request) == {"runId", "action", "phase"} and request["phase"] in {"transition", "replay"},
+ "C11 phase cannot change arbitrary environment settings")
+ current = owner.get("incarnationPhase", "capture")
+ require((current, request["phase"]) in {("capture", "transition"), ("transition", "replay")},
+ "C11 phase transition cannot refund an earlier replay window")
+ try:
+ with fault.directory(state) as fd:
+ detached = journal.decode_json(fault.read_file(fd, "C11-replay-detached.json", 16384))
+ installed = journal.decode_json(fault.read_file(fd, "C11-replay-installed.json", 16384)) if current == "transition" else None
+ require(isinstance(detached, dict), "C11 phase transaction binding is invalid")
+ params = {key: detached.get(key) for key in ("runId", "family", "taskId", "bucketIncarnationId")}
+ binding = backend.identity(owner, params, journal)
+ require(journal.same_json(detached, binding) and (current == "capture" or journal.same_json(installed, binding)),
+ "C11 phase transaction changed its original run/candidate/task/incarnation binding")
+ manifest = backend.archived(state, roots, owner, params, journal, fault)
+ records = backend.census(roots, binding, journal, fault)
+ require(all(not row["present"] for row in records) if current == "capture"
+ else journal.same_json(records, manifest["records"]),
+ "C11 phase runnable records differ from the completed original transaction")
+ except FileNotFoundError as exc:
+ raise ValueError("C11 phase requires a complete original record transaction") from exc
+ values = runtime_configuration(state, owner)
+ owner["incarnationPhase"] = request["phase"]
+ replacement = runtime_settings(owner)
+ env = state / "service.env"
+ values.update(replacement)
+ temporary = state / "C11-service.env.new"
+ with fault_module().directory(state) as fd:
+ fault_module().new_file(fd, temporary.name, ("\n".join(f"{key}={value}" for key, value in values.items()) + "\n").encode())
+ os.rename(temporary.name, env.name, src_dir_fd=fd, dst_dir_fd=fd)
+ os.fsync(fd)
+ with fault_module().directory(state) as fd:
+ fault_module().new_file(fd, "C11-phase.new", fault_module().encode({"runId": owner["runId"], "phase": request["phase"]}))
+ os.rename("C11-phase.new", "C11-phase.json", src_dir_fd=fd, dst_dir_fd=fd)
+ os.fsync(fd)
+ return {"previousPhase": current, "phase": request["phase"], "candidateUnchanged": True,
+ "workerLimit": replacement["RUSTFS_HEAL_MAX_CONCURRENT_HEALS"],
+ "mrfEnabled": replacement["RUSTFS_HEAL_MRF_ENABLE"]}
+ require("phase" not in request, "C11 record operation accepts no runtime override")
+ if request["action"] == "incarnation-census":
+ require(set(request) == {"runId", "action", "family", "taskId", "bucketIncarnationId"},
+ "C11 stopped census accepts only the original binding")
+ binding = backend.identity(owner, request, journal)
+ return {**binding, "readOnly": True, "stable": True, "records": backend.census(roots, binding, journal, fault)}
+ methods = {"incarnation-capture": backend.capture, "incarnation-install": backend.install,
+ "incarnation-finalize": backend.finalize}
+ require(request["action"] in methods, "unknown C11 record transaction")
+ if request["action"] == "incarnation-capture":
+ return backend.capture(state, roots, owner, request, journal, fault, globals()["mrf"], checked)
+ return methods[request["action"]](state, roots, owner, request, journal, fault)
+
+
+def require(condition, message):
+ if not condition:
+ raise ValueError(message)
+
+
+def command(*args, check=True):
+ result = subprocess.run(args, capture_output=True, text=True, timeout=90)
+ require(not check or result.returncode == 0, f"{args[0]} failed: {result.stderr[-1000:]}")
+ return result
+
+
+def checked(path, boundary):
+ require(path.is_absolute() and path.is_relative_to(boundary) and ".." not in path.parts,
+ "path is outside the run boundary")
+ for entry in (path, *path.parents):
+ require(not entry.is_symlink(), "symlink in run path")
+ require(path.resolve() == path, "noncanonical run path")
+ if boundary.exists():
+ for entry in (path, *path.parents):
+ if entry == boundary:
+ break
+ require(not entry.exists() or entry.stat().st_dev == boundary.stat().st_dev,
+ "nested mount in run path")
+ return path
+
+
+def digest(path):
+ info = path.lstat()
+ require(stat.S_ISREG(info.st_mode) and info.st_nlink == 1 and info.st_size <= 2 * 1024**3,
+ "expected bounded regular file")
+ result = hashlib.sha256()
+ with path.open("rb") as stream:
+ while chunk := stream.read(1024**2):
+ result.update(chunk)
+ return result.hexdigest()
+
+
+def paths(run_id):
+ require(bool(RUN_PATTERN.fullmatch(run_id)), "invalid auto-testing Heal run ID")
+ state = checked(STATE_ROOT / run_id, STATE_ROOT)
+ roots = tuple(checked(mount / "heal-validation" / run_id, mount) for mount in MOUNTS)
+ return state, roots
+
+
+def unit(run_id):
+ paths(run_id)
+ return "rustfs-validation-heal-" + run_id + ".service"
+
+
+def mounts():
+ observed = []
+ for mount in MOUNTS:
+ checked(mount, mount)
+ entries = json.loads(command("findmnt", "--json", "--mountpoint", str(mount), "--output",
+ "TARGET,SOURCE,FSTYPE,UUID,MAJ:MIN,OPTIONS").stdout).get("filesystems", [])
+ require(len(entries) == 1, "four explicit disk mountpoints are required")
+ value = entries[0]
+ require(value["target"] == str(mount) and value.get("uuid") and value.get("maj:min")
+ and "rw" in value["options"].split(","), "disk mount identity or write capability is missing")
+ observed.append(value)
+ require(len({value["uuid"] for value in observed}) == 4
+ and len({value["maj:min"] for value in observed}) == 4,
+ "four independent drives are required; directories on one drive do not qualify")
+ return observed
+
+
+def _unescape_mountinfo(value):
+ return (value.replace("\\040", " ").replace("\\011", "\t")
+ .replace("\\012", "\n").replace("\\134", "\\"))
+
+
+def _mountinfo_entries():
+ entries = []
+ for line in Path("/proc/self/mountinfo").read_text().splitlines():
+ fields = line.split()
+ if len(fields) < 10 or "-" not in fields:
+ continue
+ separator = fields.index("-")
+ if separator + 2 >= len(fields):
+ continue
+ entries.append({
+ "mountId": fields[0], "device": fields[2], "root": _unescape_mountinfo(fields[3]),
+ "target": _unescape_mountinfo(fields[4]), "options": fields[5],
+ "fstype": fields[separator + 1], "source": _unescape_mountinfo(fields[separator + 2]),
+ })
+ return entries
+
+
+def _physical_device_ids(path):
+ """Resolve leaf block identities in the same failure-domain shape as RustFS."""
+ info = path.stat()
+ device = f"{os.major(info.st_dev)}:{os.minor(info.st_dev)}"
+ sysfs = Path("/sys/dev/block") / device
+ if not sysfs.exists():
+ return [device]
+
+ def leaves(node):
+ slaves = node / "slaves"
+ entries = sorted(slaves.iterdir()) if slaves.is_dir() else []
+ if entries:
+ result = []
+ for entry in entries:
+ result.extend(leaves(entry.resolve(strict=True)))
+ return result
+ if (node / "partition").exists() and node.parent.name:
+ return [node.parent.name]
+ return [node.name]
+
+ return sorted(set(leaves(sysfs.resolve(strict=True))))
+
+
+def _replacement_mount_identity(path):
+ """Inspect one endpoint without creating, remounting or switching anything."""
+ require(path.is_absolute() and not any(part == ".." for part in path.parts),
+ "replacement endpoint path is invalid")
+ mount_root = next((mount for mount in MOUNTS if path.is_relative_to(mount)), None)
+ if mount_root is not None:
+ for entry in (path, *path.parents):
+ require(not entry.is_symlink(), "symlink in replacement endpoint path")
+ if entry == mount_root:
+ break
+ probe = path
+ while not probe.exists() and probe != probe.parent:
+ probe = probe.parent
+ require(probe.exists() and not probe.is_symlink(), "replacement endpoint has no safe existing ancestor")
+ canonical_probe = probe.resolve(strict=True)
+ canonical = path.resolve(strict=False)
+ info = canonical_probe.lstat()
+ require(stat.S_ISDIR(info.st_mode) and not stat.S_ISLNK(info.st_mode),
+ "replacement endpoint or nearest existing ancestor must be a canonical directory")
+ entries = _mountinfo_entries()
+ exact = [item for item in entries if item["target"] == str(canonical)] if path.exists() else []
+ mount = exact[-1] if len(exact) == 1 else None
+ mount_id = mount["mountId"] if mount else None
+ ancestors = [item for item in entries if canonical_probe == Path(item["target"])
+ or Path(item["target"]) in canonical_probe.parents]
+ nearest_mount = max(ancestors, key=lambda item: len(Path(item["target"]).parts), default=None)
+ physical = _physical_device_ids(canonical_probe)
+ return {
+ "endpoint": str(path),
+ "canonicalPath": str(canonical),
+ "endpointExists": path.exists(),
+ "mounted": mount is not None and mount_id is not None,
+ "mount": mount,
+ "mountId": mount_id,
+ "filesystemIdentity": None if mount_id is None else f"{mount_id}:{info.st_dev}:{info.st_ino}",
+ "device": f"{os.major(info.st_dev)}:{os.minor(info.st_dev)}",
+ "physicalDeviceIds": physical,
+ "nearestExistingAncestor": str(canonical_probe),
+ "ancestorMount": nearest_mount,
+ "writable": os.access(canonical_probe, os.W_OK),
+ }
+
+
+def replacement_admission_preflight(request):
+ """Read-only replacement admission proof; fail closed before service start or seed."""
+ require(set(request) == {"runId", "action"}, "replacement admission accepts no paths or mutation options")
+ _, roots = paths(request["runId"])
+ endpoints = [_replacement_mount_identity(root / "data") for root in roots]
+ root_identity = _replacement_mount_identity(Path("/"))
+ root_devices = set(root_identity["physicalDeviceIds"])
+ endpoint_devices = [set(item["physicalDeviceIds"]) for item in endpoints]
+ sibling_overlap = [sorted(left & right) for index, left in enumerate(endpoint_devices)
+ for right in endpoint_devices[index + 1:] if left & right]
+ root_overlap = [sorted(devices & root_devices) for devices in endpoint_devices if devices & root_devices]
+ exact_mounts = all(item["mounted"] for item in endpoints)
+ distinct_filesystems = len({item["filesystemIdentity"] for item in endpoints}) == len(endpoints)
+ adapter = {
+ "registered": False,
+ "reason": "auto-testing reserves ordinary run directories; no reviewed endpoint mount-switch adapter is installed",
+ }
+ result = {
+ "runId": request["runId"],
+ "readOnly": True,
+ "mutated": False,
+ "canonicalPassClaimed": False,
+ "endpoints": endpoints,
+ "root": root_identity,
+ "mountSwitchAdapter": adapter,
+ "checks": {
+ "exactEndpointMounts": exact_mounts,
+ "mountIdsPresent": all(item["mountId"] is not None for item in endpoints),
+ "filesystemIdentitiesDistinct": distinct_filesystems,
+ "endpointPhysicalDevicesDistinct": not sibling_overlap,
+ "endpointDevicesSeparateFromRoot": not root_overlap,
+ "mountSwitchAdapterReviewed": False,
+ },
+ "admitted": bool(exact_mounts and distinct_filesystems and not sibling_overlap
+ and not root_overlap and adapter["registered"] is True),
+ "reason": None,
+ }
+ if not result["admitted"]:
+ result["reason"] = "replacement cases require exact endpoint mount leases and a reviewed mount-switch adapter; run endpoints are ordinary directories"
+ return result
+
+
+def environment_service_state():
+ """Observe fixed service/port names; never stop or certify a clean scene."""
+ fields = {'LoadState', 'ActiveState', 'SubState', 'MainPID'}
+
+ def service():
+ raw = command('systemctl', 'show', 'rustfs.service', '--no-pager',
+ '--property=LoadState,ActiveState,SubState,MainPID', check=False)
+ values = dict(line.split('=', 1) for line in raw.stdout.splitlines() if '=' in line)
+ require(set(values) == fields and re.fullmatch(r'[0-9]+', values['MainPID']),
+ 'environment service observation is incomplete')
+ require(raw.returncode == 0 or values['LoadState'] == 'not-found',
+ 'environment service observation failed')
+ return {**values, 'MainPID': int(values['MainPID'])}
+
+ def validation_units():
+ raw = command('systemctl', 'list-units', '--all', '--type=service', '--plain',
+ '--no-legend', '--no-pager', 'rustfs-validation-heal-*.service')
+ rows = []
+ for line in raw.stdout.splitlines():
+ parts = line.split(None, 4)
+ require(len(parts) >= 4 and parts[0].startswith('rustfs-validation-heal-')
+ and parts[0].endswith('.service'), 'ambiguous validation service inventory')
+ run_id = parts[0][len('rustfs-validation-heal-'):-len('.service')]
+ require(RUN_PATTERN.fullmatch(run_id), 'unrecognized validation service requires audit')
+ rows.append(dict(zip(('unit', 'load', 'active', 'sub'), parts[:4])))
+ require(len(rows) <= 256 and len({row['unit'] for row in rows}) == len(rows),
+ 'validation service inventory is unbounded or duplicated')
+ return sorted(rows, key=lambda row: row['unit'])
+
+ def listeners():
+ raw = command('ss', '-H', '-ltnp', '( sport = :9000 )')
+ rows = []
+ for line in raw.stdout.splitlines():
+ parts = line.split(None, 5)
+ require(len(parts) >= 5 and parts[0] == 'LISTEN' and parts[3].endswith(':9000'),
+ 'ambiguous port 9000 listener')
+ rows.append({'localAddress': parts[3],
+ 'pids': sorted({int(pid) for pid in re.findall(r'\bpid=([1-9][0-9]*)\b', line)})})
+ require(len(rows) <= 256, 'port listener inventory is unbounded')
+ return sorted(rows, key=lambda row: (row['localAddress'], row['pids']))
+
+ before = (service(), validation_units(), listeners())
+ after = (service(), validation_units(), listeners())
+ require(before == after, 'environment runtime changed during read-only observation')
+ standard, units, port = after
+ return {'standardService': standard, 'loadedValidationUnits': units, 'port9000Listeners': port,
+ 'stableObservation': True, 'inspectionOnly': True, 'mutated': False,
+ 'cleanStateCertified': False, 'globalWorkflowIdleCertified': False,
+ 'retainedSceneRecoveryCertified': False}
+
+
+def bpf_tool_inventory():
+ """Read the wrapper and installed tools without installing or attaching BPF."""
+ selected = shutil.which('bpftool')
+ candidates = sorted(Path('/usr/lib/linux-tools').glob('*/bpftool'))
+ require(len(candidates) <= 64, 'BPF tool inventory exceeds its bounded installed paths')
+ tools = []
+ for path in ([Path(selected)] if selected else []) + candidates:
+ require(path.is_absolute(), 'BPF tool inventory requires absolute installed paths')
+ resolved = path.resolve(strict=True)
+ require(resolved.is_file() and resolved.stat().st_uid == 0
+ and not resolved.stat().st_mode & 0o022, 'BPF tool executable is not root-owned and protected')
+ version = command(str(path), '--version', check=False)
+ query = command(str(path), '-j', 'cgroup', 'show', '/sys/fs/cgroup', check=False)
+ try:
+ rows = json.loads(query.stdout) if query.returncode == 0 else None
+ except ValueError:
+ rows = None
+ tools.append({'path': str(path), 'resolvedPath': str(resolved), 'sha256': digest(resolved),
+ 'selectedByPath': str(path) == selected,
+ 'version': {'returncode': version.returncode, 'stdout': version.stdout[:4096], 'stderr': version.stderr[:4096]},
+ 'rootCgroupQuery': {'returncode': query.returncode, 'jsonListObserved': isinstance(rows, list),
+ 'stdout': query.stdout[:4096],
+ 'stderr': query.stderr[:4096]},
+ 'serviceAttachmentsVerified': False})
+ return {'kernelRelease': os.uname().release, 'selectedPath': selected, 'tools': tools,
+ 'inspectionOnly': True, 'mutated': False, 'canonicalPassClaimed': False}
+
+
+def environment_capabilities(request):
+ """Read existing mounts and kernel interfaces without reserving a run."""
+ paths(request['runId'])
+ observed = []
+ for number in range(1, 9):
+ mount = Path(f'/data/rustfs{number}')
+ row = {'target': str(mount), 'mounted': False, 'independentBlockDeviceVerified': False}
+ raw = command('findmnt', '--json', '--mountpoint', str(mount), '--output',
+ 'TARGET,SOURCE,FSTYPE,UUID,MAJ:MIN,OPTIONS', check=False)
+ require(raw.returncode in {0, 1}, 'environment mount inventory failed')
+ if raw.returncode == 0:
+ values = json.loads(raw.stdout).get('filesystems', [])
+ require(len(values) == 1 and values[0].get('target') == str(mount), 'environment inventory has an ambiguous mount')
+ value = values[0]
+ row.update(mounted=True, mount=value, availableBytes=shutil.disk_usage(mount).free)
+ device = value.get('maj:min', '')
+ require(re.fullmatch(r'[0-9]+:[0-9]+', device), 'environment inventory omitted the actual device identity')
+ link = Path('/sys/dev/block') / device
+ if link.exists():
+ block = link.resolve(strict=True)
+ if (block / 'partition').exists(): block = block.parent
+ # Stacked/virtual devices need a separately reviewed fault
+ # backend; directory or partition counts cannot prove H4.
+ stacked = (block / 'slaves').is_dir() and any((block / 'slaves').iterdir())
+ physical = (block / 'dev').read_text().strip()
+ require(re.fullmatch(r'[0-9]+:[0-9]+', physical), 'environment physical device is ambiguous')
+ row.update(physicalDevice=physical, blockName=block.name,
+ independentBlockDeviceVerified=not stacked and not block.name.startswith(('loop', 'dm-', 'ram')))
+ row['writableMount'] = 'rw' in value.get('options', '').split(',')
+ observed.append(row)
+ binary = Path('/usr/bin/rustfs')
+ return {'hostname': socket.gethostname(), 'mounts': observed, 'inspectionOnly': True, 'mutated': False,
+ 'runtime': environment_service_state(),
+ 'kernel': {'pidfd': hasattr(os, 'pidfd_open'), 'cgroupV2': Path('/sys/fs/cgroup/cgroup.controllers').is_file(),
+ 'fuseDevice': Path('/dev/fuse').exists() and stat.S_ISCHR(Path('/dev/fuse').stat().st_mode)},
+ 'commands': {name: shutil.which(name) is not None for name in ('docker', 'fusermount3', 'nft', 'az', 'bpftool')},
+ 'bpfToolInventory': bpf_tool_inventory(),
+ 'installedBinarySha256': digest(binary) if binary.is_file() and not binary.is_symlink() else None,
+ 'platformRestartAuthorityVerified': False, 'compatibleReleasePairVerified': False,
+ 'canonicalPassClaimed': False}
+
+
+def resource_limits(profile):
+ require(profile in {"standard", "cursor", "full-restart"}, "unsupported Heal resource profile")
+ # DX has 1,025 external objects on four local roots. The candidate keeps
+ # an xl.meta, part and integrity sidecar for each: 12,300 files even
+ # without backups. Retain every sidecar within an explicit allowance.
+ # F05 combines the 1,025-object cursor fixture with DB, DV and completed
+ # MPU objects. Each of four local roots also retains metadata backups.
+ # Its combined census needs a distinct, still bounded file allowance.
+ return {"maxSnapshotFiles": {"standard": 4096, "cursor": 16384, "full-restart": 24576}[profile],
+ "minimumDriveFreeBytes": (1 if profile == "standard" else 2) * 1024**3,
+ "runtimeMaxSeconds": 1800 if profile == "standard" else 2700}
+
+
+def endpoint_resolution(request):
+ """Observe the existing resolver only; never install hosts entries or retry."""
+ require(set(request) == {'runId', 'action'} and request['action'] == 'endpoint-resolution'
+ and isinstance(request['runId'], str) and RUN_PATTERN.fullmatch(request['runId']),
+ 'endpoint resolution accepts no host, path or mutation options')
+ hosts = ['rustfs-node1'] if request['runId'].endswith('-h1') else [f'rustfs-node{i}' for i in range(1, 5)]
+ entries = {}
+ for host in hosts:
+ try:
+ result = subprocess.run(['getent', 'ahosts', host], capture_output=True, text=True, timeout=10)
+ addresses = sorted({str(ipaddress.ip_address(line.split()[0])) for line in result.stdout.splitlines() if line.split()})
+ entries[host] = {'addresses': addresses, 'resolved': result.returncode == 0 and bool(addresses),
+ 'returnCode': result.returncode}
+ except (OSError, ValueError, subprocess.TimeoutExpired) as exc:
+ entries[host] = {'addresses': [], 'resolved': False, 'failureType': type(exc).__name__}
+ return {'hostname': socket.gethostname(), 'entries': entries, 'readOnly': True, 'mutated': False,
+ 'resolved': all(row['resolved'] for row in entries.values()),
+ 'hostIdentityCertified': False, 'canonicalPassClaimed': False}
+
+
+def preflight(request):
+ state, roots = paths(request["runId"])
+ require(not state.exists() and all(not root.exists() for root in roots), "run namespace already exists")
+ observed = mounts()
+ limits = resource_limits(request.get("resourceProfile", "standard"))
+ require(all(shutil.disk_usage(mount).free >= limits["minimumDriveFreeBytes"] for mount in MOUNTS),
+ "insufficient free space for the explicit Heal resource profile")
+ result = {"hostname": socket.gethostname(), "mounts": observed, "statePath": str(state),
+ "dataRoots": [str(root / "data") for root in roots], "resourceLimits": limits, "mutated": False}
+ if request.get('runtimeProfile') in {'lock-timeout', 'lock-successor'}:
+ result['bpfTool'] = i05_bpf_tool()
+ return result
+
+
+def reserve(request):
+ run_id = request["runId"]
+ profile = request.get("runtimeProfile", "manual")
+ background = runtime_config(profile)
+ observed = preflight(request)
+ require(observed["hostname"] == request["hostname"] and observed["mounts"] == request["mounts"],
+ "preflight host or disk identities changed")
+ require(command("systemctl", "is-active", "--quiet", "rustfs", check=False).returncode != 0,
+ "shared RustFS service must be stopped before allocating port 9000")
+ listeners = command("ss", "-H", "-lnt").stdout
+ require(not any(len(parts := line.split()) >= 4 and parts[3].endswith(":9000")
+ for line in listeners.splitlines()), "port 9000 remains occupied")
+ binary_sha = digest(Path("/usr/bin/rustfs"))
+ require(binary_sha == request["binarySha256"], "installed candidate binary differs")
+ for name in ("access", "secret"):
+ require(bool(re.fullmatch(r"[A-Za-z0-9]{16,80}", request[name])), "invalid runtime credential")
+ volumes = request["volumes"]
+ require(isinstance(volumes, list) and len(volumes) in {2, 4, 16}, "unsupported automatic Heal topology")
+ local_paths = [str(root / "data") for root in paths(run_id)[1]]
+ h1 = run_id.endswith("-h1")
+ require(profile != "lock-quorum" or not h1, "eight-parity lock boundary requires actual sixteen-drive H2")
+ expected = local_paths if h1 else [f"http://rustfs-node{node}:9000{path}"
+ for node in range(1, 5) for path in local_paths]
+ if profile == "multi-pool":
+ require(run_id.endswith("-h5"), "multi-pool service requires its explicit H5 namespace")
+ expected = [f"http://rustfs-node{{1...4}}:9000/data/rustfs{{{first}...{first + 1}}}/heal-validation/{run_id}/data"
+ for first in (1, 3)]
+ else:
+ require(not run_id.endswith("-h5"), "H5 cannot use an H1/H2 runtime")
+ if profile == "scope-isolation":
+ require(not h1, "two-set runtime requires the four-node topology")
+ expected = [f"http://rustfs-node{node}:9000{path}" for path in local_paths for node in range(1, 5)]
+ require(volumes == expected, "endpoint sequence differs from the explicit Heal deployment contract")
+ state, roots = paths(run_id)
+ owner = {"runId": run_id, "unit": unit(run_id), "hostname": observed["hostname"],
+ "mounts": observed["mounts"], "binarySha256": binary_sha,
+ "bucket": request["bucket"], "volumes": volumes, "runtimeProfile": profile,
+ "resourceProfile": request.get("resourceProfile", "standard")}
+ if request.get("inspectorSha256"):
+ require(bool(re.fullmatch(r"[0-9a-f]{64}", request["inspectorSha256"])), "invalid inspector digest")
+ owner["inspectorSha256"] = request["inspectorSha256"]
+ require(bool(re.fullmatch(r"rv-heal-[a-f0-9]{16}", owner["bucket"])), "invalid run bucket")
+ state.mkdir(parents=True, mode=0o700)
+ (state / "owner.json").write_text(json.dumps(owner))
+ for root in roots:
+ root.mkdir(parents=True, mode=0o700)
+ (root / "owner.json").write_text(json.dumps(owner))
+ (root / "data").mkdir(mode=0o700)
+ (root / "quarantine").mkdir(mode=0o700)
+ shutil.copyfile("/usr/bin/rustfs", state / "rustfs")
+ (state / "rustfs").chmod(0o500)
+ require(digest(state / "rustfs") == binary_sha, "copied candidate binary differs")
+ config = {**background, "RUSTFS_ACCESS_KEY": request["access"], "RUSTFS_SECRET_KEY": request["secret"],
+ "RUSTFS_ADDRESS": ":9000", "RUSTFS_CONSOLE_ENABLE": "false",
+ "RUSTFS_HEAL_TASK_TIMEOUT_SECS": "300"}
+ env_file = state / "service.env"
+ descriptor = os.open(env_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
+ with os.fdopen(descriptor, "w") as stream:
+ stream.write("\n".join(f"{key}={value}" for key, value in config.items()) + "\n")
+ return {"owner": owner, "reserved": True}
+
+
+def owned(run_id):
+ state, roots = paths(run_id)
+ marker = checked(state / "owner.json", state)
+ require(marker.stat().st_uid == 0 and marker.stat().st_nlink == 1, "root-owned marker required")
+ owner = json.loads(marker.read_text())
+ require(owner["runId"] == run_id and owner["unit"] == unit(run_id)
+ and owner["hostname"] == socket.gethostname(), "run owner or hostname differs")
+ current = mounts()
+ for actual, previous in zip(current, owner["mounts"], strict=True):
+ require(all(actual[key] == previous[key] for key in ("target", "uuid", "maj:min", "source", "fstype")),
+ "disk identity changed")
+ for root in roots:
+ marker = checked(root / "owner.json", root)
+ require(marker.stat().st_uid == 0, f"drive owner marker is not root-owned: {root}")
+ observed = json.loads(marker.read_text())
+ if observed != owner:
+ expected_bytes = json.dumps(owner, sort_keys=True, separators=(",", ":")).encode()
+ observed_bytes = json.dumps(observed, sort_keys=True, separators=(",", ":")).encode()
+ raise ValueError(
+ f"drive owner differs at {root}: expected_sha256={hashlib.sha256(expected_bytes).hexdigest()} "
+ f"observed_sha256={hashlib.sha256(observed_bytes).hexdigest()} "
+ f"observed_run_id={observed.get('runId')} observed_bucket={observed.get('bucket')}"
+ )
+ if owner.get("runtimeProfile") in {"incarnation-checkpoint", "incarnation-mrf"}:
+ phase_file = checked(state / "C11-phase.json", state)
+ if phase_file.exists():
+ require(phase_file.stat().st_uid == 0 and phase_file.stat().st_nlink == 1,
+ "C11 phase marker lost its private root ownership")
+ phase = json.loads(phase_file.read_text())
+ require(set(phase) == {"runId", "phase"} and phase["runId"] == run_id
+ and phase["phase"] in {"transition", "replay"}, "C11 phase marker differs")
+ owner = {**owner, "incarnationPhase": phase["phase"]}
+ if owner.get("runtimeProfile") == "outcome-legacy":
+ phase_file = checked(state / "J02-phase.json", state)
+ if phase_file.exists():
+ info = phase_file.lstat()
+ require(stat.S_ISREG(info.st_mode) and info.st_uid == 0 and info.st_nlink == 1
+ and info.st_size <= 16384 and not info.st_mode & 0o022, "J02 phase marker lost private ownership")
+ phase = json.loads(phase_file.read_text())
+ require(phase == {"runId": run_id, "phase": "legacy"}, "J02 phase marker differs")
+ owner = {**owner, "outcomePhase": "legacy"}
+ return state, roots, owner
+
+
+def start(request):
+ state, roots, owner = owned(request["runId"])
+ stopped(owner)
+ command("systemctl", "reset-failed", owner["unit"], check=False)
+ require(digest(state / "rustfs") == owner["binarySha256"], "candidate changed before start")
+ if owner.get("runtimeProfile") == "outcome-legacy":
+ require(not (state / "J02-legacy-intent.json").exists() or owner.get("outcomePhase") == "legacy",
+ "J02 phase commit is incomplete; service cannot start")
+ if owner.get("outcomePhase") == "legacy":
+ outcome_legacy_phase({"runId": owner["runId"], "action": "outcome-legacy-phase"})
+ if owner.get("runtimeProfile") in {"incarnation-checkpoint", "incarnation-mrf", "outcome-legacy", "frozen-race-legacy", "lock-timeout", "lock-successor"}:
+ runtime_configuration(state, owner)
+ filter_properties = i05_filter_properties(owner)
+ command("systemd-run", "--unit=" + owner["unit"], "--property=Type=exec",
+ "--property=KillMode=control-group", "--property=TimeoutStopSec=30",
+ "--property=LogRateLimitIntervalSec=1s", "--property=LogRateLimitBurst=1000000",
+ "--property=RuntimeMaxSec=" + str(resource_limits(owner.get("resourceProfile", "standard"))["runtimeMaxSeconds"]),
+ "--property=MemoryMax=4G",
+ *(["--property=PrivateMounts=true", "--property=CapabilityBoundingSet=~CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH"]
+ if owner.get("runtimeProfile") in {"flapping", "queue-pressure"} else []),
+ *(["--property=RemainAfterExit=yes"] if owner.get("runtimeProfile") in {"root-restart", "journal-bounded", "durable-mrf", "mrf-crash", "replacement", "replacement-target", "schema-refusal", "incarnation-checkpoint", "incarnation-mrf"} else []),
+ "--property=WorkingDirectory=" + str(state),
+ "--property=EnvironmentFile=" + str(state / "service.env"),
+ *filter_properties,
+ *gate_module().start_properties(gate_module().checked_gate(state, roots)),
+ *quorum_gate_module().properties(state, roots),
+ *(volume_module().properties(state, roots, owner) if owner.get("runtimeProfile") in {"bounded-capacity", "journal-bounded"} else []),
+ *replacement_module().start_properties(state, roots), str(state / "rustfs"), *owner["volumes"])
+ result = {"unit": owner["unit"], "started": True, "journalPolicy": journal_policy(owner)}
+ if owner.get("runtimeProfile") in {"lock-timeout", "lock-successor"}:
+ result["platformNetworkPolicy"] = i05_filter_policy(owner)
+ return result
+
+
+def i05_bpf_tool():
+ """Use the installed executable reviewed on all current auto-testing peers."""
+ resolved = I05_BPFTOOL.resolve(strict=True)
+ require(resolved.is_file() and os.access(resolved, os.X_OK), 'I05 reviewed bpftool is not executable')
+ for path in (resolved, *resolved.parents, I05_BPFTOOL, *I05_BPFTOOL.parents):
+ info = path.stat()
+ require(info.st_uid == 0 and not info.st_mode & 0o022,
+ 'I05 reviewed bpftool path is not root-owned and protected')
+ require(digest(resolved) == I05_BPFTOOL_SHA256, 'I05 installed bpftool differs from its reviewed digest')
+ version = command(str(resolved), '--version')
+ require(version.stdout.startswith('bpftool v7.7.0\nusing libbpf v1.7\n'),
+ 'I05 installed bpftool has an unexpected version')
+ return {'path': str(I05_BPFTOOL), 'resolvedPath': str(resolved), 'sha256': I05_BPFTOOL_SHA256,
+ 'version': version.stdout.strip(), 'kernelRelease': os.uname().release,
+ 'serviceAttachmentsVerified': False}
+
+
+def i05_filter_properties(owner):
+ """Exclude the observed platform source only from the private I05 unit."""
+ if owner.get("runtimeProfile") not in {"lock-timeout", "lock-successor"}:
+ return []
+ require(Path('/sys/fs/cgroup/cgroup.controllers').is_file(),
+ 'I05 requires cgroup v2 to verify its service network filter')
+ require(owner.get('unit') == unit(owner['runId']), 'I05 filter requires its exact owned service')
+ i05_bpf_tool()
+ return ['--property=IPAddressDeny=168.63.129.16/32', '--property=IPAddressAllow=']
+
+
+def i05_filter_policy(owner, expected_pid=None):
+ """Check effective systemd policy and actual kernel attachments, failing closed.
+
+ Unit properties alone do not establish cgroup BPF support. No host firewall,
+ platform agent or persistent network setting is changed. Stopping the unit
+ removes its service-scoped filters with the service's cgroup.
+ """
+ require(owner.get('runtimeProfile') in {'lock-timeout', 'lock-successor'}
+ and owner.get('unit') == unit(owner['runId']), 'I05 filter requires its exact owned service')
+ tool = i05_bpf_tool()
+ names = ('IPAddressDeny', 'IPAddressAllow', 'ControlGroup', 'MainPID')
+ raw = command('systemctl', 'show', owner['unit'],
+ *(part for name in names for part in ('-p', name))).stdout
+ values = dict(line.split('=', 1) for line in raw.splitlines() if '=' in line)
+ group = '/system.slice/' + owner['unit']
+ require(values.get('IPAddressDeny') == '168.63.129.16/32'
+ and values.get('IPAddressAllow') == '' and values.get('ControlGroup') == group
+ and re.fullmatch(r'[1-9][0-9]*', values.get('MainPID', ''))
+ and int(values['MainPID']) > 1
+ and (expected_pid is None or int(values['MainPID']) == expected_pid),
+ 'I05 effective service filter or bound process differs')
+ pid = int(values['MainPID'])
+ require((Path('/proc') / str(pid) / 'cgroup').read_text().strip() == '0::' + group,
+ 'I05 process is outside its filtered service cgroup')
+ cgroup = Path('/sys/fs/cgroup') / group.lstrip('/')
+ require(cgroup.is_dir() and cgroup.resolve(strict=True) == cgroup,
+ 'I05 filtered service cgroup is missing or aliased')
+ rows = json.loads(command(tool['resolvedPath'], '-j', 'cgroup', 'show', str(cgroup)).stdout)
+ expected = {'ingress': 'sd_fw_ingress', 'egress': 'sd_fw_egress'}
+ # Newer bpftool uses libbpf's full enum names for the same attach types.
+ directions = {'ingress': 'ingress', 'egress': 'egress',
+ 'cgroup_inet_ingress': 'ingress', 'cgroup_inet_egress': 'egress'}
+ require(isinstance(rows, list) and len(rows) == 2
+ and all(isinstance(row, dict) for row in rows)
+ and all(isinstance(row.get('attach_type'), str) for row in rows)
+ and {directions.get(row['attach_type']) for row in rows} == set(expected)
+ and all(type(row.get('id')) is int and row['id'] > 0
+ and row.get('name') == expected[directions[row['attach_type']]]
+ and row.get('attach_flags') == 'multi' for row in rows)
+ and len({row['id'] for row in rows}) == 2,
+ 'I05 kernel service BPF attachments are missing, foreign or incomplete')
+ programs = []
+ for row in sorted(rows, key=lambda item: item['attach_type']):
+ program = json.loads(command(tool['resolvedPath'], '-j', 'prog', 'show', 'id', str(row['id'])).stdout)
+ if isinstance(program, list):
+ require(len(program) == 1, 'I05 kernel filter program is ambiguous')
+ program = program[0]
+ require(isinstance(program, dict) and program.get('id') == row['id']
+ and program.get('type') == 'cgroup_skb' and program.get('name') == row['name']
+ and type(program.get('uid')) is int and program['uid'] == 0
+ and re.fullmatch(r'[0-9a-f]{16}', program.get('tag', '')),
+ 'I05 kernel filter program identity or type differs')
+ programs.append({**row, 'direction': directions[row['attach_type']],
+ 'type': program['type'], 'tag': program['tag'], 'uid': program['uid']})
+ return {'unit': owner['unit'], 'pid': pid, 'controlGroup': group,
+ 'deniedAddress': '168.63.129.16/32', 'effectiveProperties': values,
+ 'bpfTool': tool,
+ 'kernelPrograms': programs, 'scope': 'owned I05 service only',
+ 'hostFirewallChanged': False, 'persistentNetworkSettingsChanged': False,
+ 'snapshotOnly': True, 'canonicalPassClaimed': False}
+
+
+def journal_policy(owner):
+ """Verify the owned unit's effective policy before source attribution."""
+ output = command("systemctl", "show", owner["unit"], "-p", "LogRateLimitIntervalUSec",
+ "-p", "LogRateLimitBurst").stdout
+ values = dict(line.split("=", 1) for line in output.splitlines() if "=" in line)
+ require(values.get("LogRateLimitIntervalUSec") in {"1s", "1000000"}
+ and values.get("LogRateLimitBurst") == "1000000",
+ "owned service journal rate policy differs; source attribution cannot start")
+ version = command("systemctl", "--version").stdout.splitlines()
+ require(version and re.fullmatch(r"systemd [0-9]+(?: .*)?", version[0]), "systemd version is unavailable")
+ return {"unit": owner["unit"], "rateIntervalSeconds": 1, "rateBurst": 1000000,
+ "effectiveProperties": values, "systemdVersion": version[0], "hostWideSettingsChanged": False}
+
+
+def stopped(owner):
+ values = command("systemctl", "show", owner["unit"], "-p", "MainPID", "-p", "ActiveState",
+ "-p", "ControlGroup", check=False).stdout
+ state = dict(line.split("=", 1) for line in values.splitlines() if "=" in line)
+ require(state.get("MainPID") == "0" and state.get("ActiveState") in {"inactive", "failed"},
+ "file operations require the run-owned service to be stopped")
+ group = state.get("ControlGroup", "")
+ if group:
+ require(group.startswith("/") and ".." not in Path(group).parts, "invalid systemd control group")
+ root = Path("/sys/fs/cgroup") / group.lstrip("/")
+ if root.exists():
+ require(all(not entry.read_text().strip() for entry in root.rglob("cgroup.procs")),
+ "run-owned control group still has live processes")
+ return state
+
+
+def stop(request):
+ _, _, owner = owned(request["runId"])
+ command("systemctl", "stop", owner["unit"], check=False)
+ return {"unit": owner["unit"], "quiescent": True, "service": stopped(owner)}
+
+
+PROCESS_FIELDS = ("pid", "startTicks", "bootId", "unit", "executable", "binarySha256", "mountNamespace",
+ "commandSha256", "environmentFileSha256", "settings")
+
+
+def process_once(request):
+ state, _, owner = owned(request["runId"])
+ fields = ("MainPID", "ActiveState", "SubState", "ExecMainCode", "ExecMainStatus", "Result")
+ service = dict(line.split("=", 1) for line in command("systemctl", "show", owner["unit"],
+ *(part for name in fields for part in ("-p", name)), check=False).stdout.splitlines() if "=" in line)
+ pid = int(service.get("MainPID", "0"))
+ base = {"pid": pid, "unit": owner["unit"], "service": service,
+ "bootId": Path("/proc/sys/kernel/random/boot_id").read_text().strip(),
+ "binarySha256": digest(state / "rustfs"), "environmentFileSha256": digest(state / "service.env")}
+ require(base["binarySha256"] == owner["binarySha256"], "process candidate changed")
+ if pid == 0:
+ return base
+ require(pid > 1, "invalid owned process PID")
+ root = Path(f"/proc/{pid}")
+ before = (root / "stat").read_text().rpartition(") ")[2].split()
+ require(len(before) >= 20 and before[19].isdigit(), "invalid process birth record")
+ executable = (root / "exe").resolve(strict=True)
+ require(executable == state / "rustfs" and digest(executable) == owner["binarySha256"], "signal target is not the owned candidate")
+ environment = dict(item.split("=", 1) for item in (root / "environ").read_bytes().decode().split("\0") if "=" in item)
+ settings = {**runtime_settings(owner), "RUSTFS_HEAL_TASK_TIMEOUT_SECS": "300"}
+ require(all(environment.get(key) == value for key, value in settings.items()), "owned process runtime changed")
+ result = {**base, "state": before[0], "startTicks": int(before[19]), "executable": str(executable),
+ "mountNamespace": os.readlink(root / "ns/mnt"), "commandSha256": hashlib.sha256((root / "cmdline").read_bytes()).hexdigest(),
+ "settings": settings}
+ after = (root / "stat").read_text().rpartition(") ")[2].split()
+ require(after[19] == before[19], "process identity changed while collecting evidence")
+ return result
+
+
+def process(request):
+ # systemd's MainPID can remain visible briefly after /proc disappears.
+ # Refresh the whole service snapshot; absence alone never proves exit or
+ # permits an invented PID=0. Exact exit code/result checks remain in the
+ # graceful/crash observer, and pidfd callers still compare process birth.
+ for attempt in range(3):
+ try:
+ return process_once(request)
+ except FileNotFoundError as exc:
+ if not isinstance(exc.filename, str) or not re.match(r"^/proc/[1-9][0-9]*/", exc.filename):
+ raise
+ require(attempt < 2, "owned process/service state did not settle after process exit")
+ time.sleep(.05)
+
+
+def pidfd_signal(request, signum):
+ expected = request.get("expected")
+ require(isinstance(expected, dict) and all(name in expected for name in PROCESS_FIELDS)
+ and type(expected["pid"]) is int and expected["pid"] > 1, "signal lacks an exact process birth identity")
+ require(hasattr(os, "pidfd_open") and hasattr(signal, "pidfd_send_signal"), "safe process control requires Linux pidfd support")
+ descriptor = os.pidfd_open(expected["pid"], 0)
+ try:
+ current = process(request)
+ require(all(current.get(name) == expected[name] for name in PROCESS_FIELDS), "stale process identity; no signal sent")
+ signal.pidfd_send_signal(descriptor, signum, None, 0)
+ finally:
+ os.close(descriptor)
+ return current
+
+
+def process_metrics(request):
+ """Read only the owned service's resource counters, with process birth binding."""
+ require(set(request) == {"runId", "action"}, "process metrics accept no arbitrary PID or path")
+ before = process(request)
+ require(before["pid"] > 1, "resource sample requires the original live service")
+ root = Path(f"/proc/{before['pid']}")
+ fields = (root / "stat").read_text().rpartition(") ")[2].split()
+ require(len(fields) >= 22 and int(fields[19]) == before["startTicks"], "resource sample crossed process birth")
+ io = {name: int(value.strip()) for name, value in (line.split(":", 1) for line in (root / "io").read_text().splitlines())}
+ required_io = {"rchar", "wchar", "syscr", "syscw", "read_bytes", "write_bytes", "cancelled_write_bytes"}
+ require(required_io <= set(io) and all(value >= 0 for value in io.values()), "owned process IO counters are incomplete")
+ result = {"identity": before, "clockDomain": before["bootId"], "monotonicNs": time.monotonic_ns(),
+ "cpuTicks": int(fields[11]) + int(fields[12]), "clockTicksPerSecond": os.sysconf("SC_CLK_TCK"),
+ "rssBytes": int(fields[21]) * os.sysconf("SC_PAGE_SIZE"), "virtualBytes": int(fields[20]),
+ "threads": int(fields[17]), "fileDescriptors": sum(1 for _ in (root / "fd").iterdir()),
+ "io": {name: io[name] for name in sorted(required_io)}, "readOnly": True,
+ "ioScope": "owned process counters; cached logical IO and storage IO reported separately"}
+ if owned(request["runId"])[2].get("runtimeProfile") in {"lock-timeout", "lock-successor"}:
+ namespace = os.readlink(root / "ns/net")
+ result["descriptorCensus"] = caller_socket_backend.descriptor_census(root)
+ result["fileDescriptors"] = result["descriptorCensus"]["fileDescriptors"]
+ require(os.readlink(root / "ns/net") == namespace, "resource sample crossed a network namespace change")
+ result["networkNamespace"] = namespace
+ result["platformNetworkPolicy"] = i05_filter_policy(owned(request["runId"])[2], before['pid'])
+ after = process(request)
+ require(all(after.get(name) == before[name] for name in PROCESS_FIELDS), "resource sample crossed a service identity change")
+ return result
+
+
+def process_clock(request):
+ require(set(request) == {"runId", "action"}, "process clock accepts no PID, path or clock override")
+ require(owned(request["runId"])[2].get("runtimeProfile") == "outcome-grace", "process clock requires J02 grace runtime")
+ before = process(request)
+ require(type(before.get("pid")) is int and before["pid"] > 1
+ and before["settings"].get("RUSTFS_HEAL_DANGLING_DELETE_GRACE_SECS") == "120",
+ "J02 grace process is absent or has another policy")
+ epoch = time.time()
+ after = process(request)
+ require(all(after.get(name) == before[name] for name in PROCESS_FIELDS), "clock witness crossed a process identity change")
+ return {"identity": before, "nodeEpoch": epoch, "readOnly": True, "clockChanged": False}
+
+
+def process_signal(request):
+ name = request.get("signal")
+ require(name in {"SIGSTOP", "SIGCONT"}, "only owned-process pause/resume signals are allowed")
+ before = pidfd_signal(request, getattr(signal, name))
+ return {"signal": name, "before": before, "pidfdBound": True, "platformEventIssued": False}
+
+
+def process_graceful(request):
+ require(owned(request["runId"])[2].get("runtimeProfile") in {"root-restart", "journal-bounded", "replacement", "replacement-target"}
+ and process(request).get("state") == "T", "graceful observation requires the paused run-owned root service")
+ before = pidfd_signal(request, signal.SIGTERM)
+ # A paused process must receive CONT to handle TERM. Use the same pidfd
+ # and exact birth identity again; a recycled PID never receives a signal.
+ pidfd_signal(request, signal.SIGCONT)
+ deadline = time.monotonic() + 60
+ while True:
+ after = process(request)
+ if after["pid"] == 0:
+ break
+ require(all(after.get(name) == before[name] for name in PROCESS_FIELDS), "another process replaced the graceful target")
+ require(time.monotonic() < deadline, "owned coordinator did not exit gracefully in 60 seconds")
+ time.sleep(0.1)
+ service = after["service"]
+ require(after["bootId"] == before["bootId"] and after["binarySha256"] == before["binarySha256"]
+ and after["environmentFileSha256"] == before["environmentFileSha256"]
+ and service.get("ExecMainCode") == "1" and service.get("ExecMainStatus") == "0"
+ and service.get("Result") == "success", "process has no witnessed normal exit zero; signal/timeout exits are not graceful")
+ return {"before": before, "after": after, "signal": "SIGTERM", "actualExitCode": 0,
+ "pidfdBound": True, "platformEventIssued": False}
+
+
+def process_crash(request):
+ require(owned(request["runId"])[2].get("runtimeProfile") in {"replacement", "replacement-target", "root-restart", "journal-bounded", "durable-mrf", "mrf-crash", "incarnation-checkpoint", "incarnation-mrf"}
+ and process(request).get("state") == "T", "crash observation requires a paused owned recovery service")
+ before = pidfd_signal(request, signal.SIGKILL)
+ deadline = time.monotonic() + 30
+ while True:
+ after = process(request)
+ if after["pid"] == 0: break
+ require(all(after.get(name) == before[name] for name in PROCESS_FIELDS), "another process replaced the crash target")
+ require(time.monotonic() < deadline, "owned recovery process did not exit after SIGKILL")
+ time.sleep(0.1)
+ service = after["service"]
+ require(after["bootId"] == before["bootId"] and after["binarySha256"] == before["binarySha256"]
+ and after["environmentFileSha256"] == before["environmentFileSha256"]
+ and service.get("ExecMainCode") == "2" and service.get("ExecMainStatus") == "9"
+ and service.get("Result") == "signal", "process has no exact non-graceful SIGKILL exit witness")
+ return {"before": before, "after": after, "signal": "SIGKILL", "actualSignal": 9,
+ "pidfdBound": True, "graceful": False, "platformEventIssued": False}
+
+
+def fault_module():
+ # SSH requests carry this exact repository module in memory; local imports
+ # support contract tests without creating an importable file on a node.
+ if "fault" in globals():
+ return globals()["fault"]
+ from . import heal_fault
+ return heal_fault
+
+
+def encryption_module():
+ if "encryption_runtime" in globals(): return globals()["encryption_runtime"]
+ from . import encryption_runtime
+ return encryption_runtime
+
+
+def gate_module():
+ if "gate" in globals(): return globals()["gate"]
+ from . import drive_gate
+ return drive_gate
+
+
+def quorum_gate_module():
+ if "quorum_gate_backend" in globals(): return globals()["quorum_gate_backend"]
+ from . import quorum_gate
+ return quorum_gate
+
+
+def volume_module():
+ if "bounded_volume_backend" in globals(): return globals()["bounded_volume_backend"]
+ from . import bounded_volume
+ return bounded_volume
+
+
+def volume_views(state, roots, owner):
+ return volume_module().views(state, roots, owner) if owner.get("runtimeProfile") in {"bounded-capacity", "journal-bounded"} else roots
+
+
+def volume_operation(request):
+ state, roots, owner = owned(request["runId"])
+ require(owner.get("runtimeProfile") in {"bounded-capacity", "journal-bounded"}, "bounded operation requires its exact dedicated runtime")
+ module = volume_module()
+ if request["action"] == "bounded-volumes-create":
+ require(set(request) == {"runId", "action", "branch"}, "bounded allocation accepts no supplied path or limits")
+ stopped(owner)
+ return module.create(state, roots, owner, request["branch"])
+ if request["action"] == "bounded-volumes-release" and request.get("quiescent") is True:
+ require(set(request) == {"runId", "action", "quiescent"}, "bounded recovery accepts no supplied path")
+ stopped(owner)
+ changed = [module.release_filler(view) for view in module.views(state, roots, owner)]
+ return {"changes": changed, "ownedFillerReleased": True, "productRepairClaimed": False,
+ "quiescent": True, "privateMountRemovedByStoppedService": True}
+ if request["action"] == "bounded-volumes-fill" and request.get("quiescent") is True:
+ require(set(request) == {"runId", "action", "quiescent"}
+ and owner.get("runtimeProfile") == "journal-bounded",
+ "stopped exhaustion requires the dedicated journal runtime")
+ stopped(owner)
+ require(module.read(state, roots, owner)["branch"] == "bounded-volume-enospc",
+ "journal exhaustion cannot use another resource fault")
+ changed = [module.fill(view, "bounded-volume-enospc") for view in module.views(state, roots, owner)]
+ return {"changes": changed, "quiescent": True, "hostSharedMountRemounted": False,
+ "candidateProcessWitnessRequired": True, "productRepairClaimed": False}
+ require(set(request) == {"runId", "action", "expected"}, "bounded operation lacks exact process binding")
+ before = process(request)
+ require(before["pid"] > 1 and isinstance(request["expected"], dict)
+ and all(before.get(name) == request["expected"].get(name) for name in PROCESS_FIELDS),
+ "bounded operation crossed its expected original process birth")
+ views = module.views(state, roots, owner)
+ if request["action"] == "bounded-volumes-fill":
+ branch = module.read(state, roots, owner)["branch"]
+ changed = [module.fill(view, branch) for view in views]
+ observed = module.witness(state, roots, owner, before["pid"], exhausted=True)
+ elif request["action"] == "bounded-volumes-release":
+ changed = [module.release_filler(view) for view in views]
+ observed = module.witness(state, roots, owner, before["pid"], exhausted=False)
+ else:
+ require(request["action"] == "bounded-volumes-witness", "unsupported bounded operation")
+ changed = []
+ observed = module.witness(state, roots, owner, before["pid"], exhausted=True)
+ after = process(request)
+ require(all(before.get(name) == after.get(name) for name in PROCESS_FIELDS),
+ "bounded operation crossed a candidate process identity")
+ return {**observed, "changes": changed, "processBefore": before, "processAfter": after, "sameOriginalProcess": True}
+
+
+def quorum_gate_operation(request):
+ state, roots, owner = owned(request["runId"])
+ module = quorum_gate_module()
+ if request["action"] == "quorum-gate":
+ stopped(owner)
+ return module.release(state, roots) if request.get("mode") == "clear" else module.apply(state, roots, request["disks"])
+ before = process(request)
+ require(before["pid"] > 1 and digest(state / "rustfs") == owner["binarySha256"], "quorum probe candidate differs")
+ relative = request["relativePath"]
+ require(isinstance(relative, str) and relative.startswith(owner["bucket"] + "/")
+ and relative.endswith("/xl.meta"), "quorum read probe escaped its owned business bucket")
+ for root in roots:
+ checked(root / "data" / relative, root / "data")
+ observed = module.probe(state, roots, before["pid"], relative, request["expectedReads"])
+ after = process(request)
+ require(all(before.get(name) == after.get(name) for name in PROCESS_FIELDS), "quorum probe crossed candidate process identity")
+ return {**observed, "processBefore": before, "processAfter": after}
+
+
+def replacement_module():
+ if "replacement" in globals(): return globals()["replacement"]
+ from . import replace_drive
+ return replace_drive
+
+
+def replace_drive(request):
+ state, roots, owner = owned(request["runId"])
+ stopped(owner)
+ require(gate_module().checked_gate(state, roots) is None, "replacement cannot share an active drive gate")
+ require(quorum_gate_module().checked(state, roots) is None, "replacement cannot share an active quorum gate")
+ return replacement_module().begin(state, roots, request.get("disk"), request.get("expectedFormatSha256"))
+
+
+def replacement_state(request):
+ state, roots, owner = owned(request["runId"])
+ require(digest(state / "rustfs") == owner["binarySha256"], "replacement candidate changed")
+ evidence = replacement_module().attest(state, roots)
+ formats = []
+ for disk, root in enumerate(roots):
+ path = checked(root / "data/.rustfs.sys/format.json", root)
+ if path.exists():
+ require(path.stat().st_size < 65536, "replacement format exceeds its bound")
+ formats.append({"disk": disk, "present": True, "sha256": digest(path), "value": json.loads(path.read_text())})
+ else: formats.append({"disk": disk, "present": False})
+ return {**evidence, "formats": formats, "binarySha256": owner["binarySha256"], "readOnly": True}
+
+
+def replacement_probe(request):
+ state, roots, _ = owned(request["runId"])
+ before = process(request)
+ result = replacement_module().probe(state, roots, before["pid"])
+ after = process(request)
+ require(all(before.get(name) == after.get(name) for name in PROCESS_FIELDS),
+ "replacement backup probe crossed a service process change")
+ return {**result, "sameOriginalServiceProcess": True}
+
+
+def format_export(request):
+ require(set(request) == {"runId", "action", "disk"}, "format export accepts one owned member only")
+ state, roots, owner = owned(request["runId"])
+ stopped(owner)
+ disk = request["disk"]
+ require(type(disk) is int and 0 <= disk < len(roots), "invalid exported disk")
+ path = checked(roots[disk] / "data/.rustfs.sys/format.json", roots[disk])
+ require(path.stat().st_size < 65536, "exported format exceeds its bound")
+ sha = digest(path)
+ raw = path.read_bytes()
+ require(hashlib.sha256(raw).hexdigest() == sha == digest(path) and digest(state / "rustfs") == owner["binarySha256"],
+ "released donor format or candidate changed during export")
+ return {"runId": owner["runId"], "disk": disk, "binarySha256": owner["binarySha256"],
+ "sha256": sha, "rawBase64": base64.b64encode(raw).decode(), "value": json.loads(raw), "readOnly": True}
+
+
+def foreign_format(request):
+ require(set(request) == {"runId", "action", "rawBase64", "sha256"}, "foreign format accepts exact exported bytes only")
+ state, roots, owner = owned(request["runId"])
+ stopped(owner)
+ require(owner.get("runtimeProfile") == "replacement", "foreign format requires the isolated replacement runtime")
+ return replacement_module().install_foreign(state, roots, request["rawBase64"], request["sha256"])
+
+
+def restore_replacement(request):
+ require(set(request) == {"runId", "action"}, "replacement recovery accepts no paths")
+ state, roots, owner = owned(request["runId"])
+ stopped(owner)
+ return replacement_module().restore_original(state, roots)
+
+
+def drive_gate(request):
+ state, roots, owner = owned(request["runId"])
+ stopped(owner)
+ if request.get("mode") == "clear": return gate_module().release_gate(state, roots)
+ require(quorum_gate_module().checked(state, roots) is None, "single drive gate cannot share a quorum gate")
+ if request.get("scope", "disk") != "disk":
+ return gate_module().apply_gate(state, roots, request.get("disk"), request.get("mode"), scope=request["scope"])
+ return gate_module().apply_gate(state, roots, request.get("disk"), request.get("mode"))
+
+
+def drive_gate_probe(request):
+ state, roots, owner = owned(request["runId"])
+ pid = int(command("systemctl", "show", "-p", "MainPID", "--value", owner["unit"]).stdout.strip())
+ require(Path(f"/proc/{pid}/exe").resolve(strict=True) == state / "rustfs", "gate probe candidate process differs")
+ require(digest(state / "rustfs") == owner["binarySha256"], "gate probe candidate digest differs")
+ return gate_module().probe_gate(state, roots, pid)
+
+
+def drive_gate_activate(request):
+ state, roots, _ = owned(request["runId"])
+ before = process(request)
+ require(before["pid"] > 1, "permission fault requires the running owned candidate")
+ receipt = gate_module().activate_gate(state, roots)
+ after = process(request)
+ require(all(before.get(name) == after.get(name) for name in PROCESS_FIELDS),
+ "permission injection crossed the original process identity")
+ return {**receipt, "processBefore": before, "processAfter": after, "sameOriginalProcess": True}
+
+
+def drive_gate_state(request):
+ state, roots, _ = owned(request["runId"])
+ return gate_module().checked_gate(state, roots) or {"state": "CLEAR"}
+
+
+def drive_gate_arm_live(request):
+ """Re-arm one owned root without changing the running service's birth."""
+ require(set(request) == {"runId", "action", "disk", "expected"}, "live arm requires an exact owned process binding")
+ state, roots, owner = owned(request["runId"])
+ require(owner.get("runtimeProfile") in {"flapping", "queue-pressure"}, "live arm requires a preconfigured private service")
+ before = process(request)
+ require(before["pid"] > 1 and isinstance(request["expected"], dict)
+ and all(before.get(name) == request["expected"].get(name) for name in PROCESS_FIELDS),
+ "live drive gate crossed the expected process birth")
+ module = gate_module()
+ module.validate_live_boundary(before["pid"])
+ require(quorum_gate_module().checked(state, roots) is None, "live drive gate cannot share a quorum gate")
+ gate_args = {"scope": "durable-bucket"} if owner.get("runtimeProfile") == "queue-pressure" else {}
+ receipt = module.apply_gate(state, roots, request["disk"], "permission-denied", **gate_args)
+ after = process(request)
+ require(all(before.get(name) == after.get(name) for name in PROCESS_FIELDS), "live arm crossed its owned process identity")
+ return {**receipt, "processBefore": before, "processAfter": after, "sameOriginalProcess": True}
+
+
+def drive_gate_release_live(request):
+ state, roots, _ = owned(request["runId"])
+ gate = gate_module().checked_gate(state, roots)
+ require(gate and gate.get("mode") == "permission-denied" and gate.get("phase") == "active",
+ "live rejoin only releases an actual active permission fault")
+ before = process(request)
+ require(before["pid"] > 1, "live rejoin requires the original running candidate")
+ receipt = gate_module().release_gate(state, roots)
+ after = process(request)
+ require(all(before.get(name) == after.get(name) for name in PROCESS_FIELDS),
+ "live rejoin crossed the original process identity")
+ return {**receipt, "processBefore": before, "processAfter": after,
+ "sameOriginalProcess": True, "serviceRestarted": False}
+
+
+def fault_root(request):
+ state, roots, owner = owned(request["runId"])
+ stopped(owner)
+ roots = volume_views(state, roots, owner)
+ index = request.get("disk")
+ require(type(index) is int and 0 <= index < len(roots), "invalid physical disk index")
+ return roots[index], owner
+
+
+def apply_fault(request):
+ root, owner = fault_root(request)
+ module = fault_module()
+ with module.directory(root) as fd:
+ marker = root / "fault-owner.json"
+ if not marker.exists():
+ module.new_file(fd, marker.name, module.encode({"schemaVersion": 1, "runId": owner["runId"],
+ "bucketPrefix": owner["bucket"], "mountId": module.mount_id(fd), "device": os.fstat(fd).st_dev,
+ "leaseExpiresAt": (datetime.now(timezone.utc) + timedelta(minutes=30)).isoformat()}))
+ binding = module.checked_owner(fd, owner["runId"])
+ return module.apply_fault(fd, binding, request["relativePath"], request["faultId"], request["kind"],
+ request["expectedSha256"], request.get("offset"),
+ donor_relative=request.get("donorRelativePath"), donor_sha=request.get("donorSha256"))
+
+
+def recover_fault(request):
+ root, owner = fault_root(request)
+ module = fault_module()
+ with module.directory(root) as fd:
+ return module.recover_fault(fd, module.checked_owner(fd, owner["runId"], recovery=True), request["faultId"],
+ expected_receipt_sha256=request.get("expectedReceiptSha256"))
+
+
+def verify_part(request):
+ state, roots, owner = owned(request["runId"])
+ stopped(owner)
+ roots = volume_views(state, roots, owner)
+ require(owner.get("inspectorSha256"), "candidate-bound bitrot verifier is required")
+ tool = checked(state / "heal-meta-inspect", state)
+ require(digest(tool) == owner["inspectorSha256"], "bitrot verifier changed")
+ return verify_part_owned(request, roots, owner, tool)
+
+
+def verify_part_owned(request, roots, owner, tool):
+ module = fault_module()
+ relative = request["relativePath"]
+ parts = module.target_parts(relative, owner["bucket"])
+ require(parts[-1] != "xl.meta" and type(request.get("partNumber")) is int
+ and parts[-1] == f"part.{request['partNumber']}", "part identity differs from request")
+ require(type(request.get("disk")) is int and 0 <= request["disk"] < len(roots), "invalid physical disk index")
+ root = roots[request["disk"]]
+ data = root / "data"
+ part = checked(data / relative, data)
+ metadata = checked(part.parent.parent / "xl.meta", data)
+ bucket = parts[0]
+ key = metadata.parent.relative_to(data / bucket).as_posix()
+ result = json.loads(command(str(tool), "--verify-part", str(metadata), bucket, key,
+ str(part), request["versionId"], str(request["partNumber"])).stdout)
+ require(result.get("physicalBitrotVerified") is True and result.get("bucket") == bucket and result.get("key") == key
+ and result.get("versionId") == request["versionId"]
+ and result.get("partNumber") == request["partNumber"], "bitrot proof identity differs")
+ return {**result, "sha256": digest(part), "relativePath": relative, "disk": request["disk"]}
+
+
+def verify_parts(request):
+ state, roots, owner = owned(request["runId"])
+ stopped(owner)
+ roots = volume_views(state, roots, owner)
+ entries = request.get("parts")
+ require(isinstance(entries, list) and 1 <= len(entries) <= 128
+ and all(isinstance(entry, dict) and set(entry) == {"disk", "relativePath", "versionId", "partNumber"}
+ for entry in entries), "invalid bounded part verification batch")
+ require(len({(entry["disk"], entry["relativePath"], entry["versionId"], entry["partNumber"])
+ for entry in entries}) == len(entries), "duplicate part in verification batch")
+ require(owner.get("inspectorSha256"), "candidate-bound bitrot verifier is required")
+ tool = checked(state / "heal-meta-inspect", state)
+ require(digest(tool) == owner["inspectorSha256"], "bitrot verifier changed")
+ return {"complete": True, "parts": [verify_part_owned(entry, roots, owner, tool) for entry in entries]}
+
+
+def verify_inline(request):
+ state, _, owner = owned(request["runId"])
+ root, _ = fault_root(request)
+ module = fault_module()
+ parts = module.target_parts(request["relativePath"], owner["bucket"])
+ require(parts[-1] == "xl.meta", "inline verification requires the exact object metadata")
+ path = checked(root / "data" / request["relativePath"], root / "data")
+ key = "/".join(parts[1:-1])
+ tool = checked(state / "heal-meta-inspect", state)
+ require(digest(tool) == owner.get("inspectorSha256"), "inline verifier changed")
+ result = json.loads(command(str(tool), "--verify-inline", str(path), parts[0], key, request["versionId"]).stdout)
+ require(result.get("physicalBitrotVerified") is True and result.get("bucket") == parts[0] and result.get("key") == key
+ and result.get("versionId") == request["versionId"], "inline proof identity differs")
+ return {**result, "relativePath": request["relativePath"], "disk": request["disk"]}
+
+
+def install_inspector(request):
+ state, _, owner = owned(request["runId"])
+ tool = checked(state / "heal-meta-inspect", state)
+ require(digest(tool) == owner.get("inspectorSha256"), "uploaded inspector digest differs")
+ tool.chmod(0o500)
+ return {"inspectorSha256": owner["inspectorSha256"], "installed": True}
+
+
+def inspect_metadata(tool, path, bucket, key, entry, metadata_faults):
+ observed = command(str(tool), str(path), bucket, key, check=False)
+ if observed.returncode:
+ expected_fault = metadata_faults.get(f"{entry['disk']}:{entry['path']}", metadata_faults.get(entry["path"]))
+ require(expected_fault == {name: entry[name] for name in ("sha256", "size")},
+ "metadata decoder failed outside an exact witnessed file fault")
+ return {"metadataError": {"code": observed.returncode, "message": observed.stderr[-500:],
+ "exactWitnessedFault": True, "validMetadataClaimed": False}}
+ decoded = json.loads(observed.stdout)
+ require(decoded.get("bucket") == bucket and decoded.get("key") == key
+ and decoded.get("physicalBitrotVerified") is False,
+ "metadata decoder identity or verification role differs")
+ return {"metadata": decoded}
+
+
+def snapshot_buckets(owner, request):
+ buckets = request.get("buckets", [owner["bucket"]])
+ require(isinstance(buckets, list) and 1 <= len(buckets) <= 8
+ and all(isinstance(bucket, str) and 3 <= len(bucket) <= 63
+ and re.fullmatch(r"[a-z0-9][a-z0-9.-]+[a-z0-9]", bucket)
+ and (bucket == owner["bucket"] or bucket.startswith(owner["bucket"] + "-")) for bucket in buckets)
+ and len(set(buckets)) == len(buckets) and owner["bucket"] in buckets,
+ "physical bucket scope escaped the original run-owned prefix or omitted its primary bucket")
+ return buckets
+
+
+def snapshot(request):
+ state, roots, owner = owned(request["runId"])
+ roots = volume_views(state, roots, owner)
+ pid = int(command("systemctl", "show", "-p", "MainPID", "--value", owner["unit"], check=False).stdout.strip() or "0")
+ if request.get("quiescent") is True:
+ stopped(owner)
+ require(digest(state / "rustfs") == owner["binarySha256"], "stopped candidate changed")
+ else:
+ require(pid > 0, "run-owned service has no live PID")
+ executable = Path(f"/proc/{pid}/exe").resolve(strict=True)
+ require(executable == state / "rustfs" and digest(executable) == owner["binarySha256"],
+ "running binary differs from candidate")
+ environment = dict(value.split("=", 1) for value in Path(f"/proc/{pid}/environ").read_bytes().decode().split("\0")
+ if "=" in value)
+ expected_config = runtime_settings(owner)
+ require(all(environment.get(key) == value for key, value in expected_config.items()),
+ "bound Heal runtime configuration differs")
+ files, formats = [], []
+ encryption = encryption_module().attest(state, owner, checked, digest,
+ None if request.get("quiescent") is True else environment)
+ limits = resource_limits(owner.get("resourceProfile", "standard"))
+ tool = state / "heal-meta-inspect"
+ if owner.get("inspectorSha256"):
+ require(digest(checked(tool, state)) == owner["inspectorSha256"], "metadata inspector changed")
+ for index, root in enumerate(roots):
+ data = root / "data"
+ for bucket in snapshot_buckets(owner, request):
+ folder = checked(data / bucket, data)
+ if folder.exists():
+ for directory, children, names in os.walk(folder, followlinks=False):
+ for name in (*children, *names):
+ checked(Path(directory) / name, data)
+ for name in sorted(names):
+ path = Path(directory) / name
+ require(len(files) < limits["maxSnapshotFiles"] and path.stat().st_size <= 64 * 1024**2,
+ "fixture exceeds physical evidence budget")
+ entry = {"disk": index, "path": str(path.relative_to(data)),
+ "size": path.stat().st_size, "sha256": digest(path)}
+ if path.name == "xl.meta" and owner.get("inspectorSha256"):
+ key = path.parent.relative_to(folder).as_posix()
+ entry.update(inspect_metadata(tool, path, bucket, key, entry, request.get("metadataFaults", {})))
+ files.append(entry)
+ path = checked(data / ".rustfs.sys/format.json", data)
+ if not path.exists() and request.get("allowUnformattedReplacement") is True:
+ replacement = replacement_module().attest(state, roots)
+ require(not replacement.get("replacementAbsent") and replacement["disk"] == index
+ and not replacement["originalStillAtDataRoot"], "missing format is outside the exact isolated replacement member")
+ formats.append({"disk": index, "present": False, "unformattedReplacement": True})
+ continue
+ require(path.stat().st_size < 65536, "disk format exceeds budget")
+ formats.append({"disk": index, "sha256": digest(path), "value": json.loads(path.read_text())})
+ return {"hostname": owner["hostname"], "unit": owner["unit"], "pid": pid,
+ "binarySha256": owner["binarySha256"], "quiescent": request.get("quiescent") is True,
+ "backgroundConfig": None if request.get("quiescent") is True else expected_config,
+ "storageClassEnvironment": None if request.get("quiescent") is True else {
+ key: environment.get(key) for key in ("RUSTFS_STORAGE_CLASS_INLINE_BLOCK", "RUSTFS_STORAGE_CLASS_STANDARD")},
+ "resourceLimits": limits,
+ "files": sorted(files, key=lambda value: (value["disk"], value["path"])), "formats": formats,
+ "encryptionConfigAttestation": encryption,
+ "boundedVolumeViews": None if owner.get("runtimeProfile") not in {"bounded-capacity", "journal-bounded"} else {
+ "actualBackingRoots": [str(root) for root in roots],
+ "privateServiceBindingsRequired": True, "originalPhysicalDriveCountIncreased": False}}
+
+
+def logs(request):
+ _, _, owner = owned(request["runId"])
+ if request.get("checkpoint") is True or "afterCursor" in request:
+ cursor = request.get("afterCursor")
+ require(cursor is None or isinstance(cursor, str) and re.fullmatch(r"[A-Za-z0-9=;_-]{1,512}", cursor),
+ "invalid journal cursor")
+ args = ["journalctl", "-u", owner["unit"], "--quiet", "--no-pager", "--all", "-o", "json",
+ "--output-fields=__CURSOR,MESSAGE,MESSAGE_ID"]
+ # +N selects the FIRST N entries after the cursor. A plain -n N
+ # selects the tail and could silently drop the beginning of a window.
+ args += ["--after-cursor", cursor, "-n", "+10000"] if cursor else ["-n", "1"]
+ output = command(*args).stdout
+ lines, observed_cursor, entries, source_message_loss = [], None, 0, []
+ for line in output.splitlines():
+ row = json.loads(line)
+ entry_cursor = row.get("__CURSOR")
+ require(isinstance(entry_cursor, str) and re.fullmatch(r"[A-Za-z0-9=;_-]{1,512}", entry_cursor)
+ and entry_cursor not in {cursor, observed_cursor}, "journal page repeated or omitted its exact entry cursor")
+ message = row.get("MESSAGE")
+ if isinstance(message, list) and all(type(value) is int and 0 <= value <= 255 for value in message):
+ message = bytes(message).decode("utf-8")
+ require(isinstance(message, str), "journal message is not complete text")
+ # Cursor continuity cannot recover entries discarded by journald.
+ # Keep the original notice so the caller records the failed page.
+ if (row.get("MESSAGE_ID") == "a596d6fe7bfa4994828e72309e95d61e"
+ or re.fullmatch(r"Suppressed [1-9][0-9]* messages from .+", message)):
+ source_message_loss.append({"cursor": entry_cursor, "message": message,
+ "messageId": row.get("MESSAGE_ID")})
+ entries += 1
+ require(entries <= 10000, "journal page exceeds its bound; source attribution is incomplete")
+ observed_cursor = entry_cursor
+ lines.append(message)
+ require(observed_cursor is not None or cursor is not None and not lines,
+ "journal did not expose a complete cursor window")
+ return {"unit": owner["unit"], "journal": "\n".join(lines), "beforeCursor": cursor,
+ "afterCursor": observed_cursor or cursor, "complete": not source_message_loss,
+ "sourceMessageLoss": source_message_loss,
+ "entries": entries, "hasMore": cursor is not None and entries == 10000}
+ return {"unit": owner["unit"], "journal": command("journalctl", "-u", owner["unit"], "--no-pager", "-n", "2000").stdout}
+
+
+def bucket_record_operation(request):
+ state, roots, owner = owned(request["runId"])
+ stopped(owner)
+ module, fault = globals()["bucket_records"], fault_module()
+ if request["action"] == "bucket-record-snapshot":
+ tool = checked(state / "heal-meta-inspect", state)
+ require(owner.get("inspectorSha256") and digest(tool) == owner["inspectorSha256"],
+ "bucket-config inspector identity differs")
+ return module.snapshot(roots, {**owner, "bucketPrefix": owner["bucket"]}, request, checked, digest, tool, command)
+ root, owner = fault_root(request)
+ with fault.directory(root) as fd:
+ marker = root / "fault-owner.json"
+ if request["action"] == "bucket-record-apply" and not marker.exists():
+ fault.new_file(fd, marker.name, fault.encode({"schemaVersion": 1, "runId": owner["runId"],
+ "bucketPrefix": owner["bucket"], "mountId": fault.mount_id(fd), "device": os.fstat(fd).st_dev,
+ "leaseExpiresAt": (datetime.now(timezone.utc) + timedelta(minutes=30)).isoformat()}))
+ # Retain the common owner/mount/lease checks without widening its
+ # generic target allowlist to include any system path.
+ binding = fault.checked_owner(fd, owner["runId"], recovery=request["action"] == "bucket-record-recover")
+ if request["action"] == "bucket-record-apply":
+ return module.apply(fd, binding, fault, request)
+ return module.recover(fd, binding, fault, request)
+
+
+def format_schema_fault(request):
+ state, roots, owner = owned(request["runId"])
+ stopped(owner)
+ replacement = replacement_module().attest(state, roots)
+ root, _ = fault_root(request)
+ fault = fault_module()
+ with fault.directory(root) as fd:
+ marker = root / "fault-owner.json"
+ if not marker.exists():
+ fault.new_file(fd, marker.name, fault.encode({"schemaVersion": 1, "runId": owner["runId"],
+ "bucketPrefix": owner["bucket"], "mountId": fault.mount_id(fd), "device": os.fstat(fd).st_dev,
+ "leaseExpiresAt": (datetime.now(timezone.utc) + timedelta(minutes=30)).isoformat()}))
+ fault.checked_owner(fd, owner["runId"])
+ return globals()["schema_faults"].install(fd, replacement, owner, request, fault)
+
+
+def configure_encryption(request):
+ state, _, owner = owned(request["runId"])
+ stopped(owner)
+ return encryption_module().configure(state, owner, request, fault_module())
+
+
+def advance_captured_checkpoint(request, roots, owner, module, fault, before, capture):
+ require(owner.get("runtimeProfile") == "root-restart" and before.get("state") == "T"
+ and request["family"] == "checkpoint" and len(roots) == 4,
+ "checkpoint advance requires its paused owned root-restart service")
+ require(hasattr(os, "pidfd_open") and hasattr(signal, "pidfd_send_signal"),
+ "checkpoint advance requires Linux pidfd support")
+ descriptor = os.pidfd_open(before["pid"], 0)
+ resumed = False
+ try:
+ bound = process(request)
+ require(bound.get("state") == "T" and all(bound.get(name) == before[name] for name in PROCESS_FIELDS),
+ "checkpoint advance lost its original paused process birth")
+ with fault.directory(roots[request["disk"]]) as root_fd:
+ require(os.fstat(root_fd).st_dev == capture.get("device")
+ and fault.mount_id(root_fd) == capture.get("mountId"),
+ "checkpoint capture root device or mount changed before advance")
+ path = module.relative(request)
+ earlier = module.read(root_fd, path, fault)
+ require(module.same_json(module.entry(earlier), capture["captured"]),
+ "captured checkpoint changed before resuming its original process")
+ old = module.checkpoint_value(earlier, request["taskId"], owner["bucket"])
+ old_count = old["admin"]["outcome"]["counters"]["processed"]
+ require(type(old_count) is int and 0 < old_count < 2**64,
+ "earlier checkpoint has no exact positive progress")
+ deadline = time.monotonic() + 60
+ # Register the resume intent before the syscall: interruption or a
+ # lost local receipt must still refreeze this exact held pidfd.
+ resumed = True
+ signal.pidfd_send_signal(descriptor, signal.SIGCONT, None, 0)
+ while True:
+ require(time.monotonic() < deadline,
+ "NOT_OBSERVED: actual checkpoint advance exceeded its frozen 60-second budget")
+ # The held pidfd binds signals. Cheap procfs birth checks avoid
+ # hashing the full candidate between progress and SIGSTOP.
+ fields = Path(f"/proc/{before['pid']}/stat").read_text().rpartition(") ")[2].split()
+ require(len(fields) >= 20 and fields[19].isdigit() and int(fields[19]) == before["startTicks"],
+ "original checkpoint process exited or changed birth during advance")
+ try:
+ raw = module.read(root_fd, path, fault)
+ except FileNotFoundError:
+ raw = None
+ except ValueError as exc:
+ if str(exc) != "file changed while reading": raise
+ raw = None
+ if raw is not None:
+ value = module.checkpoint_value(raw, request["taskId"], owner["bucket"])
+ require(all(module.same_json(value["admin"].get(name), old["admin"].get(name))
+ for name in ("buckets", "options", "set_disk_id")),
+ "checkpoint advance changed original incarnation, options or set")
+ count = value["admin"].get("outcome", {}).get("counters", {}).get("processed")
+ require(type(count) is int and old_count <= count < 2**64,
+ "checkpoint advance regressed or lost its exact progress")
+ if count > old_count:
+ require(time.monotonic() < deadline,
+ "NOT_OBSERVED: actual checkpoint advance exceeded its frozen 60-second budget")
+ signal.pidfd_send_signal(descriptor, signal.SIGSTOP, None, 0)
+ resumed = False
+ break
+ time.sleep(.01)
+ # Signal delivery can precede the actual group stop. Await T on
+ # the same birth within the original observation deadline.
+ while True:
+ require(time.monotonic() < deadline,
+ "NOT_OBSERVED: original process did not stop within its frozen advance budget")
+ fields = Path(f"/proc/{before['pid']}/stat").read_text().rpartition(") ")[2].split()
+ require(len(fields) >= 20 and fields[19].isdigit() and int(fields[19]) == before["startTicks"],
+ "checkpoint process changed birth before its actual group stop")
+ if fields[0] == "T": break
+ time.sleep(.001)
+ after = process(request)
+ require(after.get("state") == "T" and all(after.get(name) == before[name] for name in PROCESS_FIELDS),
+ "advanced checkpoint did not freeze the original candidate process")
+ observed = module.stable_census(roots, request, fault)
+ selected = [row for row in observed["records"]
+ if (row["disk"], row["relativePath"]) == (request["disk"], request["relativePath"])]
+ require(len(selected) == 1 and selected[0].get("present") is True,
+ "NOT_OBSERVED: advanced original checkpoint disappeared before freeze")
+ row = selected[0]
+ current = base64.b64decode(row["rawBase64"], validate=True)
+ require(module.same_json(module.entry(current), {name: row.get(name) for name in ("size", "sha256")}),
+ "advanced checkpoint census bytes differ")
+ # This validates the original digest, all counters, time and cursor,
+ # without installing or otherwise modifying any product record.
+ module.transform(current, {**request, "kind": "stale-generation"}, owner, None, earlier=earlier)
+ final_count = module.checkpoint_value(current, request["taskId"], owner["bucket"])["admin"]["outcome"]["counters"]["processed"]
+ require(final_count >= count, "checkpoint progress regressed between observation and actual freeze")
+ return {**capture, "observation": observed, "processIdentity": {name: after[name] for name in PROCESS_FIELDS},
+ "processBound": True, "serviceFrozen": True, "checkpointAdvancedAndFrozen": True,
+ "pidfdBound": True, "observedProcessed": count, "naturalProductProgressObserved": True}
+ finally:
+ try:
+ if resumed:
+ try: signal.pidfd_send_signal(descriptor, signal.SIGSTOP, None, 0)
+ except ProcessLookupError: pass
+ finally:
+ os.close(descriptor)
+
+
+def journal_operation(request):
+ state, roots, owner = owned(request["runId"])
+ require(digest(state / "rustfs") == owner["binarySha256"], "durable-record candidate changed")
+ race_root_census = (owner.get("runtimeProfile") in {"manual", "frozen-race-legacy"}
+ and request.get("action") == "journal-census" and request.get("family") == "root"
+ and set(request) == {"runId", "action", "family", "taskId"})
+ require(race_root_census or owner.get("runtimeProfile") in {"root-restart", "mrf-crash", "journal-bounded", "incarnation-checkpoint", "incarnation-mrf"},
+ "durable records require the owned recovery runtime or stopped read-only race root census")
+ require(set(request) <= {"runId", "action", "family", "taskId", "disk", "relativePath", "faultId", "kind", "expectedBefore", "expected", "captureId", "mrfIdentity"},
+ "durable-record adapter accepts no arbitrary system paths or commands")
+ if owner.get("runtimeProfile") == "journal-bounded":
+ require(request.get("family") == "checkpoint" or request.get("family") == "root"
+ and request.get("action") in {"journal-census", "journal-observe"},
+ "bounded journal runtime mutates only the owned Admin checkpoint family")
+ roots = volume_views(state, roots, owner)
+ module, fault = globals()["journal_fault_backend"], fault_module()
+ if request["action"] in {"journal-observe", "journal-capture", "journal-capture-advance"}:
+ fields = {"runId", "action", "family", "taskId", "expected"}
+ if request["action"] in {"journal-capture", "journal-capture-advance"}:
+ fields |= {"disk", "relativePath", "faultId", "expectedBefore"}
+ require(set(request) == fields, "live durable-record observation/capture has unknown mutation or path inputs")
+ before = process(request)
+ expected = request.get("expected")
+ require(isinstance(expected, dict) and before.get("pid", 0) > 1
+ and all(name in expected and before.get(name) == expected[name] for name in PROCESS_FIELDS),
+ "live durable-record observation lost its exact service birth or candidate")
+ if request["action"] == "journal-capture-advance":
+ require(owner.get("runtimeProfile") == "root-restart" and before.get("state") == "T"
+ and request["family"] == "checkpoint" and len(roots) == 4,
+ "checkpoint advance requires its paused owned root-restart service")
+ require(hasattr(os, "pidfd_open") and hasattr(signal, "pidfd_send_signal"),
+ "checkpoint advance requires Linux pidfd support")
+ observed = module.stable_census(roots, request, fault)
+ if request["action"] in {"journal-capture", "journal-capture-advance"}:
+ require(request["family"] == "checkpoint" and type(request["disk"]) is int and 0 <= request["disk"] < 4,
+ "earlier checkpoint capture has no exact owned Admin record/disk")
+ selected = [row for row in observed["records"]
+ if (row["disk"], row["relativePath"]) == (request["disk"], request["relativePath"])]
+ require(len(selected) == 1 and selected[0].get("present") is True
+ and module.same_json({name: selected[0].get(name) for name in ("size", "sha256")}, request["expectedBefore"]),
+ "NOT_OBSERVED: earlier checkpoint differs from its two stable process-bound reads")
+ with fault.directory(roots[request["disk"]]) as fd:
+ observed = {**module.capture(fd, request, owner, fault), "observation": observed}
+ if request["action"] == "journal-capture-advance":
+ return advance_captured_checkpoint(request, roots, owner, module, fault, before, observed)
+ after = process(request)
+ require(all(after.get(name) == before.get(name) for name in PROCESS_FIELDS),
+ "service restarted while observing exact durable records")
+ return {**observed, "processIdentity": {name: before[name] for name in PROCESS_FIELDS},
+ "processBound": True, "serviceFrozen": before.get("state") == after.get("state") == "T"}
+ require("expected" not in request, "stopped durable-record operations accept no live-process override")
+ stopped(owner)
+ if request["action"] == "journal-census":
+ return module.census(roots, request, fault)
+ require(request["action"] in {"journal-apply", "journal-recover"}
+ and type(request.get("disk")) is int and 0 <= request["disk"] < 4, "invalid durable-record operation or slot")
+ root = roots[request["disk"]]
+ with fault.directory(root) as fd:
+ if request["action"] == "journal-apply":
+ if request["family"] == "mrf":
+ observed = globals()["mrf"].read_records(state, roots, owner, checked)
+ path = request.get("relativePath", "")
+ match = re.fullmatch(r"\.rustfs.sys/\.heal-mrf-snapshot\.([01])\.bin", path)
+ require(match is not None, "MRF fault escaped its exact committed payload path")
+ committed = globals()["mrf"].checkpoint(observed, replica=(request["disk"], int(match[1])))
+ identity = module.active_mrf_identity(committed, owner["bucket"])
+ require(module.same_json(request.get("mrfIdentity"), identity),
+ "MRF fault changed its frozen owner, sequence, payload or bucket incarnation")
+ return module.install(fd, request, owner, fault, globals()["mrf"])
+ return module.recover(fd, request, owner, fault)
+
+
+def network_operation(request):
+ state, _, owner = owned(request["runId"])
+ module = globals()["network_gate_backend"]
+ module.identities(owner)
+ action = request["action"]
+ require(set(request) <= {"runId", "action", "addresses", "localIndex", "blockedIndices", "expired"},
+ "network adapter accepts no external paths, ports, rules or commands")
+ if action == "network-release":
+ require(set(request) <= {"runId", "action", "expired"},
+ "network release accepts only its expiry proof")
+ return module.release(state, owner, expired=request.get("expired", False))
+ identity = process(request)
+ require(identity["pid"] > 1 and identity["executable"] == str(state / "rustfs"),
+ "network fault requires an actual owned candidate process")
+ listeners = command("ss", "-H", "-lntp", "sport = :9000").stdout
+ pids = {int(value) for value in re.findall(r"pid=([0-9]+)", listeners)}
+ require(pids == {identity["pid"]} and listeners.strip(),
+ "TCP 9000 is not exclusively owned by this run's candidate")
+ if action == "network-preflight":
+ values = module.resolve()
+ local = []
+ for index, address in enumerate(values):
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client:
+ try: client.bind((address, 0))
+ except OSError: continue
+ local.append(index)
+ require(len(local) == 1, "fleet aliases do not bind exactly one actual local address")
+ return {"addresses": values, "localIndex": local[0], "process": identity,
+ "exclusiveServicePort": 9000, "nftVersion": command("nft", "--version").stdout.strip()}
+ if action == "network-prepare":
+ return module.prepare(state, owner, request, globals()["NETWORK_GATE_SOURCE"])
+ if action == "network-activate":
+ return module.activate(state, owner)
+ require(action == "network-observe", "unsupported network fault operation")
+ return {**module.observe(state, owner), "process": identity}
+
+
+def cleanup(request):
+ state, roots, owner = owned(request["runId"])
+ command("systemctl", "stop", owner["unit"], check=False)
+ pid = command("systemctl", "show", "-p", "MainPID", "--value", owner["unit"], check=False).stdout.strip()
+ require(pid in {"", "0"}, "run-owned service remains active")
+ stopped(owner)
+ if (state / "network-gate.json").exists():
+ globals()["network_gate_backend"].release(state, owner)
+ gate_module().release_gate(state, roots)
+ quorum_gate_module().release(state, roots)
+ replacement_module().attest(state, roots)
+ if owner.get("runtimeProfile") in {"bounded-capacity", "journal-bounded"}:
+ volume_module().dispose(state, roots, owner)
+ command("systemctl", "reset-failed", owner["unit"], check=False)
+ for root in (*roots, state):
+ for directory, children, names in os.walk(root, followlinks=False):
+ for name in (*children, *names):
+ checked(Path(directory) / name, root)
+ shutil.rmtree(root)
+ require(not state.exists() and all(not root.exists() for root in roots), "run namespace remains")
+ return {"cleanup": "PASS", "unitStopped": True, "rootsRemoved": True, "hostname": owner["hostname"]}
+
+
+def namespace_state(request):
+ """Inspect one exact run namespace without requiring its owner marker."""
+ require(set(request) == {"runId", "action"}, "namespace inspection accepts only the run ID")
+ state, roots = paths(request["runId"])
+ service = {}
+ for field in ("LoadState", "ActiveState", "SubState", "MainPID"):
+ service[field] = command("systemctl", "show", "-p", field, "--value",
+ unit(request["runId"]), check=False).stdout.strip()
+ def marker_evidence(folder):
+ path = checked(folder / "owner.json", folder)
+ try:
+ descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
+ except FileNotFoundError:
+ return {"present": False}
+ try:
+ info = os.fstat(descriptor)
+ require(stat.S_ISREG(info.st_mode) and info.st_nlink == 1 and info.st_size <= 16384,
+ "namespace owner is not a bounded non-linked regular file")
+ raw = os.read(descriptor, 16385)
+ after = os.fstat(descriptor)
+ require(len(raw) == info.st_size and (info.st_dev, info.st_ino, info.st_size, info.st_mtime_ns, info.st_ctime_ns)
+ == (after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns, after.st_ctime_ns),
+ "namespace owner changed during inspection")
+ value = json.loads(raw)
+ require(isinstance(value, dict), "namespace owner is not an object")
+ return {"present": True, "size": len(raw), "uid": info.st_uid, "gid": info.st_gid,
+ "mode": stat.S_IMODE(info.st_mode), "device": info.st_dev, "inode": info.st_ino,
+ "sha256": hashlib.sha256(raw).hexdigest(),
+ "canonicalSha256": hashlib.sha256(json.dumps(value, sort_keys=True, separators=(",", ":")).encode()).hexdigest()}
+ finally:
+ os.close(descriptor)
+ retained_states = [checked(state.parent / name / state.name, state.parent).exists()
+ for name in ("retained", "retained-" + state.name)]
+ retained_roots = [[checked(root.parent.parent / name / root.name, root.parent.parent).exists()
+ for name in ("heal-validation-retained", "heal-validation-retained-" + root.name)] for root in roots]
+ require(sum(retained_states) <= 1 and all(sum(row) <= 1 for row in retained_roots),
+ "namespace has both shared and per-run retained copies")
+ return {"runId": request["runId"], "service": service,
+ "state": {"path": str(state), "exists": state.exists(),
+ "ownerMarker": (state / "owner.json").is_file(), "ownerEvidence": marker_evidence(state)},
+ "roots": [{"path": str(root), "exists": root.exists(),
+ "ownerMarker": (root / "owner.json").is_file(), "ownerEvidence": marker_evidence(root),
+ "data": (root / "data").is_dir()} for root in roots],
+ "retained": {"state": any(retained_states), "roots": [any(row) for row in retained_roots]},
+ "inspectionOnly": True, "mutated": False}
+
+
+def retained_scene_operation(request):
+ return globals()["retained_scene_backend"].run(request, globals())
+
+
+def audit_finalize(request):
+ """Finalize only an invalid-ID rejection proved identical to its archived pre-fault scene."""
+ _, _, owner = owned(request["runId"])
+ stopped(owner)
+ fault_id = request.get("rejectedFaultId", "")
+ require(re.fullmatch(r"[A-J][0-9]{2}-[a-z0-9-]{49,128}", fault_id) is not None,
+ "audited finalization only supports an overlength ID rejected before injection")
+ require(request.get("binarySha256") == owner["binarySha256"], "audited candidate identity differs")
+ root, _ = fault_root(request)
+ for suffix in (".intent.json", ".original", ".applied.json"):
+ require(not checked(root / "quarantine" / (fault_id + suffix), root).exists(),
+ "rejected fault unexpectedly has intent or backup bytes; retain scene")
+ observed = snapshot({**request, "quiescent": True})
+ content = {key: observed[key] for key in ("files", "formats")}
+ scene_sha = hashlib.sha256(json.dumps(content, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
+ require(re.fullmatch(r"[0-9a-f]{64}", request.get("expectedSceneSha256", "")) is not None
+ and scene_sha == request["expectedSceneSha256"], "physical scene drifted from archived pre-fault evidence; retain namespace")
+ result = cleanup(request)
+ return {**result, "faultNeverApplied": True, "archivedPreFaultSceneEqual": True, "sceneSha256": scene_sha,
+ "backupRestorationUsed": False, "productRepairClaimed": False}
+
+
+def quota_audit_finalize(request):
+ """Remove the private quota fixture only after exact pre-fault rescue."""
+ require(set(request) == {"runId", "action", "bucket", "binarySha256", "expectedSceneSha256"},
+ "quota finalization accepts only the exact archived namespace and scene")
+ state, roots, owner = owned(request["runId"])
+ stopped(owner)
+ require(request["runId"].endswith("-h1") and owner.get("runtimeProfile") == "manual"
+ and owner.get("bucket") == request["bucket"] and request["binarySha256"] == owner["binarySha256"]
+ and digest(state / "rustfs") == owner["binarySha256"], "quota rescue candidate, bucket or private runtime differs")
+ require(owner.get("volumes") == [str(root / "data") for root in roots]
+ and all(not (state / name).exists() for name in ("network-gate.json", "drive-gate.json", "quorum-gate.json", "bounded-volumes.json")),
+ "quota rescue cannot remove a shared/foreign volume or another active fault backend")
+ observed = snapshot({"runId": request["runId"], "action": "snapshot", "quiescent": True})
+ content = {key: observed[key] for key in ("files", "formats")}
+ scene_sha = hashlib.sha256(json.dumps(content, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
+ require(re.fullmatch(r"[0-9a-f]{64}", request.get("expectedSceneSha256", "")) is not None
+ and scene_sha == request["expectedSceneSha256"], "quota rescue did not restore the complete original fixture")
+ result = cleanup(request)
+ return {**result, "privateBucketQuotaNamespaceRemoved": True, "archivedPreFaultSceneEqual": True,
+ "sceneSha256": scene_sha, "serviceStartedDuringRecovery": False,
+ "backupRestorationUsed": True, "productRepairClaimed": False}
+
+
+def journal_rejection_audit_finalize(request):
+ """Finalize an unchanged MRF namespace only when no journal mutation began."""
+ require(set(request) == {"runId", "action", "bucket", "binarySha256", "rejectedFaultId", "expectedSceneSha256", "expectedMrfSha256"},
+ "journal rejection finalization accepts only an exact archived identity")
+ require(request["action"] in {"journal-rejection-audit", "journal-rejection-audit-finalize"},
+ "unsupported journal rejection audit action")
+ state, roots, owner = owned(request["runId"])
+ stopped(owner)
+ require(request["runId"].endswith("-h2") and owner.get("runtimeProfile") == "mrf-crash"
+ and owner.get("bucket") == request["bucket"] and request["binarySha256"] == owner["binarySha256"]
+ and digest(state / "rustfs") == owner["binarySha256"]
+ and request["rejectedFaultId"] == "F07-mrf-checksum-0-0-0",
+ "journal rejection candidate, bucket, runtime or first fault identity differs")
+ for root in roots:
+ with fault.directory(root) as fd, fault.parent(fd, ["quarantine"]) as backup:
+ require(not os.listdir(backup), "journal mutation backup or intent exists; retain namespace")
+ records = mrf_records({"runId": request["runId"], "action": "mrf-records", "quiescent": True})
+ mrf_sha = hashlib.sha256(json.dumps(records, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
+ require(re.fullmatch(r"[0-9a-f]{64}", request.get("expectedMrfSha256", "")) is not None
+ and mrf_sha == request["expectedMrfSha256"], "journal rejection MRF bytes drifted; retain namespace")
+ observed = snapshot({"runId": request["runId"], "action": "snapshot", "quiescent": True})
+ content = {key: observed[key] for key in ("files", "formats")}
+ scene_sha = hashlib.sha256(json.dumps(content, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
+ require(re.fullmatch(r"[0-9a-f]{64}", request.get("expectedSceneSha256", "")) is not None
+ and scene_sha == request["expectedSceneSha256"], "journal rejection scene drifted; retain namespace")
+ result = cleanup(request) if request["action"] == "journal-rejection-audit-finalize" else {"readyToFinalize": True}
+ return {**result, "faultNeverApplied": True, "archivedPreFaultSceneEqual": True,
+ "sceneSha256": scene_sha, "backupRestorationUsed": False, "productRepairClaimed": False}
+
+
+def journal_retained_audit_finalize(request):
+ """Export an advanced MRF fault fixture; never restore its earlier product record."""
+ require(set(request) == {"runId", "action", "bucket", "binarySha256", "faults", "originalBackup",
+ "expectedSceneSha256", "expectedMrfSha256"}, "retained journal audit accepts only its archived identity")
+ require(request["action"] in {"journal-retained-audit", "journal-retained-audit-finalize"}, "invalid retained journal audit")
+ state, roots, owner = owned(request["runId"])
+ stopped(owner)
+ require(request["runId"].endswith("-h2") and owner.get("runtimeProfile") == "mrf-crash"
+ and owner.get("bucket") == request["bucket"] and request["binarySha256"] == owner["binarySha256"]
+ and digest(state / "rustfs") == owner["binarySha256"]
+ and gate_module().checked_gate(state, roots) is None,
+ "retained journal candidate, bucket, runtime or member gate differs")
+ module, fault = globals()["journal_fault_backend"], fault_module()
+ faults, originals = request["faults"], request["originalBackup"]
+ require(isinstance(faults, list) and len(faults) <= 1 and isinstance(originals, dict)
+ and set(originals) == {item.get("faultId") for item in faults}, "retained journal backup bindings differ")
+ by_disk = {}
+ exported = []
+ for item in faults:
+ require(isinstance(item, dict) and set(item) == {"schemaVersion", "runId", "disk", "family", "taskId", "relativePath",
+ "faultId", "binarySha256", "device", "mountId", "productRepairClaimed", "kind", "before", "injected"}
+ and item.get("disk") == 0 and type(item["disk"]) is int and item.get("family") == "mrf"
+ and item.get("taskId") is None and item.get("kind") == "checksum"
+ and item.get("faultId") == "F07-mrf-checksum-0-0-0"
+ and item.get("relativePath") == ".rustfs.sys/.heal-mrf-snapshot.0.bin", "retained journal fault escaped its exact identity")
+ by_disk[item["disk"]] = item
+ for disk, root in enumerate(roots):
+ with fault.directory(root) as fd, fault.parent(fd, ["quarantine"]) as backup:
+ item = by_disk.get(disk)
+ names = set() if item is None else {item["faultId"] + suffix for suffix in (".original", ".intent.json", ".applied.json")}
+ require(set(os.listdir(backup)) == names, "retained journal has an unarchived mutation or missing backup")
+ if item is None: continue
+ identity = module.binding(fd, item, owner, fault)
+ require(all(module.same_json(item.get(key), value) for key, value in identity.items()), "retained journal physical fault binding differs")
+ original = fault.read_file(backup, item["faultId"] + ".original", module.LIMIT)
+ require(isinstance(originals[item["faultId"]], str)
+ and len(originals[item["faultId"]]) <= (module.LIMIT + 2) // 3 * 4,
+ "retained journal original bytes exceed their bound")
+ require(base64.b64decode(originals[item["faultId"]], validate=True) == original
+ and module.entry(original) == item["before"], "retained journal original backup drifted")
+ raw_intent = fault.read_file(backup, item["faultId"] + ".intent.json", 16384)
+ raw_applied = fault.read_file(backup, item["faultId"] + ".applied.json", 16384)
+ require(module.same_json(module.decode_json(raw_intent), item)
+ and module.same_json(module.decode_json(raw_applied), item)
+ and module.entry(module.transform(original, item, owner, globals()["mrf"])) == item["injected"],
+ "retained journal intent, applied receipt or exact corruption differs")
+ exported.append({"disk": disk, "faultId": item["faultId"],
+ "original": {**module.entry(original), "rawBase64": base64.b64encode(original).decode()},
+ "intent": {**module.entry(raw_intent), "rawBase64": base64.b64encode(raw_intent).decode()},
+ "applied": {**module.entry(raw_applied), "rawBase64": base64.b64encode(raw_applied).decode()}})
+ records = mrf_records({"runId": request["runId"], "action": "mrf-records", "quiescent": True})
+ mrf_sha = hashlib.sha256(json.dumps(records, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
+ observed = snapshot({"runId": request["runId"], "action": "snapshot", "quiescent": True})
+ content = {key: observed[key] for key in ("files", "formats")}
+ scene_sha = hashlib.sha256(json.dumps(content, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
+ require(all(re.fullmatch(r"[0-9a-f]{64}", request.get(name, "")) for name in ("expectedMrfSha256", "expectedSceneSha256"))
+ and mrf_sha == request["expectedMrfSha256"] and scene_sha == request["expectedSceneSha256"],
+ "retained journal or physical scene drifted; retain namespace")
+ result = cleanup(request) if request["action"] == "journal-retained-audit-finalize" else {"readyToFinalize": True}
+ return {**result, "originalEvidenceExported": True, "originalJournalBackups": exported, "retainedMrf": records,
+ "sceneSha256": scene_sha, "backupRestorationUsed": False, "productRepairClaimed": False}
+
+
+def task_records(request):
+ """Read three exact Admin journal names; never mutate system metadata."""
+ state, roots, owner = owned(request["runId"])
+ roots = volume_views(state, roots, owner)
+ token = request.get("taskId")
+ require(isinstance(token, str) and str(uuid.UUID(token)) == token, "noncanonical Admin journal task ID")
+ require(digest(state / "rustfs") == owner["binarySha256"], "journal candidate changed")
+ if request.get("quiescent") is True:
+ stopped(owner)
+ else:
+ pid = int(command("systemctl", "show", "-p", "MainPID", "--value", owner["unit"]).stdout.strip() or "0")
+ require(pid > 0 and Path(f"/proc/{pid}/exe").resolve(strict=True) == state / "rustfs", "journal service is not run-owned")
+ records = []
+ for disk, root in enumerate(roots):
+ data = root / "data"
+ for kind, prefix, limit in (("intent", "root-heal-", 64 * 1024),
+ ("terminal", "terminal-root-heal-", 64 * 1024),
+ ("report", "heal-terminal-report-", 8 * 1024**2)):
+ relative = ".rustfs.sys/" + prefix + token + ".json"
+ path = checked(data / relative, data)
+ entry = {"disk": disk, "kind": kind, "relativePath": relative, "present": False}
+ try:
+ descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW)
+ except FileNotFoundError:
+ records.append(entry)
+ continue
+ with os.fdopen(descriptor, "rb") as stream:
+ info = os.fstat(stream.fileno())
+ require(stat.S_ISREG(info.st_mode) and info.st_nlink == 1 and info.st_size <= limit,
+ "Admin journal exceeds its exact regular-file budget")
+ raw = stream.read(limit + 1)
+ after = os.fstat(stream.fileno())
+ require(len(raw) == info.st_size and len(raw) <= limit
+ and all(getattr(after, name) == getattr(info, name) for name in
+ ("st_ino", "st_dev", "st_size", "st_mtime_ns", "st_ctime_ns", "st_nlink")),
+ "Admin journal changed during its bounded read")
+ entry.update(present=True, size=len(raw), sha256=hashlib.sha256(raw).hexdigest(), value=json.loads(raw))
+ records.append(entry)
+ return {"taskId": token, "binarySha256": owner["binarySha256"], "readOnly": True, "records": records}
+
+
+def generation_operation(request):
+ state, roots, owner = owned(request['runId'])
+ stopped(owner)
+ require(digest(state / 'rustfs') == owner['binarySha256'], 'generation candidate binary changed')
+ disk = request['disk']
+ require(type(disk) is int and 0 <= disk < 4, 'generation operation escaped an original independent root')
+ operation = request['action']
+ require(operation in {'generation-capture', 'generation-install', 'generation-recover'}, 'unregistered generation operation')
+ backend = globals()['generation_backend']
+ with fault_module().directory(roots[disk]) as root:
+ method = {'generation-capture': backend.capture, 'generation-install': backend.install, 'generation-recover': backend.recover}[operation]
+ return method(root, owner, request, fault_module())
+
+
+def orphan_operation(request):
+ state, roots, owner = owned(request['runId'])
+ stopped(owner)
+ require(owner.get('runtimeProfile') in {'orphan-preserve', 'orphan-before', 'orphan-after', 'outcome-grace'}
+ and digest(state / 'rustfs') == owner['binarySha256'], 'C10 runtime/candidate changed')
+ fault, generation, backend = fault_module(), globals()['generation_backend'], globals()['orphan_backend']
+ action = request['action']
+ require(owner.get('runtimeProfile') != 'outcome-grace' or action in {
+ 'orphan-observe', 'orphan-capture', 'orphan-arm', 'orphan-install', 'orphan-recover'},
+ 'J02 grace permits only stopped original-object archive/replay/recovery')
+ tool = state / 'heal-shared-fixture'
+ if action == 'orphan-install-tool':
+ require(re.fullmatch(r'[0-9a-f]{64}', request['sha256']), 'C10 encoder digest absent')
+ with fault.directory(state) as fd:
+ raw = fault.read_file(fd, tool.name)
+ require(fault.digest(raw) == request['sha256'] and os.stat(tool, follow_symlinks=False).st_uid == 0,
+ 'C10 uploaded encoder changed or is not privately root owned')
+ fault.new_file(fd, 'shared-tool.json', fault.encode({'runId': owner['runId'],
+ 'ownerSha256': digest(state / 'owner.json'), 'binarySha256': owner['binarySha256'], 'sha256': request['sha256']}))
+ tool.chmod(0o500)
+ return {'installed': True, 'sha256': request['sha256']}
+ disk = request['disk']
+ require(type(disk) is int and 0 <= disk < 4, 'C10 escaped an original independent root')
+ with fault.directory(roots[disk]) as root:
+ if action == 'orphan-multipart': return backend.multipart(root, fault)
+ if action == 'orphan-observe':
+ present, _, files = backend.scene(root, owner, request, fault, generation)
+ return {'present': present, 'files': files, 'nodeEpoch': time.time()}
+ if action == 'orphan-convert':
+ with fault.directory(state) as fd:
+ record = json.loads(fault.read_file(fd, 'shared-tool.json', 65536))
+ require(record == {'runId': owner['runId'], 'ownerSha256': digest(state / 'owner.json'),
+ 'binarySha256': owner['binarySha256'], 'sha256': digest(tool)}, 'C10 encoder binding changed')
+ return backend.convert(root, owner, request, fault, generation, checked(tool, state))
+ methods = {'orphan-capture': backend.capture, 'orphan-arm': backend.arm, 'orphan-install': backend.install,
+ 'orphan-recover': backend.recover, 'orphan-disarm': backend.disarm}
+ require(action in methods, 'unregistered C10 operation')
+ return methods[action](root, owner, request, fault, generation)
+
+
+def mrf_records(request):
+ require(set(request) <= {"runId", "action", "quiescent"}, "MRF observer does not accept paths or mutation options")
+ state, roots, owner = owned(request["runId"])
+ if request.get("quiescent") is True:
+ stopped(owner)
+ else:
+ pid = int(command("systemctl", "show", "-p", "MainPID", "--value", owner["unit"]).stdout.strip() or "0")
+ require(pid > 0 and Path(f"/proc/{pid}/exe").resolve(strict=True) == state / "rustfs", "MRF observer requires its owned process")
+ module = globals().get("mrf")
+ require(module is not None, "MRF observer source missing")
+ return module.read_records(state, roots, owner, checked)
+
+
+def replacement_records(request):
+ require(set(request) <= {"runId", "action", "taskId", "quiescent"},
+ "replacement journal accepts no paths or mutation options")
+ state, roots, owner = owned(request["runId"])
+ require(owner.get("runtimeProfile") in {"replacement", "replacement-target"}
+ and digest(state / "rustfs") == owner["binarySha256"], "replacement checkpoint candidate/runtime changed")
+ if request.get("quiescent") is True: stopped(owner)
+ else: require(process(request)["pid"] > 1, "replacement checkpoint has no live owned service")
+ return globals()["replacement_journal"].read_records(roots, request.get("taskId"), checked)
+
+
+def main(request):
+ require(os.geteuid() == 0, "node adapter requires passwordless sudo")
+ actions = {"preflight": preflight, "reserve": reserve, "start": start, "stop": stop, "snapshot": snapshot,
+ "namespace-state": namespace_state,
+ "mrf-records": mrf_records,
+ "logs": logs, "cleanup": cleanup, "audit-finalize": audit_finalize, "install-inspector": install_inspector,
+ "apply-fault": apply_fault, "recover-fault": recover_fault, "verify-part": verify_part, "verify-parts": verify_parts,
+ "verify-inline": verify_inline, "drive-gate": drive_gate, "drive-gate-probe": drive_gate_probe,
+ "drive-gate-activate": drive_gate_activate, "gate-state": drive_gate_state,
+ "drive-gate-release-live": drive_gate_release_live, "drive-gate-arm-live": drive_gate_arm_live}
+ for action in ("incarnation-census", "incarnation-capture", "incarnation-install", "incarnation-finalize", "incarnation-phase"):
+ actions[action] = incarnation_operation
+ actions["task-records"] = task_records
+ actions["mpu-scene"] = lambda value: globals()["mpu_scene_backend"].run(value, globals())
+ actions["mpu-race-scene"] = lambda value: globals()["mpu_race_backend"].run(value, globals())
+ actions["network-delete-records"] = lambda value: globals()["network_delete_records_backend"].run(value, globals())
+ actions["race-io-cap"] = lambda value: globals()["race_io_backend"].run(value, globals())
+ actions["race-io-observe"] = lambda value: globals()["race_io_backend"].run(value, globals())
+ actions["mpu-race-backups"] = lambda value: globals()["mpu_race_backups_backend"].run(value, globals())
+ actions["version-race-backups"] = lambda value: globals()["mpu_race_backups_backend"].run(value, globals())
+ actions["process-clock"] = process_clock
+ actions["environment-capabilities"] = environment_capabilities
+ actions['endpoint-resolution'] = endpoint_resolution
+ actions["replacement-admission-preflight"] = replacement_admission_preflight
+ for action in ("retained-scene-audit", "retained-scene-isolate"):
+ actions[action] = retained_scene_operation
+ for action in ("owner-markers-audit", "owner-markers-recover"):
+ actions[action] = lambda value: globals()["owner_recovery_backend"].run(value, globals())
+ actions["outcome-legacy-phase"] = outcome_legacy_phase
+ actions["quota-audit-finalize"] = quota_audit_finalize
+ actions["journal-rejection-audit-finalize"] = journal_rejection_audit_finalize
+ actions["journal-rejection-audit"] = journal_rejection_audit_finalize
+ actions["journal-retained-audit"] = journal_retained_audit_finalize
+ actions["journal-retained-audit-finalize"] = journal_retained_audit_finalize
+ for action in ("journal-census", "journal-observe", "journal-capture", "journal-capture-advance", "journal-apply", "journal-recover"):
+ actions[action] = journal_operation
+ for action in ('generation-capture', 'generation-install', 'generation-recover'):
+ actions[action] = generation_operation
+ for action in ('orphan-install-tool', 'orphan-observe', 'orphan-multipart', 'orphan-convert',
+ 'orphan-capture', 'orphan-arm', 'orphan-install', 'orphan-recover', 'orphan-disarm'):
+ actions[action] = orphan_operation
+ for action in ("bucket-record-snapshot", "bucket-record-apply", "bucket-record-recover"):
+ actions[action] = bucket_record_operation
+ for action in ("quorum-gate", "quorum-gate-probe"):
+ actions[action] = quorum_gate_operation
+ for action in ("bounded-volumes-create", "bounded-volumes-fill", "bounded-volumes-release", "bounded-volumes-witness"):
+ actions[action] = volume_operation
+ actions["format-schema-fault"] = format_schema_fault
+ actions["encryption-configure"] = configure_encryption
+ for action in ("network-preflight", "network-prepare", "network-activate", "network-observe", "network-release"):
+ actions[action] = network_operation
+ actions["replacement-records"] = replacement_records
+ actions.update({"replace-drive": replace_drive, "replacement-state": replacement_state, "replacement-probe": replacement_probe})
+ actions.update({"format-export": format_export, "foreign-format": foreign_format, "restore-replacement": restore_replacement})
+ actions.update({"process": process, "process-metrics": process_metrics, "process-signal": process_signal, "process-graceful": process_graceful, "process-crash": process_crash})
+ actions["caller-socket"] = lambda req: caller_socket_backend.run(req, globals())
+ require(request.get("action") in actions, "unsupported node operation")
+ try:
+ result = actions[request["action"]](request)
+ except ValueError as exc:
+ if request["action"] not in {"journal-observe", "journal-capture"} or not str(exc).startswith("NOT_OBSERVED:"):
+ raise
+ result = {"observationUnavailable": str(exc)}
+ print(json.dumps(result))
diff --git a/scripts/validation_heal/full_restart.py b/scripts/validation_heal/full_restart.py
new file mode 100644
index 00000000..83980c69
--- /dev/null
+++ b/scripts/validation_heal/full_restart.py
@@ -0,0 +1,365 @@
+#!/usr/bin/env python3
+"""F05: all owned peers restart with original durable root responsibility."""
+
+from __future__ import annotations
+
+import argparse
+import copy
+from datetime import datetime, timedelta, timezone
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import secrets
+import signal
+import sys
+import time
+import uuid
+
+try:
+ from .heal_probe import Probe, canonical, compare_inventories, payload, require, save
+ from .heal_workload import Recorder, seed, verify_seed
+ from .healthy import Fleet, physical_content, validate_topology
+ from .inspector import verify as verify_inspector
+ from .matrix import select
+ from .heal_fault import bounded_fault_id
+ from .shards import pending_inventory, validate_repaired_physical
+ from .cursor import OBJECTS, SIZE, TASK_SECONDS, map_cohorts, verify_all_parts, validate_missing_batch
+ from .root_restart import (PATH, OPTIONS, records, owned_records, root_intent, same_owner,
+ partial_physical, recovery_status)
+ from .stop import wait_partial, idle_fleet
+ from .fleet_node import PROCESS_FIELDS
+ from .versions import public_version_id
+except ImportError:
+ from heal_probe import Probe, canonical, compare_inventories, payload, require, save
+ from heal_workload import Recorder, seed, verify_seed
+ from healthy import Fleet, physical_content, validate_topology
+ from inspector import verify as verify_inspector
+ from matrix import select
+ from heal_fault import bounded_fault_id
+ from shards import pending_inventory, validate_repaired_physical
+ from cursor import OBJECTS, SIZE, TASK_SECONDS, map_cohorts, verify_all_parts, validate_missing_batch
+ from root_restart import PATH, OPTIONS, records, owned_records, root_intent, same_owner, partial_physical, recovery_status
+ from stop import wait_partial, idle_fleet
+ from fleet_node import PROCESS_FIELDS
+ from versions import public_version_id
+
+
+CASE_ID = "heal.full-cluster-restart"
+BRANCHES = ("all-graceful-restart", "all-restart-with-mpu", "repeat-full-restart")
+
+
+def manifest(fixtures):
+ wanted = {(bucket, entry["key"], entry["versionId"]): entry
+ for bucket, fixture in fixtures.items() for entry in fixture["expected"]}
+ require(len(wanted) == sum(len(fixture["expected"]) for fixture in fixtures.values())
+ and len(wanted) > 1024, "full restart lacks its frozen complete ACK manifest")
+ return wanted
+
+
+def responsibility(current, wanted, repaired):
+ outcome = current.get("outcome") or {}
+ require(current.get("summary") == "finished" and outcome.get("execution", {}).get("state") == "completed"
+ and outcome.get("coverage") == "complete" and outcome.get("objectsTruncated") is True,
+ "root recovery has no complete responsibility or bounded diagnostic receipts")
+ require(repaired and set(repaired) <= set(wanted), "root recovery repair set escaped original ACKs")
+ counts = {"processed": len(wanted), "healed": len(repaired), "unchanged": len(wanted) - len(repaired),
+ "skipped": 0, "failed": 0, "unknown": 0, "attemptFailures": 0, "overflowed": False}
+ counters = outcome.get("counters", {})
+ require(all(type(counters.get(name)) is type(value) and counters[name] == value for name, value in counts.items()),
+ "root recovery counters omit ACKs, lost responsibility or claim false repair")
+ objects = outcome.get("objects")
+ require(isinstance(objects, list) and 0 < len(objects) <= 128, "root diagnostics exceed their bounded contract")
+ seen, incarnations = set(), {}
+ for receipt in objects:
+ identity = receipt.get("identity", {})
+ pair = identity.get("bucket"), identity.get("object"), public_version_id(identity.get("versionId"))
+ incarnation = identity.get("bucketIncarnationId")
+ require(pair in wanted and pair not in seen and isinstance(incarnation, str) and incarnation
+ and type(identity.get("poolIndex")) is int and identity["poolIndex"] == 0
+ and type(identity.get("setIndex")) is int and identity["setIndex"] == 0,
+ "root diagnostics contain an unknown, duplicate or incomplete ACK identity")
+ require(incarnations.setdefault(pair[0], incarnation) == incarnation, "root diagnostics mixed bucket incarnations")
+ state = "repaired" if pair in repaired else "metadata_healthy" if wanted[pair]["deleteMarker"] else "verified_healthy"
+ require(receipt.get("disposition", {}).get("state") == state, "root diagnostic falsely attributed a repair or integrity check")
+ seen.add(pair)
+ return {"canonicalCounters": counters, "diagnosticObjects": len(objects),
+ "diagnosticsAreFullManifest": False, "originalACKs": len(wanted)}
+
+
+def terminal_records(observed, token, owner, current, wanted):
+ present = owned_records(observed, token)
+ require(set(present) == {"terminal", "report"}
+ and all((item["node"], item["disk"]) == (owner["node"], owner["disk"]) for item in present.values()),
+ "all-peer restart lost the original record owner or retained runnable responsibility")
+ terminal, report = present["terminal"]["value"], present["report"]["value"]
+ require(type(terminal.get("schema")) is int and terminal["schema"] == 1
+ and terminal.get("task_id") == token and terminal.get("status") == "Completed"
+ and terminal.get("heal_type") == {"type": "cluster"}
+ and canonical(terminal.get("options")) == canonical(owner["value"]["options"]),
+ "root completion changed task, scope, checkpointed options or original budget")
+ require(type(report.get("schema")) is int and report["schema"] == 1 and report.get("terminal") == terminal
+ and canonical(report.get("outcome")) == canonical(current.get("outcome")),
+ "root durable report differs from the original recovered task")
+ counters = (report.get("outcome") or {}).get("counters") or {}
+ require(type(report.get("next_seq")) is int and type(counters.get("processed")) is int
+ and report["next_seq"] >= counters["processed"] + 1
+ and type(report.get("min_seq")) is int and 1 <= report["min_seq"] <= report["next_seq"]
+ and isinstance(report.get("seqed_items"), list) and 0 < len(report["seqed_items"]) <= 1024,
+ "root durable report has incomplete retained sequence bounds")
+ seen, bucket_events, previous = set(), set(), report["min_seq"] - 1
+ wanted_buckets = {pair[0] for pair in wanted}
+ for sequence, item in report["seqed_items"]:
+ require(type(sequence) is int and sequence == previous + 1 and sequence < report["next_seq"],
+ "root durable report skipped a sequence or changed its retained suffix")
+ if item.get("type") == "bucket":
+ require(item.get("bucket") in wanted_buckets and item.get("object") == ""
+ and item.get("versionId") == "" and item.get("objectSize") == 0
+ and item["bucket"] not in bucket_events,
+ "root durable report contains an unknown or duplicate bucket event")
+ bucket_events.add(item["bucket"])
+ else:
+ pair = item.get("bucket"), item.get("object"), public_version_id(item.get("versionId"))
+ require(item.get("type") == "object" and pair in wanted and pair not in seen
+ and item.get("objectSize") == wanted[pair].get("payload", {}).get("size", 0),
+ "root durable report changed an ACK version/marker/size")
+ seen.add(pair)
+ previous = sequence
+ require(previous == report["next_seq"] - 1, "root durable report omitted its retained suffix")
+ return {"sameRecordDisk": True, "runnableIntentRetiredOnEveryDisk": True,
+ "terminalAndReportMatchOriginalToken": True, "retainedItems": len(seen)}
+
+
+def all_processes(fleet, label):
+ return fleet.record(label, {node: fleet.remote(node, "process") for node in fleet.nodes})
+
+
+def continuity(before, after):
+ require(set(before) == set(after) and len(before) == 4, "full restart omitted an original peer")
+ for node, old in before.items():
+ new = after[node]
+ require(all(new.get(name) == old.get(name) for name in PROCESS_FIELDS if name not in {"pid", "startTicks"})
+ and type(new.get("pid")) is int and new["pid"] > 1 and type(new.get("startTicks")) is int
+ and (new["pid"], new["startTicks"]) != (old["pid"], old["startTicks"]),
+ "all-peer restart changed candidate/boot/unit/environment/namespace or did not restart a peer")
+ return {"allFourOriginalProcessesRestarted": True, "sameBootCandidateUnitsEnvironmentAndNamespace": True}
+
+
+def graceful_all(fleet, processes, first):
+ require(first in processes and set(processes) == set(fleet.nodes), "graceful stop omitted original peer identities")
+ order = [first, *(node for node in fleet.nodes if node != first)]
+ fleet.record("registered-all-peer-stop-order", {"nodes": order, "onlyOwnedUnits": True})
+ exits = {}
+ for node in order:
+ identity = processes[node]
+ if node not in fleet.frozen_processes:
+ fleet.frozen_processes[node] = identity
+ fleet.record("full-restart-freeze", fleet.remote(node, "process-signal", signal="SIGSTOP", expected=identity))
+ exits[node] = fleet.record("full-restart-graceful-exit", {"node": node,
+ **fleet.remote(node, "process-graceful", expected=identity)})
+ require(exits[node].get("actualExitCode") == 0 and exits[node].get("signal") == "SIGTERM"
+ and exits[node].get("pidfdBound") is True, "a peer lacks a witnessed normal graceful exit")
+ del fleet.frozen_processes[node]
+ fleet.record("release-exited-unit", {"node": node, **fleet.remote(node, "stop")})
+ return exits
+
+
+def verify_public(probe, fleet, fixtures, pending, label):
+ for endpoint in probe.binding["endpoints"]:
+ peer = Probe(probe.binding, endpoint)
+ for bucket, fixture in fixtures.items():
+ actual = fleet.record(label, {"endpoint": endpoint, "bucket": bucket,
+ **peer.inventory(bucket, unversioned_current_tags=fixture["fixture"] != "DV", page_size=127)})
+ verify_seed(fixture["expected"], actual)
+ compare_inventories(fixture["baseline"], actual)
+ actual_pending = fleet.record(label + "-pending-mpu", {"endpoint": endpoint,
+ "uploads": pending_inventory(peer, pending["bucket"])})
+ require(actual_pending["uploads"] == pending["uploads"], "full restart lost an exact pending UploadId/part/ETag")
+
+
+def complete_pending_uploads(recorder, fleet, binding, pending):
+ bucket = pending["bucket"]
+ for upload in pending["uploads"]:
+ recorder.call("complete_multipart_upload", {"Bucket": bucket, "Key": upload["key"], "UploadId": upload["uploadId"],
+ "MultipartUpload": {"Parts": [{"PartNumber": item["PartNumber"], "ETag": item["ETag"]} for item in upload["parts"]]}})
+ expected_body = payload(f"{bucket}/{upload['key']}", 5 * 1024**2 + 3)
+ for endpoint in binding["endpoints"]:
+ observed = fleet.record("pending-mpu-content-after-repeat", Probe(binding, endpoint).s3("get_object", {"Bucket": bucket, "Key": upload["key"]}))
+ require(observed.get("acknowledged") is True and observed.get("resultUnknown") is False
+ and observed.get("payload") == {"size": len(expected_body), "sha256": hashlib.sha256(expected_body).hexdigest()},
+ "restarted pending MPU parts lost their actual acknowledged bytes")
+
+
+def run_topology(args, nodes):
+ directory = args.evidence / "H2"
+ directory.mkdir()
+ run_id = f"gh-{os.environ['GITHUB_RUN_ID']}-{os.environ.get('GITHUB_RUN_ATTEMPT', '1')}-h2"
+ fleet = Fleet(nodes, directory, run_id, args.tool_directory, resource_profile="full-restart", runtime_profile="root-restart")
+ candidate = json.loads(args.candidate.read_text())
+ fleet.record("inspector-build", verify_inspector(args.tool_directory, candidate))
+ bucket = "rv-heal-" + hashlib.sha256(run_id.encode()).hexdigest()[:16]
+ fleet.snapshot_buckets = [bucket, bucket + "-dx", bucket + "-versions"]
+ access, secret = secrets.token_hex(16), secrets.token_hex(24)
+ os.environ.update(HEAL_ACCESS_KEY=access, HEAL_SECRET_KEY=secret)
+ binding = {"schemaVersion": 1, "runId": run_id, "jumpHostOnly": True, "exclusiveHealService": True,
+ "leaseExpiresAt": (datetime.now(timezone.utc) + timedelta(minutes=45)).isoformat(), "bucketPrefix": bucket,
+ "endpoints": {f"node{index}": f"http://rustfs-node{index}:9000" for index in range(1, 5)},
+ "defaultEndpoint": "node1", "candidateCommit": candidate["source_sha"], "harnessCommit": args.testing_commit,
+ "candidatePackageSha256": candidate["package_sha256"], "candidateBinarySha256": args.binary_sha,
+ "rcBinarySha256": hashlib.sha256(args.rc_bin.read_bytes()).hexdigest(),
+ "maxInventoryObjects": 2048, "maxInventoryBytes": 2 * 1024**3}
+ fleet.record("binding", binding)
+ require(select(CASE_ID)[0]["variants"] == list(BRANCHES), "full restart omitted a source branch")
+ result = {"topology": "H2", "verdict": "RUNNING", "branches": {}, "cleanup": "PENDING"}
+ active, probe = [], Probe(binding)
+ try:
+ fleet.deploy(args.binary_sha, bucket, access, secret)
+ fleet.record("topology-proof", validate_topology(fleet.snapshot("deployed-topology"), "H2"))
+ fleet.record("registered-full-restart-budget", {"dxObjects": OBJECTS, "dxBytes": SIZE,
+ "taskSeconds": 300, "recoveryObservationSeconds": TASK_SECONDS, "serviceMinutes": 45,
+ "sourceFixtures": ["DB", "DV", "DM"], "allOriginalUnitsRestartedTwice": True})
+ recorder = Recorder(probe, directory / "fixture")
+ db = seed(recorder, bucket, "DB", 128, [0, 4096, SIZE, 5 * 1024**2 + 123])
+ db += seed(recorder, bucket, "DM", 1, [SIZE], existing_bucket=True)
+ dx_bucket, dv_bucket = bucket + "-dx", bucket + "-versions"
+ dx = seed(recorder, dx_bucket, "DX", OBJECTS, [SIZE])
+ dv = seed(recorder, dv_bucket, "DV", 4, [5 * 1024**2 + 123])
+ fixtures = {name: {"fixture": kind, "expected": expected} for name, kind, expected in
+ ((bucket, "DB", db), (dx_bucket, "DX", dx), (dv_bucket, "DV", dv))}
+ for name, fixture in fixtures.items():
+ fixture["baseline"] = fleet.record("s3-baseline", probe.inventory(name, unversioned_current_tags=fixture["fixture"] != "DV", page_size=127))
+ fleet.record("fixture-manifest", {"bucket": name, "expected": fixture["expected"], **verify_seed(fixture["expected"], fixture["baseline"])})
+ wanted = manifest(fixtures)
+ pending = {"bucket": bucket, "uploads": fleet.record("mpu-baseline", pending_inventory(probe, bucket))}
+ fleet.stop()
+ original = fleet.snapshot("physical-baseline", quiescent=True)
+ dx_scene = copy.deepcopy(original)
+ for value in dx_scene.values(): value["files"] = [item for item in value["files"] if item["path"].startswith(dx_bucket + "/")]
+ cohorts = map_cohorts(dx_scene, dx_bucket, dx)
+ verify_all_parts(fleet, cohorts, "full-restart-baseline-streaming")
+ targets, receipts = [cohorts[index] for index in range(0, OBJECTS, 32)], []
+ for index, target in enumerate(targets):
+ params = {"disk": target["disk"], "relativePath": target["relativePath"], "kind": "missing",
+ "faultId": bounded_fault_id("F05", "full-restart", index), "expectedSha256": target["sha256"]}
+ fleet.faults.append((target["node"], params))
+ receipts.append(fleet.record("fault", {"node": target["node"], **fleet.remote(target["node"], "apply-fault", **params)}))
+ faulted = fleet.snapshot("physical-fault", quiescent=True)
+ fleet.record("fault-proof", validate_missing_batch(original, faulted, targets, receipts))
+ fleet.start()
+ require(physical_content(faulted) == physical_content(fleet.snapshot("physical-before-trigger")),
+ "ATTRIBUTION_NOT_OBSERVED: restart fault changed before root admission")
+ admission = fleet.record("root-admission", probe.admin(PATH, [], json.dumps(OPTIONS).encode()))
+ token = admission.get("response", {}).get("clientToken")
+ require(admission.get("httpStatus") == 200 and isinstance(token, str) and str(uuid.UUID(token)) == token, "root admission failed")
+ active.append((PATH, token))
+ first = wait_partial(probe, fleet, PATH, token, targets, repair_required=True)
+ owners = owned_records(records(fleet, token, "root-intent-owner"), token)
+ require(set(owners) == {"intent"}, "NOT_OBSERVED: root completed before full-cluster interruption")
+ coordinator = owners["intent"]["node"]
+ processes = all_processes(fleet, "all-peers-before-restart")
+ fleet.frozen_processes[coordinator] = processes[coordinator]
+ fleet.record("root-freeze", fleet.remote(coordinator, "process-signal", signal="SIGSTOP", expected=processes[coordinator]))
+ _, before_partial = partial_physical(original, fleet.snapshot("root-frozen-physical"), targets)
+ before_record = root_intent(records(fleet, token, "root-intent-before-all-stop"), token, coordinator)
+ exits = graceful_all(fleet, processes, coordinator)
+ after_observed = fleet.record("root-intent-after-all-stop", {node: fleet.remote(node, "task-records", taskId=token, quiescent=True) for node in nodes})
+ after_record = root_intent(after_observed, token, coordinator)
+ budget = same_owner(before_record, after_record)
+ stopped = fleet.snapshot("all-peers-stopped-physical", quiescent=True)
+ remaining, after_partial = partial_physical(original, stopped, targets)
+ fleet.start()
+ restarted = all_processes(fleet, "all-peers-after-restart")
+ changed = continuity(processes, restarted)
+ deadline = time.monotonic() + TASK_SECONDS
+ while True:
+ current = recovery_status(probe, fleet, token)
+ require(current.get("summary") in {"running", "finished"}, "original root task failed, stopped or disappeared after full restart")
+ if current["summary"] == "finished": break
+ require(time.monotonic() < deadline, "original root task exceeded its full-restart recovery budget")
+ time.sleep(0.05)
+ repaired = {(item["bucket"], item["key"], item["versionId"]) for item in remaining}
+ proof = responsibility(current, wanted, repaired)
+ active.remove((PATH, token))
+ idle_fleet(fleet, binding)
+ journal = records(fleet, token, "root-terminal-before-repeat")
+ durable = terminal_records(journal, token, after_record, current, wanted)
+ fleet.stop()
+ rebuilt = fleet.snapshot("physical-repaired-before-get", quiescent=True)
+ validate_repaired_physical(original, rebuilt, targets[0], repair_targets=targets)
+ verify_all_parts(fleet, cohorts, "full-restart-repaired-streaming")
+ fleet.start()
+ verify_public(probe, fleet, fixtures, pending, "all-peer-s3-after-first-restart")
+ before_repeat = all_processes(fleet, "all-peers-before-repeat")
+ graceful_all(fleet, before_repeat, coordinator)
+ require(physical_content(rebuilt) == physical_content(fleet.snapshot("physical-before-repeat", quiescent=True)), "first recovery GET repaired a previously unproven file")
+ fleet.start()
+ repeated = all_processes(fleet, "all-peers-after-repeat")
+ repeat_processes = continuity(before_repeat, repeated)
+ idle_fleet(fleet, binding)
+ repeat = recovery_status(probe, fleet, token)
+ require(repeat.get("summary") == "finished" and canonical(repeat.get("outcome")) == canonical(current["outcome"]), "repeat full restart revived or changed original responsibility")
+ repeated_journal = records(fleet, token, "root-terminal-after-repeat")
+ terminal_records(repeated_journal, token, after_record, repeat, wanted)
+ require(canonical(journal) == canonical(repeated_journal), "repeat full restart changed completed journal bytes")
+ verify_public(probe, fleet, fixtures, pending, "all-peer-s3-after-repeat")
+ fleet.stop()
+ require(physical_content(rebuilt) == physical_content(fleet.snapshot("physical-after-repeat-get", quiescent=True)), "repeat full restart or verification changed a user file")
+ fleet.start()
+ complete_pending_uploads(recorder, fleet, binding, pending)
+ result["branches"] = {
+ BRANCHES[0]: {"verdict": "PASS", "originalTaskId": token, "runningWindow": first["outcome"]["counters"],
+ "beforeInterruptionPhysical": before_partial, "afterAllActualExitsPhysical": after_partial,
+ "actualPeerExits": exits, "processContinuity": changed, "budget": budget, "responsibility": proof,
+ "durableTerminal": durable, "newRootAdmissionIssued": False},
+ BRANCHES[1]: {"verdict": "PASS", "exactPendingUploadsAndPartsPreserved": True,
+ "pendingPartsCompletedAndFullContentCheckedOnAllPeers": True, "completedMPUAndHistoricalVersionsAndMarkersVerified": True},
+ BRANCHES[2]: {"verdict": "PASS", "processContinuity": repeat_processes,
+ "completedJournalBytesUnchanged": True, "originalTerminalUnchanged": True, "allUserFilesUnchanged": True}}
+ result["verdict"] = "PASS"
+ except (Exception, KeyboardInterrupt) as exc:
+ result.update(verdict="FAIL", failure={"type": type(exc).__name__, "message": str(exc)})
+ try:
+ # Fleet.cleanup resumes a frozen original process before stopping it.
+ if not fleet.frozen_processes:
+ fleet.stop(); fleet.snapshot("physical-failure", quiescent=True)
+ except Exception as evidence_error: result["failureEvidenceError"] = str(evidence_error)
+ finally:
+ for path, token in active:
+ try: fleet.record("stop-unresolved", probe.admin(path, [("clientToken", token), ("forceStop", "true")], b""))
+ except Exception as exc: fleet.record("stop-error", {"error": str(exc)})
+ result["cleanup"] = "PASS" if fleet.cleanup() else "FAIL"
+ fleet.record("topology-result", result)
+ return result
+
+
+def main():
+ def interrupted(signum, frame): raise KeyboardInterrupt(f"Heal full-restart interrupted by signal {signum}")
+ signal.signal(signal.SIGTERM, interrupted)
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--candidate", type=Path, required=True)
+ parser.add_argument("--case-id", choices=(CASE_ID,), required=True)
+ parser.add_argument("--testing-commit", required=True)
+ parser.add_argument("--binary-sha", required=True)
+ parser.add_argument("--rc-bin", type=Path, required=True)
+ parser.add_argument("--evidence", type=Path, required=True)
+ parser.add_argument("--tool-directory", type=Path, required=True)
+ args = parser.parse_args()
+ nodes = os.environ.get("RUSTFS_NODES", "vm000 vm001 vm002 vm003").split()[:4]
+ require(len(nodes) == len(set(nodes)) == 4 and all(re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", node) for node in nodes), "four current Heal peers required")
+ require(re.fullmatch(r"[1-9][0-9]*", os.environ.get("GITHUB_RUN_ID", ""))
+ and re.fullmatch(r"[0-9a-f]{64}", args.binary_sha), "immutable candidate and workflow identity required")
+ results = []
+ try: results.append(run_topology(args, nodes))
+ finally:
+ complete = len(results) == 1 and results[0]["verdict"] == "PASS" and results[0]["cleanup"] == "PASS"
+ save(args.evidence / "healthy-summary.json", {"caseId": CASE_ID, "results": results,
+ "cleanup": results[0]["cleanup"] if results else "FAIL", "complete": complete})
+ require(complete, "full-cluster restart requires every H2 source branch and clean finalization")
+
+
+if __name__ == "__main__":
+ try: main()
+ except Exception as exc:
+ print(f"Heal full-restart validation failed: {exc}", file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_heal/generation_fault.py b/scripts/validation_heal/generation_fault.py
new file mode 100644
index 00000000..4b0f93f2
--- /dev/null
+++ b/scripts/validation_heal/generation_fault.py
@@ -0,0 +1,189 @@
+"""C07: preserve two exact stopped object generations and restore only the last ACK.
+
+All operations are relative to the original run-owned root's descriptors.
+Archive copies and recovery are fixture operations, never product repair.
+"""
+
+import json
+import os
+import re
+import stat
+import uuid
+
+
+def require(value, message):
+ if not value:
+ raise ValueError(message)
+
+
+def scope(owner, request):
+ bucket, key = request['bucket'], request['key']
+ require(isinstance(bucket, str) and re.fullmatch(r'rv-heal-[0-9a-f]{16}(?:-dv)?', bucket)
+ and (bucket == owner['bucket'] or bucket == owner['bucket'] + '-dv')
+ and key == 'conflict.bin', 'generation fixture escaped its exact owned bucket/key')
+ require(owner.get('runtimeProfile') == 'authority-split', 'generation replay requires the registered C07 runtime')
+ return [bucket, key]
+
+
+def tree(fd, fault, *, allow_sparse=False):
+ """Read the complete small object tree; reject aliases or unexpected leaves."""
+ values = {}
+ names = sorted(os.listdir(fd))
+ require(len(names) <= 17 and (allow_sparse or 'xl.meta' in names and len(names) > 1),
+ 'generation lacks bounded external metadata/data directories')
+ for name in names:
+ if name == 'xl.meta':
+ values[name] = fault.read_file(fd, name)
+ continue
+ require(str(uuid.UUID(name)) == name and uuid.UUID(name).int != 0, 'generation contains an unreviewed object child')
+ with fault.parent(fd, [name]) as part_fd:
+ parts = sorted(os.listdir(part_fd))
+ require(parts and len(parts) <= 32, 'generation exceeds its part/commitment file budget')
+ payloads, commitments = set(), set()
+ for part in parts:
+ match = re.fullmatch(r'part\.([1-9][0-9]*)(?:\.integrity\.([0-9a-f-]{36}))?', part)
+ require(match is not None, 'generation contains unreviewed multipart leaves')
+ number, nonce = match.groups()
+ if nonce is None:
+ payloads.add(number)
+ else:
+ require(str(uuid.UUID(nonce)) == nonce and uuid.UUID(nonce).int != 0 and number not in commitments,
+ 'generation has ambiguous or noncanonical shard commitment identity')
+ commitments.add(number)
+ values[name + '/' + part] = fault.read_file(part_fd, part)
+ require(0 < len(payloads) <= 16 and commitments <= payloads,
+ 'generation commitment has no exact corresponding original part')
+ require(sum(map(len, values.values())) <= 32 * 1024**2, 'generation exceeds its per-drive byte budget')
+ manifest = [{'path': name, 'size': len(raw), 'sha256': fault.digest(raw)} for name, raw in sorted(values.items())]
+ return values, manifest
+
+
+def tree_sha(manifest, fault):
+ return fault.digest(json.dumps(manifest, sort_keys=True, separators=(',', ':')).encode())
+
+
+def copy_into(fd, values, fault):
+ require(not os.listdir(fd), 'exclusive generation copy target is not empty')
+ for name, raw in sorted(values.items()):
+ parts = name.split('/')
+ if len(parts) == 1:
+ fault.new_file(fd, name, raw)
+ else:
+ require(len(parts) == 2, 'generation copy depth changed')
+ if parts[0] not in os.listdir(fd):
+ os.mkdir(parts[0], 0o700, dir_fd=fd)
+ os.fsync(fd)
+ with fault.parent(fd, parts[:1]) as child:
+ fault.new_file(child, parts[1], raw)
+ os.fsync(fd)
+
+
+def archive(root_fd, label, fault):
+ return fault.parent(root_fd, ['quarantine', 'C07-' + label])
+
+
+def capture(root_fd, owner, request, fault):
+ parts = scope(owner, request)
+ label = request['generation']
+ require(label in {'a', 'b'}, 'unregistered generation archive')
+ with fault.parent(root_fd, ['data', *parts]) as original:
+ values, manifest = tree(original, fault)
+ require(manifest == request['expectedFiles'], 'capture differs from the frozen full-drive object tree')
+ record = {'schemaVersion': 1, 'runId': owner['runId'], 'bucket': parts[0], 'key': parts[1],
+ 'disk': request['disk'], 'generation': label, 'mountId': fault.mount_id(root_fd),
+ 'device': os.fstat(root_fd).st_dev, 'files': manifest, 'treeSha256': tree_sha(manifest, fault),
+ 'backupRestorationIsProductRepair': False}
+ with fault.parent(root_fd, ['quarantine']) as quarantine:
+ os.mkdir('C07-' + label, 0o700, dir_fd=quarantine)
+ os.fsync(quarantine)
+ with archive(root_fd, label, fault) as saved:
+ fault.new_file(saved, 'manifest.json', fault.encode(record))
+ os.mkdir('object', 0o700, dir_fd=saved)
+ with fault.parent(saved, ['object']) as destination:
+ copy_into(destination, values, fault)
+ require(tree(destination, fault)[1] == manifest, 'archived generation is incomplete')
+ fault.new_file(saved, 'complete.json', fault.encode(record))
+ return record
+
+
+def captured(root_fd, owner, request, label, fault):
+ parts = scope(owner, request)
+ with archive(root_fd, label, fault) as saved:
+ record = json.loads(fault.read_file(saved, 'complete.json', 65536))
+ require(record == json.loads(fault.read_file(saved, 'manifest.json', 65536))
+ and record['schemaVersion'] == 1 and record['runId'] == owner['runId']
+ and (record['bucket'], record['key'], record['disk'], record['generation'])
+ == (parts[0], parts[1], request['disk'], label)
+ and record['mountId'] == fault.mount_id(root_fd) and record['device'] == os.fstat(root_fd).st_dev,
+ 'archived generation ownership or original device drifted')
+ with fault.parent(saved, ['object']) as original:
+ values, manifest = tree(original, fault)
+ require(manifest == record['files'] and tree_sha(manifest, fault) == record['treeSha256'],
+ 'archived generation bytes differ from the captured authority')
+ return values, record
+
+
+def install(root_fd, owner, request, fault):
+ parts = scope(owner, request)
+ old_values, old = captured(root_fd, owner, request, 'a', fault)
+ _, current = captured(root_fd, owner, request, 'b', fault)
+ require(old['treeSha256'] != current['treeSha256']
+ and request['expectedSceneSha256'] == current['treeSha256'], 'generation fault has no distinct frozen last ACK')
+ with fault.parent(root_fd, ['data', *parts]) as live:
+ require(tree(live, fault)[1] == current['files'], 'last acknowledged object drifted before generation replay')
+ intent = {'schemaVersion': 1, 'runId': owner['runId'], 'disk': request['disk'], 'bucket': parts[0], 'key': parts[1],
+ 'mountId': fault.mount_id(root_fd), 'device': os.fstat(root_fd).st_dev,
+ 'originalTreeSha256': current['treeSha256'], 'injectedTreeSha256': old['treeSha256'],
+ 'restoreGeneration': 'b', 'backupRestorationIsProductRepair': False}
+ with fault.parent(root_fd, ['quarantine']) as quarantine:
+ require('C07-pre-split' not in os.listdir(quarantine), 'generation replay would replace a retained scene')
+ fault.new_file(quarantine, 'C07-replay-intent.json', fault.encode(intent))
+ with fault.parent(root_fd, ['data', parts[0]]) as bucket:
+ os.rename(parts[1], 'C07-pre-split', src_dir_fd=bucket, dst_dir_fd=quarantine)
+ os.fsync(bucket)
+ os.fsync(quarantine)
+ os.mkdir(parts[1], 0o700, dir_fd=bucket)
+ with fault.parent(bucket, [parts[1]]) as destination:
+ copy_into(destination, old_values, fault)
+ require(tree(destination, fault)[1] == old['files'], 'generation replay did not install every exact archived byte')
+ os.fsync(bucket)
+ fault.new_file(quarantine, 'C07-replay-applied.json', fault.encode(intent))
+ return {**intent, 'state': 'APPLIED', 'oldAndNewArchivesPreserved': True}
+
+
+def recover(root_fd, owner, request, fault):
+ parts = scope(owner, request)
+ values, original = captured(root_fd, owner, request, 'b', fault)
+ with fault.parent(root_fd, ['quarantine']) as quarantine:
+ intent = json.loads(fault.read_file(quarantine, 'C07-replay-intent.json', 65536))
+ require(intent['schemaVersion'] == 1 and intent['runId'] == owner['runId']
+ and (intent['bucket'], intent['key'], intent['disk']) == (parts[0], parts[1], request['disk'])
+ and intent['restoreGeneration'] == 'b' and intent['originalTreeSha256'] == original['treeSha256']
+ and intent['mountId'] == fault.mount_id(root_fd) and intent['device'] == os.fstat(root_fd).st_dev,
+ 'recovery cannot establish original run/device/last-ACK ownership')
+ with fault.parent(root_fd, ['data', parts[0]]) as bucket:
+ if 'C07-failed-current' in os.listdir(quarantine):
+ # A completed rename can precede a lost SSH receipt. Confirm
+ # exact restoration; partial recovery remains DIRTY.
+ with fault.parent(bucket, [parts[1]]) as live:
+ require(tree(live, fault)[1] == original['files'], 'partial prior recovery cannot be discarded')
+ else:
+ if parts[1] in os.listdir(bucket):
+ observed = os.stat(parts[1], dir_fd=bucket, follow_symlinks=False)
+ require(stat.S_ISDIR(observed.st_mode) and observed.st_uid == 0
+ and observed.st_dev == os.fstat(root_fd).st_dev, 'fault scene no longer owns its original directory')
+ os.rename(parts[1], 'C07-failed-current', src_dir_fd=bucket, dst_dir_fd=quarantine)
+ else:
+ os.mkdir('C07-failed-current', 0o700, dir_fd=quarantine)
+ os.fsync(quarantine)
+ os.fsync(bucket)
+ os.mkdir(parts[1], 0o700, dir_fd=bucket)
+ with fault.parent(bucket, [parts[1]]) as live:
+ copy_into(live, values, fault)
+ require(tree(live, fault)[1] == original['files'], 'recovery did not restore every last-ACK byte')
+ os.fsync(bucket)
+ receipt = {**intent, 'state': 'CLEAR', 'originalLastACKGenerationRestored': True,
+ 'failedScenePreserved': True, 'productRepairClaimed': False}
+ if 'C07-replay-recovered.json' not in os.listdir(quarantine):
+ fault.new_file(quarantine, 'C07-replay-recovered.json', fault.encode(receipt))
+ return receipt
diff --git a/scripts/validation_heal/heal_fault.py b/scripts/validation_heal/heal_fault.py
new file mode 100644
index 00000000..59dcf651
--- /dev/null
+++ b/scripts/validation_heal/heal_fault.py
@@ -0,0 +1,358 @@
+#!/usr/bin/env python3
+"""Linux-only, descriptor-relative file faults in a dedicated Heal run root.
+
+No arbitrary shell, disk formatting, shared mount changes, or internal system
+metadata. Keep the service quiescent while applying/undoing a file fault.
+"""
+
+from __future__ import annotations
+
+import argparse
+from contextlib import contextmanager
+from datetime import UTC, datetime
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import stat
+import sys
+
+
+LIMIT = 64 * 1024**2
+ROOT_PATTERN = re.compile(r"/data/rustfs[1-9][0-9]*/heal-validation/((?:gh|manual)-[a-z0-9-]{5,60})")
+FAULTS = ("missing", "bitflip", "truncate", "zero", "header", "replace-part", "future-xl-schema")
+
+
+def bounded_fault_id(code: str, branch: str, index: int) -> str:
+ require(bool(re.fullmatch(r"[A-J][0-9]{2}", code)) and isinstance(branch, str) and branch
+ and type(index) is int and index >= 0, "invalid fault identity inputs")
+ return code + "-" + hashlib.sha256(f"{branch}/{index}".encode()).hexdigest()[:32]
+
+
+def require(condition: bool, message: str) -> None:
+ if not condition:
+ raise ValueError(message)
+
+
+def digest(value: bytes) -> str:
+ return hashlib.sha256(value).hexdigest()
+
+
+def mount_id(fd: int) -> int:
+ text = Path(f"/proc/self/fdinfo/{fd}").read_text()
+ values = re.findall(r"^mnt_id:\s*([0-9]+)$", text, re.MULTILINE)
+ require(len(values) == 1, "Linux mount identity unavailable")
+ return int(values[0])
+
+
+def target_parts(relative: str, prefix: str) -> list[str]:
+ parts = relative.split("/")
+ require(3 <= len(parts) <= 48 and all(part not in {"", ".", ".."} for part in parts),
+ "invalid relative object path")
+ require(parts[0] == prefix or parts[0].startswith(prefix + "-"), "target bucket not owned by run")
+ require(not any(part.startswith(".rustfs") for part in parts), "system metadata faults are prohibited")
+ require(parts[-1] == "xl.meta" or bool(re.fullmatch(r"part\.[1-9][0-9]*", parts[-1])),
+ "only explicit object xl.meta/part files are supported")
+ return parts
+
+
+@contextmanager
+def directory(path: Path):
+ require(path.is_absolute() and ".." not in path.parts, "noncanonical root")
+ fd = os.open("/", os.O_RDONLY | os.O_DIRECTORY)
+ try:
+ for part in path.parts[1:]:
+ child = os.open(part, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=fd)
+ os.close(fd)
+ fd = child
+ yield fd
+ finally:
+ os.close(fd)
+
+
+@contextmanager
+def parent(root_fd: int, parts: list[str]):
+ fd = os.dup(root_fd)
+ expected_mount = mount_id(root_fd)
+ try:
+ for part in parts:
+ child = os.open(part, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=fd)
+ os.close(fd)
+ fd = child
+ require(mount_id(fd) == expected_mount, "nested mount or bind-mount escape")
+ yield fd
+ finally:
+ os.close(fd)
+
+
+def read_file(fd: int, name: str, limit: int = LIMIT) -> bytes:
+ opened = os.open(name, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=fd)
+ try:
+ info = os.fstat(opened)
+ require(stat.S_ISREG(info.st_mode) and info.st_nlink <= 1 and info.st_size <= limit,
+ "expected bounded regular non-hardlinked file")
+ require(mount_id(opened) == mount_id(fd), "file bind-mount escape")
+ # Atomic publication can unlink the opened predecessor before fstat.
+ # Reject those bytes as churn; only bounded live observers may retry.
+ require(info.st_nlink == 1, "file changed while reading")
+ with os.fdopen(opened, "rb", closefd=False) as stream:
+ value = stream.read(limit + 1)
+ after = os.fstat(opened)
+ require(after.st_nlink == 1 and len(value) <= limit and info.st_size == len(value)
+ and (info.st_size, info.st_mtime_ns, info.st_ctime_ns)
+ == (after.st_size, after.st_mtime_ns, after.st_ctime_ns), "file changed while reading")
+ return value
+ finally:
+ os.close(opened)
+
+
+def new_file(fd: int, name: str, value: bytes) -> None:
+ opened = os.open(name, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600, dir_fd=fd)
+ with os.fdopen(opened, "wb") as stream:
+ stream.write(value)
+ stream.flush()
+ os.fsync(stream.fileno())
+ os.fsync(fd)
+
+
+def encode(value: dict) -> bytes:
+ return (json.dumps(value, ensure_ascii=False, indent=2) + "\n").encode()
+
+
+def mutation(value: bytes, kind: str, offset: int | None = None) -> bytes | None:
+ require(kind in FAULTS, "unknown fault")
+ require(kind != "replace-part", "replacement requires a verified owned donor")
+ if kind == "missing":
+ return None
+ if kind == "future-xl-schema":
+ require(offset is None and len(value) >= 8 and value[:4] == b"XL2 "
+ and int.from_bytes(value[4:6], "little") == 1, "future envelope requires exact reviewed XL2 major 1")
+ return value[:4] + (2).to_bytes(2, "little") + value[6:]
+ require(bool(value), "empty file cannot witness this fault")
+ if kind == "truncate":
+ length = len(value) // 2 if offset is None else offset
+ require(type(length) is int and 0 <= length < len(value), "truncate length outside file")
+ return value[:length]
+ if kind == "zero":
+ changed = bytes(len(value))
+ elif kind == "header":
+ changed = bytes(byte ^ 0xFF for byte in value[:32]) + value[32:]
+ else:
+ position = len(value) // 2 if offset is None else offset
+ require(type(position) is int and 0 <= position < len(value), "bitflip offset outside file")
+ changed = value[:position] + bytes([value[position] ^ 1]) + value[position + 1:]
+ require(changed != value, "fault would not change the target")
+ return changed
+
+
+def checked_owner(root_fd: int, run_id: str, recovery: bool = False) -> dict:
+ owner = json.loads(read_file(root_fd, "fault-owner.json", 16384))
+ require(owner.get("runId") == run_id and owner.get("schemaVersion") == 1, "owner mismatch")
+ require(owner.get("mountId") == mount_id(root_fd) and owner.get("device") == os.fstat(root_fd).st_dev,
+ "mount identity changed; rebind only after an operator audit")
+ expiry = datetime.fromisoformat(owner["leaseExpiresAt"].replace("Z", "+00:00"))
+ require(expiry.tzinfo is not None, "lease must have timezone")
+ require(recovery or expiry > datetime.now(UTC), "fault lease expired")
+ return owner
+
+
+def apply_fault(root_fd: int, owner: dict, relative: str, fault_id: str, kind: str,
+ expected_sha: str, offset: int | None = None, *, donor_relative: str | None = None,
+ donor_sha: str | None = None) -> dict:
+ parts = target_parts(relative, owner["bucketPrefix"])
+ require(kind != "future-xl-schema" or parts[-1] == "xl.meta", "future schema fault requires exact object metadata")
+ require(bool(re.fullmatch(r"[A-J][0-9]{2}-[a-z0-9-]{1,48}", fault_id)), "invalid fault ID")
+ require(bool(re.fullmatch(r"[0-9a-f]{64}", expected_sha)), "baseline SHA-256 required")
+ with parent(root_fd, ["data", *parts[:-1]]) as target_fd, parent(root_fd, ["quarantine"]) as backup_fd:
+ before = read_file(target_fd, parts[-1])
+ require(digest(before) == expected_sha, "target differs from frozen baseline")
+ donor = None
+ if kind == "replace-part":
+ require(offset is None and isinstance(donor_relative, str) and isinstance(donor_sha, str)
+ and re.fullmatch(r"[0-9a-f]{64}", donor_sha), "verified replacement donor required")
+ source = target_parts(donor_relative, owner["bucketPrefix"])
+ require(source[-1] == parts[-1] and source[-1].startswith("part.") and source != parts,
+ "replacement must use another owned generation's same-number part")
+ with parent(root_fd, ["data", *source[:-1]]) as source_fd:
+ changed = read_file(source_fd, source[-1])
+ require(digest(changed) == donor_sha and changed != before, "donor changed or is identical to target")
+ donor = {"relativePath": donor_relative, "sha256": donor_sha, "size": len(changed)}
+ else:
+ require(donor_relative is None and donor_sha is None, "donor parameters require replace-part")
+ changed = mutation(before, kind, offset)
+ receipt = {"schemaVersion": 1, "runId": owner["runId"], "faultId": fault_id,
+ "relativePath": relative, "kind": kind, "offset": offset,
+ "before": {"sha256": digest(before), "size": len(before)},
+ "fault": None if changed is None else {"sha256": digest(changed), "size": len(changed)},
+ "createdAt": datetime.now(UTC).isoformat(), "backup": fault_id + ".original",
+ "backend": "logical-file-fault", "requiresQuiescentService": True}
+ if donor is not None:
+ receipt["donor"] = donor
+ # Intent and recoverable bytes precede the mutation. An interrupted
+ # apply remains recoverable even when no completion receipt was emitted.
+ new_file(backup_fd, fault_id + ".intent.json", encode(receipt))
+ new_file(backup_fd, receipt["backup"], before)
+ require(read_file(target_fd, parts[-1]) == before, "target changed before injection")
+ if changed is None:
+ os.unlink(parts[-1], dir_fd=target_fd)
+ else:
+ temporary = ".heal-fault-" + fault_id
+ new_file(target_fd, temporary, changed)
+ os.rename(temporary, parts[-1], src_dir_fd=target_fd, dst_dir_fd=target_fd)
+ os.fsync(target_fd)
+ if changed is None:
+ require(not os.path.lexists(f"/proc/self/fd/{target_fd}/{parts[-1]}"), "missing fault not observed")
+ else:
+ require(read_file(target_fd, parts[-1]) == changed, "changed bytes not observed")
+ receipt["witnessedAt"] = datetime.now(UTC).isoformat()
+ new_file(backup_fd, fault_id + ".applied.json", encode(receipt))
+ return receipt
+
+
+def recover_fault(root_fd: int, owner: dict, fault_id: str, *, expected_receipt_sha256: str | None = None) -> dict:
+ require(bool(re.fullmatch(r"[A-J][0-9]{2}-[a-z0-9-]{1,48}", fault_id)), "invalid fault ID")
+ with parent(root_fd, ["quarantine"]) as backup_fd:
+ receipt = json.loads(read_file(backup_fd, fault_id + ".intent.json", 16384))
+ if expected_receipt_sha256 is not None:
+ require(isinstance(expected_receipt_sha256, str) and re.fullmatch(r"[0-9a-f]{64}", expected_receipt_sha256)
+ and digest(json.dumps(receipt, sort_keys=True, separators=(",", ":")).encode()) == expected_receipt_sha256,
+ "recovery intent differs from archived fault receipt")
+ require(receipt["runId"] == owner["runId"] and receipt["faultId"] == fault_id
+ and receipt["backup"] == fault_id + ".original", "recovery receipt mismatch")
+ parts = target_parts(receipt["relativePath"], owner["bucketPrefix"])
+ before = read_file(backup_fd, receipt["backup"])
+ require(digest(before) == receipt["before"]["sha256"] and len(before) == receipt["before"]["size"],
+ "backup corrupted; preserve scene")
+ with parent(root_fd, ["data", *parts[:-1]]) as target_fd:
+ try:
+ current = read_file(target_fd, parts[-1])
+ except FileNotFoundError:
+ current = None
+ restored = current != before
+ if restored:
+ # A new generation or a legitimate metadata rewrite is not
+ # ours to overwrite. Preserve it for an operator-led rescue.
+ expected_fault = receipt["fault"]
+ require(current is None or expected_fault is not None
+ and len(current) == expected_fault["size"]
+ and digest(current) == expected_fault["sha256"],
+ "target changed after fault; refusing to overwrite possible acknowledged data")
+ # Never erase post-failure bytes. Exact recovery is a rescue,
+ # not proof that the product healed the target.
+ if current is not None:
+ preserved = fault_id + ".failed-" + digest(current)
+ try:
+ new_file(backup_fd, preserved, current)
+ except FileExistsError:
+ require(read_file(backup_fd, preserved) == current, "preserved bytes differ")
+ temporary = ".heal-recovery-" + fault_id
+ try:
+ new_file(target_fd, temporary, before)
+ except FileExistsError:
+ require(read_file(target_fd, temporary) == before, "incomplete recovery temporary")
+ os.rename(temporary, parts[-1], src_dir_fd=target_fd, dst_dir_fd=target_fd)
+ os.fsync(target_fd)
+ require(read_file(target_fd, parts[-1]) == before, "recovery verification failed")
+ return {"runId": owner["runId"], "faultId": fault_id, "restoredFromBackup": restored,
+ "physicalRepairPassAllowed": False, "retainedBackup": receipt["backup"],
+ "recoveredAt": datetime.now(UTC).isoformat()}
+
+
+def snapshot(root_fd: int, owner: dict, bucket: str) -> dict:
+ require(bucket == owner["bucketPrefix"] or bucket.startswith(owner["bucketPrefix"] + "-"),
+ "snapshot bucket outside run")
+ require(bool(re.fullmatch(r"[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]", bucket)), "invalid bucket")
+ files, total = [], 0
+
+ def walk(fd: int, relative: list[str], depth: int = 0) -> None:
+ nonlocal total
+ require(depth <= 48, "snapshot depth exceeds budget")
+ for name in sorted(os.listdir(fd)):
+ info = os.stat(name, dir_fd=fd, follow_symlinks=False)
+ if stat.S_ISDIR(info.st_mode):
+ with parent(fd, [name]) as child:
+ walk(child, [*relative, name], depth + 1)
+ else:
+ value = read_file(fd, name)
+ total += len(value)
+ require(total <= 8 * 1024**3 and len(files) < 10000, "snapshot budget exceeded")
+ files.append({"path": "/".join([*relative, name]), "size": len(value), "sha256": digest(value)})
+
+ with parent(root_fd, ["data", bucket]) as bucket_fd:
+ walk(bucket_fd, [bucket])
+ return {"runId": owner["runId"], "bucket": bucket, "files": files, "bytes": total,
+ "capturedAt": datetime.now(UTC).isoformat(), "decodedMetadata": False,
+ "physicalRepairPassAllowed": False}
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, required=True)
+ parser.add_argument("--run-id", required=True)
+ sub = parser.add_subparsers(dest="action", required=True)
+ init = sub.add_parser("init")
+ init.add_argument("--bucket-prefix", required=True)
+ init.add_argument("--lease-expires-at", required=True)
+ inspect = sub.add_parser("inspect")
+ inspect.add_argument("--path", required=True)
+ capture = sub.add_parser("snapshot")
+ capture.add_argument("--bucket", required=True)
+ apply = sub.add_parser("apply")
+ apply.add_argument("--path", required=True)
+ apply.add_argument("--fault-id", required=True)
+ apply.add_argument("--kind", choices=FAULTS, required=True)
+ apply.add_argument("--expected-sha256", required=True)
+ apply.add_argument("--offset", type=int)
+ apply.add_argument("--donor-path")
+ apply.add_argument("--donor-sha256")
+ apply.add_argument("--service-quiescent", action="store_true", required=True)
+ recover = sub.add_parser("recover")
+ recover.add_argument("--fault-id", required=True)
+ recover.add_argument("--service-quiescent", action="store_true", required=True)
+ args = parser.parse_args()
+ require(sys.platform == "linux" and os.geteuid() == 0, "run only with sudo on the Linux jump-host target")
+ match = ROOT_PATTERN.fullmatch(str(args.root))
+ require(match is not None and match.group(1) == args.run_id, "root is not a dedicated Heal run root")
+ with directory(args.root) as root_fd:
+ require(os.fstat(root_fd).st_uid == 0 and os.fstat(root_fd).st_mode & 0o022 == 0,
+ "run root must be root-owned and not group/world writable")
+ if args.action == "init":
+ require(bool(re.fullmatch(r"rv-heal-[a-z0-9-]{6,35}", args.bucket_prefix)), "invalid bucket prefix")
+ expiry = datetime.fromisoformat(args.lease_expires_at.replace("Z", "+00:00"))
+ require(expiry.tzinfo is not None and expiry > datetime.now(UTC), "invalid lease")
+ # Existing run ownership must already be provisioned by the lab
+ # operator (or pilot setup). This tool never provisions a data disk.
+ existing = json.loads(read_file(root_fd, "owner.json", 16384))
+ require(existing.get("runId") == args.run_id, "missing lab ownership marker")
+ with parent(root_fd, ["data"]), parent(root_fd, ["quarantine"]):
+ pass
+ result = {"schemaVersion": 1, "runId": args.run_id, "bucketPrefix": args.bucket_prefix,
+ "leaseExpiresAt": args.lease_expires_at, "mountId": mount_id(root_fd),
+ "device": os.fstat(root_fd).st_dev}
+ new_file(root_fd, "fault-owner.json", encode(result))
+ else:
+ owner = checked_owner(root_fd, args.run_id, recovery=args.action == "recover")
+ if args.action == "snapshot":
+ result = snapshot(root_fd, owner, args.bucket)
+ elif args.action == "inspect":
+ parts = target_parts(args.path, owner["bucketPrefix"])
+ with parent(root_fd, ["data", *parts[:-1]]) as target_fd:
+ value = read_file(target_fd, parts[-1])
+ result = {"runId": args.run_id, "path": args.path, "size": len(value), "sha256": digest(value)}
+ elif args.action == "apply":
+ result = apply_fault(root_fd, owner, args.path, args.fault_id, args.kind,
+ args.expected_sha256, args.offset,
+ donor_relative=args.donor_path, donor_sha=args.donor_sha256)
+ else:
+ result = recover_fault(root_fd, owner, args.fault_id)
+ print(json.dumps(result, ensure_ascii=False))
+ return 0
+
+
+if __name__ == "__main__":
+ try:
+ raise SystemExit(main())
+ except (ValueError, OSError) as error:
+ print(json.dumps({"state": "INFRA_ERROR", "error": str(error)}), file=sys.stderr)
+ raise SystemExit(1) from error
diff --git a/scripts/validation_heal/heal_probe.py b/scripts/validation_heal/heal_probe.py
new file mode 100644
index 00000000..25276061
--- /dev/null
+++ b/scripts/validation_heal/heal_probe.py
@@ -0,0 +1,526 @@
+#!/usr/bin/env python3
+"""Bounded S3/Admin evidence collector for operator-assisted Heal fleet runs.
+
+This is an operation tool, not an 84-case PASS generator. Run on the jump-host
+runner, using a reviewed, expiring lab binding and credentials from environment.
+"""
+
+from __future__ import annotations
+
+import argparse
+from contextlib import contextmanager
+from datetime import UTC, datetime, timedelta
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import time
+import uuid
+from urllib import request
+from urllib.error import HTTPError
+from urllib.parse import unquote, urlencode, urlsplit
+
+
+MAX_BODY = 64 * 1024**2
+# The reviewed candidate's channel::encode_heal_task_status_payload allows
+# 8 MiB (channel.rs SHA-256 7f669ca43b9559db9b97fba6b419a6c76a94d76acd8526db132d74b7c534d6b0).
+# Retained H2 diagnostics can exceed 2 MiB without exceeding that wire contract.
+ADMIN_RESPONSE_LIMIT = 8 * 1024**2
+# Only compare monotonic observations from this collector process. Other
+# processes/hosts have a different domain even if wall clocks look aligned.
+CLOCK_DOMAIN = str(uuid.uuid4())
+
+
+@contextmanager
+def observation_phase(observation, name):
+ """Measure an optional collector phase without adding evidence writes."""
+ if observation is None:
+ yield
+ return
+ phase = {'startedMonotonicNs': time.monotonic_ns(), 'completed': False}
+ observation['phases'][name] = phase
+ try:
+ yield
+ phase['completed'] = True
+ finally:
+ phase['finishedMonotonicNs'] = time.monotonic_ns()
+
+
+S3_OPERATIONS = frozenset("""
+create_bucket head_bucket delete_bucket put_object get_object head_object delete_object
+delete_objects list_objects_v2 list_object_versions copy_object
+put_bucket_versioning get_bucket_versioning put_bucket_tagging get_bucket_tagging
+put_bucket_policy get_bucket_policy delete_bucket_policy
+put_object_tagging get_object_tagging delete_object_tagging
+create_multipart_upload upload_part list_parts complete_multipart_upload
+abort_multipart_upload list_multipart_uploads
+put_bucket_encryption get_bucket_encryption delete_bucket_encryption
+put_object_retention get_object_retention put_object_legal_hold get_object_legal_hold
+put_object_lock_configuration get_object_lock_configuration
+put_bucket_lifecycle_configuration get_bucket_lifecycle_configuration
+delete_bucket_lifecycle put_bucket_replication get_bucket_replication
+delete_bucket_replication restore_object get_bucket_location
+""".split())
+SECRET_FIELDS = {"authorization", "ssecustomerkey", "ssecustomerkeymd5",
+ "copysourcessecustomerkey", "copysourcessecustomerkeymd5",
+ "aws_access_key_id", "aws_secret_access_key", "aws_session_token"}
+
+
+def require(condition: bool, message: str) -> None:
+ if not condition:
+ raise ValueError(message)
+
+
+def canonical(value: object) -> str:
+ return json.dumps(value, ensure_ascii=False, sort_keys=True, default=str)
+
+
+def redact(value):
+ if isinstance(value, dict):
+ return {key: "[REDACTED]" if key.lower() in SECRET_FIELDS else redact(item)
+ for key, item in value.items()}
+ if isinstance(value, list):
+ return [redact(item) for item in value]
+ if isinstance(value, str):
+ for name in ("HEAL_ACCESS_KEY", "HEAL_SECRET_KEY", "HEAL_SESSION_TOKEN", "HEAL_SSE_C_KEY", "HEAL_SSE_C_WRONG_KEY"):
+ secret = os.environ.get(name)
+ if secret:
+ value = value.replace(secret, "[REDACTED]")
+ return value
+
+
+def save(path: Path, value: dict) -> None:
+ # Never overwrite earlier observations, including failed operations.
+ path.parent.mkdir(parents=True, exist_ok=True)
+ with path.open("x", encoding="utf-8") as output:
+ output.write(json.dumps(redact(value), ensure_ascii=False, indent=2, default=str) + "\n")
+ output.flush()
+ os.fsync(output.fileno())
+ directory = os.open(path.parent, os.O_RDONLY)
+ try:
+ os.fsync(directory)
+ finally:
+ os.close(directory)
+
+
+def validate_binding(binding: dict, now: datetime | None = None) -> None:
+ require(binding.get("schemaVersion") == 1, "unsupported lab binding")
+ require(bool(re.fullmatch(r"(?:gh|manual)-[a-z0-9-]{5,60}", binding.get("runId", ""))),
+ "invalid run ID")
+ require(binding.get("jumpHostOnly") is True and binding.get("exclusiveHealService") is True,
+ "an exclusive Heal service and jump-host route must be assigned")
+ require(binding.get("assignmentRequired", False) is False, "example/unassigned lab cannot run")
+ expiry = datetime.fromisoformat(binding["leaseExpiresAt"].replace("Z", "+00:00"))
+ require(expiry.tzinfo is not None and expiry > (now or datetime.now(UTC)), "fleet lease expired")
+ require(bool(re.fullmatch(r"rv-heal-[a-z0-9-]{6,35}", binding.get("bucketPrefix", ""))),
+ "dedicated bucket prefix required")
+ endpoints = binding.get("endpoints", {})
+ require(bool(endpoints) and binding.get("defaultEndpoint") in endpoints, "missing endpoint binding")
+ for name, endpoint in endpoints.items():
+ require(bool(re.fullmatch(r"[a-zA-Z0-9_-]+", name)), "invalid endpoint name")
+ parsed = urlsplit(endpoint)
+ require(parsed.scheme in {"http", "https"} and parsed.hostname is not None
+ and parsed.username is None and parsed.password is None and not parsed.query
+ and not parsed.fragment and parsed.path in {"", "/"}, "invalid endpoint URL")
+ require(parsed.port is not None and 1024 <= parsed.port <= 65535,
+ "an explicitly allocated test port is required")
+ for name in ("candidateCommit", "harnessCommit"):
+ require(bool(re.fullmatch(r"[0-9a-f]{40}", binding.get(name, ""))), f"missing pinned {name}")
+ for name in ("candidatePackageSha256", "candidateBinarySha256", "rcBinarySha256"):
+ require(bool(re.fullmatch(r"[0-9a-f]{64}", binding.get(name, ""))), f"missing pinned {name}")
+ require(type(binding.get("maxInventoryObjects")) is int
+ and binding["maxInventoryObjects"] in range(1, 10001), "invalid inventory object budget")
+ require(type(binding.get("maxInventoryBytes")) is int
+ and 1 <= binding["maxInventoryBytes"] <= 64 * 1024**3, "invalid inventory byte budget")
+
+
+def bucket_allowed(binding: dict, bucket: str) -> None:
+ prefix = binding["bucketPrefix"]
+ require(isinstance(bucket, str) and len(bucket) <= 63
+ and (bucket == prefix or bucket.startswith(prefix + "-"))
+ and bool(re.fullmatch(r"[a-z0-9][a-z0-9.-]+[a-z0-9]", bucket)), "bucket is outside this run")
+
+
+def validate_s3_request(binding: dict, operation: str, params: dict, *, allow_governance_bypass: bool = False) -> None:
+ require(operation in S3_OPERATIONS, "S3 operation is not allowlisted")
+ bucket_allowed(binding, params.get("Bucket", ""))
+ source = params.get("CopySource")
+ if source is not None:
+ require(isinstance(source, dict), "CopySource must use the structured SDK form")
+ bucket_allowed(binding, source.get("Bucket", ""))
+ if operation == "delete_objects":
+ items = params.get("Delete", {}).get("Objects", [])
+ require(0 < len(items) <= 1000 and all(isinstance(item.get("Key"), str) for item in items),
+ "invalid bounded batch deletion")
+ if params.get("BypassGovernanceRetention"):
+ require(allow_governance_bypass is True and params["BypassGovernanceRetention"] is True
+ and operation == "delete_object" and isinstance(params.get("Key"), str) and params["Key"]
+ and isinstance(params.get("VersionId"), str) and params["VersionId"] not in {"", "null"},
+ "retention bypass requires an explicit owned-version DELETE proof or finalizer")
+ require("Body" not in params, "use --size/--seed or --body-file; do not put payloads in JSON")
+
+
+def payload(seed: str, size: int) -> bytes:
+ require(type(size) is int and 0 <= size <= MAX_BODY, "payload exceeds 64 MiB budget")
+ return hashlib.shake_256(seed.encode()).digest(size)
+
+
+class NoRedirect(request.HTTPRedirectHandler):
+ def redirect_request(self, req, fp, code, msg, headers, newurl):
+ raise ValueError("Admin redirects are prohibited")
+
+
+class Probe:
+ def __init__(self, binding: dict, endpoint: str | None = None, *, credential_pair=None):
+ validate_binding(binding)
+ self.binding = binding
+ self.endpoint_name = endpoint or binding["defaultEndpoint"]
+ require(self.endpoint_name in binding["endpoints"], "endpoint is not bound")
+ self.endpoint = binding["endpoints"][self.endpoint_name].rstrip("/")
+ require(credential_pair is None or isinstance(credential_pair, tuple) and len(credential_pair) == 2
+ and all(isinstance(value, str) and value for value in credential_pair), "invalid in-memory principal credentials")
+ self.credential_pair = credential_pair
+
+ def credentials(self):
+ return self.credential_pair or (os.environ["HEAL_ACCESS_KEY"], os.environ["HEAL_SECRET_KEY"])
+
+ def s3_client(self):
+ import boto3
+ from botocore.config import Config
+ access, secret = self.credentials()
+ return boto3.client("s3", endpoint_url=self.endpoint,
+ aws_access_key_id=access, aws_secret_access_key=secret,
+ aws_session_token=None if self.credential_pair else os.environ.get("HEAL_SESSION_TOKEN"),
+ region_name="us-east-1",
+ config=Config(signature_version="s3v4", s3={"addressing_style": "path"},
+ retries={"max_attempts": 0}, connect_timeout=5,
+ read_timeout=30, proxies={}))
+
+ def s3(self, operation: str, params: dict, body: bytes | None = None, *, allow_governance_bypass: bool = False) -> dict:
+ validate_binding(self.binding) # Recheck expiry before every mutation.
+ validate_s3_request(self.binding, operation, params, allow_governance_bypass=allow_governance_bypass)
+ outgoing = dict(params)
+ for key in ("SSECustomerKey", "CopySourceSSECustomerKey"):
+ if outgoing.get(key) in {"$HEAL_SSE_C_KEY", "$HEAL_SSE_C_WRONG_KEY"}:
+ outgoing[key] = os.environ[outgoing[key][1:]]
+ if body is not None:
+ require(operation in {"put_object", "upload_part"}, "body not allowed for this operation")
+ outgoing["Body"] = body
+ started = time.monotonic()
+ evidence = {"operation": operation, "params": redact(params), "endpoint": self.endpoint_name,
+ "startedAt": datetime.now(UTC).isoformat(), "clockDomain": CLOCK_DOMAIN,
+ "callStartedMonotonicNs": time.monotonic_ns(),
+ "clientCallInvoked": False,
+ "timingScope": "client-call; not a server commit/lock witness"}
+ if body is not None:
+ evidence["payload"] = {"size": len(body), "sha256": hashlib.sha256(body).hexdigest()}
+ client = None
+ try:
+ client = self.s3_client()
+ evidence['callStartedMonotonicNs'] = time.monotonic_ns()
+ evidence['clientCallInvoked'] = True
+ response = getattr(client, operation)(**outgoing)
+ stream = response.pop("Body", None)
+ if stream is not None:
+ digest, count = hashlib.sha256(), 0
+ try:
+ while chunk := stream.read(1024**2):
+ count += len(chunk)
+ require(count <= MAX_BODY, "GET exceeds per-object evidence budget")
+ digest.update(chunk)
+ finally:
+ stream.close()
+ evidence["payload"] = {"size": count, "sha256": digest.hexdigest()}
+ require(response.get("ContentLength") == count, "GET length differs from response ContentLength")
+ evidence.update(response=response, acknowledged=True, resultUnknown=False)
+ except Exception as exc:
+ response = getattr(exc, "response", None)
+ evidence.update(response=response, acknowledged=False,
+ resultUnknown=response is None,
+ error={"type": type(exc).__name__, "message": str(exc)})
+ finally:
+ evidence["callFinishedMonotonicNs"] = time.monotonic_ns()
+ if client is not None:
+ try:client.close()
+ except Exception as exc:
+ evidence['clientCleanupError']={'type':type(exc).__name__,'message':str(exc)}
+ evidence["elapsedSeconds"] = time.monotonic() - started
+ evidence["finishedAt"] = datetime.now(UTC).isoformat()
+ return evidence
+
+ def admin(self, path: str, query: list[tuple[str, str]], body: bytes,
+ method: str = "POST", auth: str = "signed", *, observation=None) -> dict:
+ from botocore.awsrequest import AWSRequest
+ try:
+ from .heal_signing import sign_admin_request
+ except ImportError:
+ from heal_signing import sign_admin_request
+ with observation_phase(observation, 'requestPreparation'):
+ validate_binding(self.binding)
+ quota_path = path.startswith("/rustfs/admin/v3/quota/")
+ quota_config_path = path == "/rustfs/admin/v3/get-bucket-quota"
+ pool_path = path in {"/rustfs/admin/v3/pools/list", "/rustfs/admin/v3/pools/status"}
+ require(path.startswith("/rustfs/admin/v3/heal/") or quota_path or quota_config_path or pool_path or path in {
+ "/rustfs/admin/v3/background-heal/status", "/rustfs/admin/v4/heal/replacement-recovery",
+ "/rustfs/admin/v3/info"}, "only bounded Heal evidence routes are allowed")
+ if path == "/rustfs/admin/v3/info":
+ require(method == "GET" and not query and body == b"" and auth == "signed",
+ "server-info evidence requires an exact signed read-only GET")
+ if pool_path:
+ require(self.binding['runId'].endswith('-h5') and len(self.binding['endpoints']) == 4
+ and method == 'GET' and body == b'' and auth == 'signed',
+ 'pool observations require the dedicated four-node H5 read-only binding')
+ allowed_query = query == [] if path.endswith('/list') else query in (
+ [('pool', '0'), ('by-id', 'true')], [('pool', '1'), ('by-id', 'true')])
+ require(allowed_query, 'pool observation selector escaped the two exact registered H5 pools')
+ if quota_path:
+ bucket = path.removeprefix("/rustfs/admin/v3/quota/")
+ bucket_allowed(self.binding, bucket)
+ require(method in {"GET", "PUT"} and not query and auth == "signed",
+ "owned quota evidence requires an exact signed GET/PUT")
+ if method == "GET":
+ require(body == b"", "quota GET cannot carry a mutation body")
+ else:
+ quota = json.loads(body)
+ require(isinstance(quota, dict) and set(quota) == {"quota", "quota_type"}
+ and quota["quota_type"] == "HARD" and type(quota["quota"]) is int
+ and quota["quota"] in {1024**2, 256 * 1024**2},
+ "quota mutation is outside the two registered I02 limits")
+ if quota_config_path:
+ require(method == "GET" and len(query) == 1 and len(query[0]) == 2
+ and query[0][0] == "bucket" and body == b"" and auth == "signed",
+ "owned quota config requires an exact signed read-only GET")
+ bucket_allowed(self.binding, query[0][1])
+ require("?" not in path and "#" not in path and len(body) <= 2 * 1024**2,
+ "invalid Admin path or body budget")
+ require(not any(part in {".", ".."} for part in unquote(path).split("/")),
+ "Admin route traversal is prohibited")
+ # Root traversal is safe only because the binding attests a dedicated
+ # service. Bucket paths are still independently bounded to this run.
+ suffix = path.removeprefix("/rustfs/admin/v3/heal/")
+ if path.startswith("/rustfs/admin/v3/heal/") and suffix:
+ bucket_allowed(self.binding, unquote(suffix.split("/", 1)[0]))
+ url = self.endpoint + path + ("?" + urlencode(query) if query else "")
+ headers = {"Content-Type": "application/json", "x-amz-content-sha256": hashlib.sha256(body).hexdigest()}
+ with observation_phase(observation, 'signing'):
+ require(auth in {"signed", "anonymous", "bad-signature", "expired-signature"}, "unsupported Admin auth mode")
+ signing_time = None
+ if auth != "anonymous":
+ access, secret = self.credentials()
+ if auth == "bad-signature":
+ secret = "deliberately-invalid-heal-signature"
+ if auth == "expired-signature":
+ signing_time = (datetime.now(UTC) - timedelta(days=1)).strftime("%Y%m%dT%H%M%SZ")
+ outgoing = AWSRequest(method=method, url=url, data=body, headers=headers)
+ sign_admin_request(outgoing, access, secret,
+ None if self.credential_pair else os.environ.get("HEAL_SESSION_TOKEN"), timestamp=signing_time)
+ headers = dict(outgoing.headers.items())
+ started = time.monotonic()
+ evidence = {"path": path, "query": query, "requestBody": body.decode(errors="replace"),
+ "method": method, "auth": auth, "endpoint": self.endpoint_name,
+ "startedAt": datetime.now(UTC).isoformat(), "clockDomain": CLOCK_DOMAIN,
+ "callStartedMonotonicNs": time.monotonic_ns(),
+ "timingScope": "client-call; not a server commit/lock witness"}
+ if signing_time:
+ evidence["isolatedClientSigningTimestamp"] = signing_time
+ if observation is not None:
+ evidence['statusObservation'] = dict(sessionId=observation['sessionId'], sequence=observation['sequence'])
+ observation['callStartedMonotonicNs'] = evidence['callStartedMonotonicNs']
+ try:
+ response = self._open_admin(request.Request(url, data=body if method != "GET" else None,
+ method=method, headers=headers))
+ except HTTPError as exc:
+ response = exc
+ with observation_phase(observation, 'responseBody'):
+ with response:
+ raw = response.read(ADMIN_RESPONSE_LIMIT + 1)
+ require(len(raw) <= ADMIN_RESPONSE_LIMIT, "Admin response exceeds evidence budget")
+ length = response.headers.get('Content-Length')
+ require(length is None or (isinstance(length, str) and length.isdigit() and int(length) == len(raw)),
+ 'Admin response Content-Length differs from complete body')
+ evidence["httpStatus"] = response.code
+ evidence["responseHeaders"] = dict(response.headers.items())
+ evidence["responseBytes"] = len(raw)
+ evidence["responseBudgetBytes"] = ADMIN_RESPONSE_LIMIT
+ with observation_phase(observation, 'responseParsing'):
+ try:
+ evidence["response"] = json.loads(raw)
+ except (ValueError, UnicodeDecodeError):
+ evidence["response"] = raw.decode(errors="replace")
+ evidence["elapsedSeconds"] = time.monotonic() - started
+ evidence["callFinishedMonotonicNs"] = time.monotonic_ns()
+ evidence["finishedAt"] = datetime.now(UTC).isoformat()
+ if observation is not None:
+ observation.update(callFinishedMonotonicNs=evidence['callFinishedMonotonicNs'], responseObserved=True)
+ return evidence
+
+ def _open_admin(self, outgoing):
+ opener = request.build_opener(request.ProxyHandler({}), NoRedirect())
+ return opener.open(outgoing, timeout=30)
+
+ def inventory(self, bucket: str, *, unversioned_current_tags: bool = False, page_size: int | None = None,
+ object_read_options: dict | None = None) -> dict:
+ """Enumerate every version/marker, then hash complete version-addressed GETs.
+
+ Only call before injection or after attribution/physical proof: a GET can
+ itself trigger read repair. Never use this to watch scanner progress.
+ """
+ bucket_allowed(self.binding, bucket)
+ read_options = {} if object_read_options is None else object_read_options
+ require(isinstance(read_options, dict) and set(read_options) <= {"SSECustomerAlgorithm", "SSECustomerKey"}
+ and (not read_options or read_options == {"SSECustomerAlgorithm": "AES256", "SSECustomerKey": "$HEAL_SSE_C_KEY"}),
+ "inventory decryption options must use the reviewed in-memory customer-key reference")
+ require(page_size is None or type(page_size) is int and 1 <= page_size <= 1000,
+ "invalid explicit inventory page size")
+ client, entries, count, byte_count = self.s3_client(), [], 0, 0
+ pagination = {"requestedPageSize": page_size, "versionPageSizes": [], "currentPageSizes": []}
+ options = {"PaginationConfig": {"PageSize": page_size}} if page_size else {}
+ if unversioned_current_tags:
+ mode = self.s3("get_bucket_versioning", {"Bucket": bucket})
+ require(mode["acknowledged"] and mode["response"].get("Status") is None,
+ "current-tag addressing requires a never-versioned fixture bucket")
+ for page in client.get_paginator("list_object_versions").paginate(Bucket=bucket, **options):
+ page_count = len(page.get("Versions", [])) + len(page.get("DeleteMarkers", []))
+ require(page_size is None or page_count <= page_size, "version LIST exceeded the requested page size")
+ pagination["versionPageSizes"].append(page_count)
+ require(not unversioned_current_tags or not page.get("DeleteMarkers"),
+ "unversioned fixture unexpectedly has delete markers")
+ for marker in page.get("DeleteMarkers", []):
+ entries.append({"key": marker["Key"], "versionId": marker["VersionId"],
+ "isLatest": marker["IsLatest"], "deleteMarker": True})
+ count += 1
+ for version in page.get("Versions", []):
+ count += 1
+ require(count <= self.binding["maxInventoryObjects"], "inventory object budget exceeded")
+ params = {"Bucket": bucket, "Key": version["Key"], "VersionId": version["VersionId"]}
+ result = self.s3("get_object", {**params, **read_options})
+ require(result["acknowledged"], "version GET failed: " + canonical(result))
+ tag_params = dict(params)
+ if unversioned_current_tags:
+ require(version["VersionId"] == "null" and version["IsLatest"] is True,
+ "current-tag addressing cannot read historical versions")
+ tag_params.pop("VersionId")
+ tags = self.s3("get_object_tagging", tag_params)
+ require(tags["acknowledged"], "version tag GET failed: " + canonical(tags))
+ response = result["response"]
+ byte_count += result["payload"]["size"]
+ require(byte_count <= self.binding["maxInventoryBytes"], "inventory byte budget exceeded")
+ entries.append({"key": version["Key"], "versionId": version["VersionId"],
+ "isLatest": version["IsLatest"], "deleteMarker": False,
+ "payload": result["payload"],
+ "properties": {key: response[key] for key in (
+ "ETag", "ContentLength", "ContentType", "ContentEncoding",
+ "ContentDisposition", "CacheControl", "Metadata", "StorageClass",
+ "ServerSideEncryption", "SSEKMSKeyId", "SSECustomerAlgorithm", "SSECustomerKeyMD5",
+ "ObjectLockMode", "ObjectLockRetainUntilDate",
+ "ObjectLockLegalHoldStatus") if key in response},
+ "tags": sorted(tags["response"].get("TagSet", []), key=lambda item: item["Key"])})
+ require(count <= self.binding["maxInventoryObjects"], "inventory object budget exceeded")
+ identities = [(entry["key"], entry["versionId"]) for entry in entries]
+ require(len(set(identities)) == len(identities), "duplicate version/marker in pagination")
+ # Cross-check current LIST against the separately paginated version view.
+ current = []
+ for page in client.get_paginator("list_objects_v2").paginate(Bucket=bucket, **options):
+ require(page_size is None or len(page.get("Contents", [])) <= page_size,
+ "current LIST exceeded the requested page size")
+ pagination["currentPageSizes"].append(len(page.get("Contents", [])))
+ current.extend((item["Key"], item["Size"], item["ETag"]) for item in page.get("Contents", []))
+ require(len(current) <= self.binding["maxInventoryObjects"], "current LIST exceeds budget")
+ expected = [(item["key"], item["payload"]["size"], item["properties"]["ETag"])
+ for item in entries if item["isLatest"] and not item["deleteMarker"]]
+ require(sorted(current) == sorted(expected), "current LIST differs from version manifest")
+ if unversioned_current_tags:
+ mode = self.s3("get_bucket_versioning", {"Bucket": bucket})
+ require(mode["acknowledged"] and mode["response"].get("Status") is None,
+ "fixture versioning changed during inventory")
+ return {"bucket": bucket, "entries": sorted(entries, key=lambda item: (item["key"], item["versionId"])),
+ "objectsAndMarkers": count, "bytesRead": byte_count,
+ "tagAddressing": "current-never-versioned-fixture" if unversioned_current_tags else "explicit-version",
+ "pagination": pagination}
+
+
+def compare_inventories(before: dict, after: dict) -> dict:
+ require(before["bucket"] == after["bucket"] and bool(before["entries"]), "wrong or empty baseline")
+ require(canonical(before["entries"]) == canonical(after["entries"]), "S3 versions/content/metadata/tags changed")
+ return {"equal": True, "objectsAndMarkers": len(before["entries"]),
+ "proof": "full-version-content-and-public-metadata", "physicalRepairProved": False}
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--binding", type=Path, required=True)
+ parser.add_argument("--endpoint")
+ parser.add_argument("--out", type=Path, required=True)
+ subs = parser.add_subparsers(dest="command", required=True)
+ admin = subs.add_parser("admin")
+ admin.add_argument("--path", required=True)
+ admin.add_argument("--method", choices=("POST", "GET"), default="POST")
+ admin.add_argument("--query", action="append", default=[])
+ admin.add_argument("--body", default="")
+ admin.add_argument("--body-file", type=Path)
+ admin.add_argument("--auth", choices=("signed", "anonymous", "bad-signature", "expired-signature"), default="signed")
+ admin.add_argument("--expect-http", type=int, default=200)
+ s3 = subs.add_parser("s3")
+ s3.add_argument("--operation", choices=sorted(S3_OPERATIONS), required=True)
+ s3.add_argument("--params", default="{}")
+ s3.add_argument("--params-file", type=Path)
+ s3.add_argument("--size", type=int)
+ s3.add_argument("--seed", default="heal")
+ s3.add_argument("--body-file", type=Path)
+ s3.add_argument("--expect-http", type=int, default=200)
+ inventory = subs.add_parser("inventory")
+ inventory.add_argument("--bucket", required=True)
+ compare = subs.add_parser("compare")
+ compare.add_argument("--before", type=Path, required=True)
+ compare.add_argument("--after", type=Path, required=True)
+ subs.add_parser("binding-check")
+ args = parser.parse_args()
+ require(not args.out.exists(), "output already exists; preserve earlier evidence")
+ binding = json.loads(args.binding.read_text())
+ evidence = {"schemaVersion": 1, "runId": binding.get("runId"),
+ "bindingSha256": hashlib.sha256(args.binding.read_bytes()).hexdigest(),
+ "capturedAt": datetime.now(UTC).isoformat(), "command": args.command,
+ "casePassClaimed": False}
+ exit_code = 0
+ try:
+ probe = Probe(binding, args.endpoint)
+ if args.command == "admin":
+ query = []
+ for item in args.query:
+ require("=" in item, "query must be name=value; repeats are preserved")
+ query.append(tuple(item.split("=", 1)))
+ body = args.body_file.read_bytes() if args.body_file else args.body.encode()
+ evidence["observation"] = probe.admin(args.path, query, body, args.method, args.auth)
+ require(evidence["observation"]["httpStatus"] == args.expect_http, "unexpected Admin HTTP status")
+ elif args.command == "s3":
+ params = json.loads(args.params_file.read_text() if args.params_file else args.params)
+ require(not (args.body_file and args.size is not None), "choose file or generated body")
+ if args.body_file:
+ require(args.body_file.stat().st_size <= MAX_BODY, "body file exceeds budget")
+ body = args.body_file.read_bytes()
+ else:
+ body = payload(args.seed, args.size) if args.size is not None else None
+ result = probe.s3(args.operation, params, body)
+ evidence["observation"] = result
+ actual = (result.get("response") or {}).get("ResponseMetadata", {}).get("HTTPStatusCode")
+ require(actual == args.expect_http, "unexpected S3 HTTP status (unknown writes require ACK reconciliation)")
+ elif args.command == "inventory":
+ evidence["inventory"] = probe.inventory(args.bucket)
+ elif args.command == "compare":
+ before, after = (json.loads(path.read_text())["inventory"] for path in (args.before, args.after))
+ evidence["comparison"] = compare_inventories(before, after)
+ else:
+ evidence["bindingCheck"] = {"valid": True, "liveEnvironmentVerified": False}
+ evidence["operationCheck"] = "PASS"
+ except Exception as exc:
+ evidence.update(operationCheck="FAIL", error={"type": type(exc).__name__, "message": str(exc)})
+ exit_code = 1
+ save(args.out, evidence)
+ print(json.dumps({"evidence": str(args.out), "operationCheck": evidence["operationCheck"],
+ "casePassClaimed": False}))
+ return exit_code
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/scripts/validation_heal/heal_signing.py b/scripts/validation_heal/heal_signing.py
new file mode 100644
index 00000000..92bfee31
--- /dev/null
+++ b/scripts/validation_heal/heal_signing.py
@@ -0,0 +1,33 @@
+"""MinIO-compatible S3 signing for raw Heal Admin requests."""
+
+from urllib.parse import quote_from_bytes, unquote_to_bytes
+
+
+def sign_admin_request(request, access_key: str, secret_key: str, session_token: str | None = None,
+ *, timestamp: str | None = None) -> None:
+ # Keep SDK imports lazy so offline plan/binding tools need only stdlib.
+ from botocore.auth import S3SigV4Auth
+ from botocore.credentials import Credentials
+
+ class AdminS3Auth(S3SigV4Auth):
+ def add_auth(self, request):
+ if timestamp is None:
+ return super().add_auth(request)
+ # Recompute the complete signature for the isolated client's old
+ # timestamp. Editing an already signed date would test bad signing.
+ from datetime import datetime
+ datetime.strptime(timestamp, "%Y%m%dT%H%M%SZ")
+ request.context["timestamp"] = timestamp
+ self._modify_request_before_signing(request)
+ canonical = self.canonical_request(request)
+ string_to_sign = self.string_to_sign(request, canonical)
+ self._inject_signature_to_request(request, self.signature(string_to_sign, request))
+
+ def _normalize_url_path(self, path):
+ # MinIO signs the decoded URL.Path after S3 URI encoding. Do not
+ # normalize dot segments or decode twice: %252F is a literal %2F
+ # key, while %2F and / identify the same separator. The wire URL
+ # stays untouched so the server's routing/decoding is still tested.
+ return quote_from_bytes(unquote_to_bytes(path), safe="/~")
+
+ AdminS3Auth(Credentials(access_key, secret_key, session_token), "s3", "us-east-1").add_auth(request)
diff --git a/scripts/validation_heal/heal_workload.py b/scripts/validation_heal/heal_workload.py
new file mode 100644
index 00000000..8d4fe9b9
--- /dev/null
+++ b/scripts/validation_heal/heal_workload.py
@@ -0,0 +1,313 @@
+#!/usr/bin/env python3
+"""Create deterministic Heal fixtures or run a bounded, ACK-logged S3 workload."""
+
+from __future__ import annotations
+
+import argparse
+import copy
+from concurrent.futures import ThreadPoolExecutor
+from datetime import UTC, datetime
+import json
+import hashlib
+from pathlib import Path
+import threading
+import time
+
+try:
+ from .heal_probe import CLOCK_DOMAIN, MAX_BODY, Probe, bucket_allowed, payload, require, save, validate_s3_request
+ from .heal_workload_journal import AdmissionExpired, MutationOrder, batch_receipts, validate_batch_request
+except ImportError:
+ from heal_probe import CLOCK_DOMAIN, MAX_BODY, Probe, bucket_allowed, payload, require, save, validate_s3_request
+ from heal_workload_journal import AdmissionExpired, MutationOrder, batch_receipts, validate_batch_request
+
+
+class FixtureStateMismatch(ValueError):
+ """Observed S3 state disagrees with valid, successful fixture ACKs."""
+
+
+class RecordedCallError(ValueError):
+ """Failed check with an already durable, exactly bound observation.
+
+ Callers can retain known per-item batch effects without treating the whole
+ call as success or guessing its result from a later LIST. Never a retry hint.
+ """
+ def __init__(self,message,operation,params,observation,request_sha256):
+ super().__init__(message)
+ self.operation=operation;self.params=copy.deepcopy(params)
+ self.observation=copy.deepcopy(observation);self.request_sha256=request_sha256
+
+
+class Recorder:
+ def __init__(self, probe: Probe, output: Path, *, writer=None):
+ self.probe, self.output = probe, output
+ self.writer = writer
+ output.mkdir(parents=True, exist_ok=False)
+ self.sequence, self.failures, self.bytes_written = 0, 0, 0
+ self.lock = threading.Lock()
+ self.mutations = MutationOrder()
+
+ def call(self, operation: str, params: dict, body: bytes | None = None,
+ identity: dict | None = None, expect_http: int | tuple[int, ...] | None = None, *, admission_deadline=None) -> dict:
+ params=copy.deepcopy(params);identity=copy.deepcopy(identity)
+ require(expect_http is None or type(expect_http) is int and 100 <= expect_http <= 599
+ or isinstance(expect_http, tuple) and 1 <= len(expect_http) <= 3
+ and all(type(value) is int and 100 <= value <= 599 for value in expect_http)
+ and len(set(expect_http)) == len(expect_http), 'invalid finite expected HTTP status set')
+ validate_s3_request(self.probe.binding, operation, params)
+ if operation == 'delete_objects':validate_batch_request(params)
+ require(body is None or isinstance(body,bytes) and len(body)<=MAX_BODY and operation in {'put_object','upload_part'},
+ 'invalid foreground payload/type/budget')
+ with self.mutations.hold(operation, params,admission_deadline):
+ try:
+ return self._call(operation, params, body, identity, expect_http)
+ except BaseException:
+ # Already in-flight calls drain and retain their own evidence;
+ # no later foreground mutation starts after an uncertain write.
+ self.mutations.poison('request/receipt failed')
+ raise
+
+ def _call(self, operation, params, body, identity, expect_http):
+ with self.lock:
+ self.sequence += 1
+ sequence = self.sequence
+ self.bytes_written += len(body) if body is not None else 0
+ require(self.bytes_written <= self.probe.binding["maxInventoryBytes"], "batch payload byte budget exceeded")
+ request = {"schemaVersion": 2, "runId": self.probe.binding["runId"], "sequence": sequence,
+ "operation": operation, "params": params, "identity": identity, "expectHttp": expect_http,
+ "clockDomain": CLOCK_DOMAIN, "preparedMonotonicNs": time.monotonic_ns(),
+ "preparedAt": datetime.now(UTC).isoformat(), "casePassClaimed": False}
+ if body is not None:request['payload']={'size':len(body),'sha256':hashlib.sha256(body).hexdigest()}
+ request_path = self.output / f"{sequence:06d}-request.json"
+ # fsync file+directory before allowing any network invocation. A
+ # request without a durable result remains UNKNOWN, never disappears.
+ (self.writer or save)(request_path, request)
+ request_sha = hashlib.sha256(request_path.read_bytes()).hexdigest()
+ invoked = time.monotonic_ns()
+ try:
+ result = self.probe.s3(operation, params, body)
+ except Exception as exc:
+ result = {"acknowledged": False, "resultUnknown": True, "response": getattr(exc,'response',None),
+ "error": {"type": type(exc).__name__, "message": str(exc)}}
+ finished = time.monotonic_ns()
+ status = (result.get("response") or {}).get("ResponseMetadata", {}).get("HTTPStatusCode")
+ if expect_http is None:
+ checked = result["acknowledged"] and type(status) is int and 200<=status<300
+ elif isinstance(expect_http, tuple):
+ checked = status in expect_http
+ else:
+ checked = status == expect_http
+ checked = checked and not result.get('clientCleanupError')
+ per_item = None
+ if operation == 'delete_objects' and status == 200:
+ per_item = batch_receipts(params, result['response'])
+ checked = checked and not per_item['errors']
+ (self.writer or save)(self.output / f"{sequence:06d}-ack.json", {"schemaVersion": 2,
+ "runId": self.probe.binding["runId"], "sequence": sequence, "identity": identity,
+ "requestFile":request_path.name,"requestSha256":request_sha,"batchDelete":per_item,
+ "clockDomain":CLOCK_DOMAIN,"recordedMonotonicNs":time.monotonic_ns(),
+ "collectorCallStartedMonotonicNs":invoked,"collectorCallFinishedMonotonicNs":finished,
+ "operationCheck": "PASS" if checked else "FAIL", "observation": result,
+ "casePassClaimed": False})
+ with self.lock:
+ self.failures += not checked
+ if not checked:
+ raise RecordedCallError(f"unexpected {operation} result; inspect ACK {sequence:06d}, do not silently retry",
+ operation,params,result,request_sha)
+ return result.get("response") or {}
+
+
+def seed(recorder: Recorder, bucket: str, fixture: str, count: int, sizes: list[int],
+ *, key_prefix: str = "objects/", existing_bucket: bool = False) -> list[dict]:
+ bucket_allowed(recorder.probe.binding, bucket)
+ require(1 <= count <= 2048 and bool(sizes) and all(0 <= size <= 64 * 1024**2 for size in sizes),
+ "invalid bounded fixture dimensions")
+ require(key_prefix in {"objects/", "objects-"} and (key_prefix == "objects/" or fixture == "DB"),
+ "flat keys are only supported for the bounded DB fixture")
+ if not existing_bucket:
+ recorder.call("create_bucket", {"Bucket": bucket})
+ else:
+ mode = recorder.call("get_bucket_versioning", {"Bucket": bucket})
+ require(fixture == "DM" and mode.get("Status") is None,
+ "appended multipart fixtures require the run's never-versioned bucket")
+ expected = []
+ if fixture == "DV":
+ recorder.call("put_bucket_versioning", {"Bucket": bucket, "VersioningConfiguration": {"Status": "Enabled"}})
+ if fixture == "DM":
+ for index in range(count):
+ key = f"multipart/{index:04d}.bin"
+ upload = recorder.call("create_multipart_upload", {"Bucket": bucket, "Key": key,
+ "Metadata": {"heal-fixture": "DM"}, "ContentType": "application/octet-stream"})
+ parts = []
+ digest, full_size = hashlib.sha256(), 0
+ for number, size in enumerate((5 * 1024**2 + 17, 5 * 1024**2 + 93, 1024**2 + 47), 1):
+ body = payload(f"{bucket}/{key}/{number}", size)
+ digest.update(body)
+ full_size += len(body)
+ result = recorder.call("upload_part", {"Bucket": bucket, "Key": key,
+ "UploadId": upload["UploadId"], "PartNumber": number},
+ body)
+ parts.append({"PartNumber": number, "ETag": result["ETag"]})
+ completed = recorder.call("complete_multipart_upload", {"Bucket": bucket, "Key": key,
+ "UploadId": upload["UploadId"], "MultipartUpload": {"Parts": parts}})
+ expected.append({"key": key, "versionId": completed.get("VersionId", "null"),
+ "deleteMarker": False, "isLatest": True,
+ "payload": {"sha256": digest.hexdigest(), "size": full_size}})
+ # Separate uploads for Complete/Abort races. Their actual IDs live in
+ # the ACK ledger, never inferred from a later upload's ID.
+ for name in ("pending-a", "pending-b"):
+ response = recorder.call("create_multipart_upload", {"Bucket": bucket, "Key": name})
+ recorder.call("upload_part", {"Bucket": bucket, "Key": name, "UploadId": response["UploadId"],
+ "PartNumber": 1}, payload(f"{bucket}/{name}", 5 * 1024**2 + 3))
+ return expected
+ for index in range(count):
+ key = f"{key_prefix}{index:04d}.bin"
+ if fixture == "DS":
+ prefixes = ("中文/文件", "space key", "literal%2Fslash", "actual/slash", "a/child", "ab/control", "b/other")
+ key = f"{prefixes[index % len(prefixes)]}/{index:04d}.bin"
+ versions = 3 if fixture == "DV" else 1
+ for version in range(versions):
+ size = sizes[index % len(sizes)]
+ body = payload(f"{bucket}/{key}/{version}", size)
+ response = recorder.call("put_object", {"Bucket": bucket, "Key": key,
+ "ContentType": "application/octet-stream",
+ "Metadata": {"heal-fixture": fixture, "nonce": f"{index}-{version}"},
+ "Tagging": "suite=heal&fixture=" + fixture},
+ body)
+ require(fixture != "DV" or response.get("VersionId") not in {None, "null"},
+ "enabled versioning PUT omitted a real version ID")
+ expected.append({"key": key, "versionId": response.get("VersionId", "null"),
+ "isLatest": version == versions - 1, "deleteMarker": False,
+ "payload": {"sha256": hashlib.sha256(body).hexdigest(), "size": len(body)}})
+ if fixture == "DV" and index % 2 == 0:
+ response = recorder.call("delete_object", {"Bucket": bucket, "Key": key})
+ require(response.get("DeleteMarker") is True and response.get("VersionId"), "marker ACK is incomplete")
+ expected[-1]["isLatest"] = False
+ expected.append({"key": key, "versionId": response["VersionId"], "isLatest": True, "deleteMarker": True})
+ return expected
+
+
+def verify_seed(expected: list[dict], inventory: dict) -> dict:
+ actual = {(entry["key"], entry["versionId"]): entry for entry in inventory["entries"]}
+ wanted = {(entry["key"], entry["versionId"]): entry for entry in expected}
+ require(bool(wanted) and len(wanted) == len(expected), "fixture ACK identities are empty or duplicate")
+ if len(actual) != len(inventory["entries"]) or set(wanted) != set(actual):
+ raise FixtureStateMismatch("fixture version/marker set differs from ACKs")
+ for identity, entry in wanted.items():
+ # payloadNonce binds a deterministic Range oracle to the original ACK.
+ # It is collector state, not an S3 response property; payload length,
+ # digest and every public identity/assertion remain exact comparisons.
+ if "payloadNonce" in entry:
+ require(type(entry["payloadNonce"]) is int and entry["payloadNonce"] >= 0,
+ "invalid acknowledged payload nonce")
+ if not all(actual[identity].get(key) == value for key, value in entry.items() if key != "payloadNonce"):
+ raise FixtureStateMismatch("baseline content/version/marker differs from deterministic fixture ACK")
+ return {"expectedEntries": len(expected), "seedAckAndFullGetAgree": True, "casePassClaimed": False}
+
+
+def substitute(value, round_number: int, worker: int):
+ if isinstance(value, dict):
+ return {key: substitute(item, round_number, worker) for key, item in value.items()}
+ if isinstance(value, list):
+ return [substitute(item, round_number, worker) for item in value]
+ if isinstance(value, str):
+ return value.replace("${round}", str(round_number)).replace("${worker}", str(worker))
+ return value
+
+
+def run_trace(recorder: Recorder, trace: list[dict], rounds: int, workers: int,
+ interval: float, duration: int) -> dict:
+ require(1 <= rounds <= 100000 and 1 <= workers <= 16 and 0 <= interval <= 60
+ and 1 <= duration <= 72 * 3600 and 1 <= len(trace) <= 100,
+ "invalid bounded workload dimensions")
+ require(rounds * workers * len(trace) <= 1_000_000, "request count exceeds batch cap")
+ # Reject the whole trace before its first mutation. A later cross-run
+ # bucket or invalid payload must not leave an earlier partial workload.
+ for step in trace:
+ require(isinstance(step, dict) and set(step) <= {"operation", "params", "size", "expectHttp"},
+ "invalid trace fields")
+ validate_s3_request(recorder.probe.binding, step["operation"], substitute(step["params"], 0, 0))
+ if step['operation']=='delete_objects':validate_batch_request(step['params'])
+ if "size" in step:
+ require(type(step["size"]) is int and 0 <= step["size"] <= 64 * 1024**2
+ and step["operation"] in {"put_object", "upload_part"}, "invalid trace payload")
+ if "expectHttp" in step:
+ require(type(step["expectHttp"]) is int and 100 <= step["expectHttp"] <= 599, "invalid expected HTTP code")
+ deadline, abort = time.monotonic() + duration, threading.Event()
+
+ def worker_run(worker: int) -> dict:
+ completed = 0
+ for round_number in range(rounds):
+ for index, step in enumerate(trace):
+ if abort.is_set() or time.monotonic() >= deadline:
+ return {'worker':worker,'completedOperations':completed,'stopReason':'abort' if abort.is_set() else 'deadline'}
+ identity = {"worker": worker, "round": round_number, "step": index}
+ params = substitute(step["params"], round_number, worker)
+ body = payload(f"{recorder.probe.binding['runId']}/{worker}/{round_number}/{index}", step["size"]) if "size" in step else None
+ try:
+ recorder.call(step["operation"], params, body, identity, step.get("expectHttp"),admission_deadline=deadline)
+ except AdmissionExpired:
+ return {'worker':worker,'completedOperations':completed,'stopReason':'admission-deadline'}
+ except Exception:
+ abort.set()
+ raise
+ completed += 1
+ if interval:
+ abort.wait(min(interval, max(0, deadline - time.monotonic())))
+ return {'worker':worker,'completedOperations':completed,'stopReason':'round-limit'}
+
+ with ThreadPoolExecutor(max_workers=workers) as pool:
+ futures = [pool.submit(worker_run, worker) for worker in range(workers)]
+ outcomes = [future.result() for future in futures]
+ return {'workers':outcomes,'plannedOperationUpperBound':rounds*workers*len(trace),
+ 'completedOperations':sum(v['completedOperations'] for v in outcomes),
+ 'allRoundsCompleted':all(v['stopReason']=='round-limit' for v in outcomes),'casePassClaimed':False}
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--binding", type=Path, required=True)
+ parser.add_argument("--out", type=Path, required=True)
+ parser.add_argument("--endpoint")
+ sub = parser.add_subparsers(dest="command", required=True)
+ fixture = sub.add_parser("seed")
+ fixture.add_argument("--fixture", choices=("DB", "DM", "DV", "DS", "DX"), required=True)
+ fixture.add_argument("--bucket", required=True)
+ fixture.add_argument("--count", type=int)
+ fixture.add_argument("--sizes", help="comma-separated actual byte sizes, including candidate-specific boundaries")
+ workload = sub.add_parser("run")
+ workload.add_argument("--trace", type=Path, required=True)
+ workload.add_argument("--rounds", type=int, required=True)
+ workload.add_argument("--workers", type=int, default=1)
+ workload.add_argument("--interval", type=float, default=0.1, help="minimum seconds between each worker's requests")
+ workload.add_argument("--duration", type=int, required=True, help="hard admission deadline in seconds")
+ args = parser.parse_args()
+ binding = json.loads(args.binding.read_text())
+ recorder = Recorder(Probe(binding, args.endpoint), args.out)
+ summary = {"runId": binding["runId"], "startedAt": datetime.now(UTC).isoformat(),
+ "command": args.command, "casePassClaimed": False, "state": "RUNNING"}
+ start = time.monotonic()
+ try:
+ if args.command == "seed":
+ defaults = {"DB": 128, "DM": 1, "DV": 4, "DS": 257, "DX": 257}
+ sizes = [int(value) for value in args.sizes.split(",")] if args.sizes else (
+ [0, 1, 4095, 4096, 4097, 1024**2 - 1, 1024**2, 1024**2 + 1, 5 * 1024**2 + 123]
+ if args.fixture == "DB" else [5 * 1024**2 + 123])
+ expected = seed(recorder, args.bucket, args.fixture, args.count if args.count is not None else defaults[args.fixture], sizes)
+ save(args.out / "expected-fixture.json", {"runId": binding["runId"], "bucket": args.bucket, "entries": expected})
+ baseline = recorder.probe.inventory(args.bucket)
+ save(args.out / "baseline.json", {"runId": binding["runId"], "inventory": baseline})
+ summary["baselineVerification"] = verify_seed(expected, baseline)
+ else:
+ summary['workload'] = run_trace(recorder, json.loads(args.trace.read_text()), args.rounds, args.workers, args.interval, args.duration)
+ summary["state"] = "OPERATIONS_COMPLETE"
+ except Exception as exc:
+ summary.update(state="FAIL", error={"type": type(exc).__name__, "message": str(exc)})
+ summary.update(requests=recorder.sequence, failedOperations=recorder.failures,
+ elapsedSeconds=time.monotonic() - start, finishedAt=datetime.now(UTC).isoformat())
+ save(args.out / "summary.json", summary)
+ print(json.dumps(summary))
+ return 0 if summary["state"] == "OPERATIONS_COMPLETE" else 1
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/scripts/validation_heal/heal_workload_journal.py b/scripts/validation_heal/heal_workload_journal.py
new file mode 100644
index 00000000..2877ff9e
--- /dev/null
+++ b/scripts/validation_heal/heal_workload_journal.py
@@ -0,0 +1,84 @@
+#!/usr/bin/env python3
+"""Foreground mutation ordering and per-item DELETE ACKs; no repair verdict."""
+from contextlib import contextmanager
+import threading
+import time
+
+try:
+ from .heal_probe import require
+except ImportError:
+ from heal_probe import require
+
+
+class AdmissionExpired(TimeoutError):
+ """No call was sent: the admission deadline expired while waiting."""
+
+
+def mutation_resources(operation,params):
+ if operation.startswith(('get_','head_','list_')):return ()
+ bucket=params['Bucket']
+ if operation=='delete_objects':
+ keys=[v['Key'] for v in params['Delete']['Objects']]
+ return tuple(sorted({(bucket,k) for k in keys}))
+ if 'Key' in params:
+ require(isinstance(params['Key'],str) and params['Key'],'empty/invalid mutation key')
+ return ((bucket,params['Key']),)
+ # Bucket configuration changes exclude all foreground mutations in that
+ # bucket, while different keys and buckets can otherwise run concurrently.
+ return ((bucket,None),)
+
+
+def conflicts(left,right):
+ return any(a==b and (x is None or y is None or x==y) for a,x in left for b,y in right)
+
+
+class MutationOrder:
+ def __init__(self):
+ self.condition=threading.Condition();self.active={};self.poisoned=None;self.next_ticket=0
+
+ @contextmanager
+ def hold(self,operation,params,deadline=None):
+ resources=mutation_resources(operation,params);ticket=None
+ if resources:
+ with self.condition:
+ while any(conflicts(resources,r) for r in self.active.values()) and self.poisoned is None:
+ remaining=None if deadline is None else deadline-time.monotonic()
+ if remaining is not None and remaining<=0:raise AdmissionExpired('foreground admission deadline expired')
+ self.condition.wait(timeout=remaining)
+ require(self.poisoned is None,'foreground mutations halted after prior failure/unknown result')
+ if deadline is not None and time.monotonic()>=deadline:raise AdmissionExpired('foreground admission deadline expired')
+ ticket=self.next_ticket;self.next_ticket+=1;self.active[ticket]=resources
+ elif deadline is not None and time.monotonic()>=deadline:raise AdmissionExpired('foreground admission deadline expired')
+ try:yield resources
+ finally:
+ if ticket is not None:
+ with self.condition:
+ del self.active[ticket];self.condition.notify_all()
+
+ def poison(self,reason):
+ with self.condition:self.poisoned=self.poisoned or reason;self.condition.notify_all()
+
+
+def validate_batch_request(params):
+ delete=params['Delete'];items=delete['Objects']
+ require(delete.get('Quiet',False) is False,'DELETE batch requires explicit per-item receipts; Quiet is unsupported')
+ identities=[(v['Key'],v.get('VersionId')) for v in items]
+ require(all(isinstance(k,str) and k and (v is None or isinstance(v,str) and v) for k,v in identities),'invalid DELETE batch identity')
+ require(len(identities)==len(set(identities)),'duplicate DELETE batch identity')
+
+
+def batch_receipts(params,response):
+ """HTTP 200 is not an acknowledgement of every member of a DELETE batch."""
+ validate_batch_request(params);items=params['Delete']['Objects'];out=[];seen=set();errors=[]
+ wanted={(v['Key'],v.get('VersionId')) for v in items};by_identity={}
+ for name,state in (('Deleted','ACK'),('Errors','REJECTED')):
+ for row in response.get(name,[]):
+ key=(row.get('Key'),row.get('VersionId'))
+ if key not in wanted or key in seen:errors.append('unbound/duplicate DELETE result: '+repr(key))
+ seen.add(key);by_identity.setdefault(key,[]).append({'state':state,'receipt':row})
+ for item in items:
+ matches=by_identity.get((item['Key'],item.get('VersionId')),[])
+ value=matches[0] if len(matches)==1 else {'state':'UNKNOWN','receipts':matches}
+ out.append({'request':item,**value})
+ if value['state']!='ACK':errors.append('DELETE member not acknowledged: '+repr(item))
+ return {'items':out,'errors':errors}
diff --git a/scripts/validation_heal/healthy.py b/scripts/validation_heal/healthy.py
new file mode 100644
index 00000000..9a3a1ed4
--- /dev/null
+++ b/scripts/validation_heal/healthy.py
@@ -0,0 +1,631 @@
+#!/usr/bin/env python3
+"""Execute A01 healthy and A07 invalid-request branches on the current H1/H2 fleet."""
+
+from __future__ import annotations
+
+import argparse
+from concurrent.futures import ThreadPoolExecutor
+from datetime import datetime, timedelta, timezone
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import secrets
+import signal
+import subprocess
+import sys
+import time
+from urllib import request
+from urllib.parse import quote
+import xml.etree.ElementTree as ET
+
+try:
+ from .heal_probe import Probe, canonical, compare_inventories, require, save
+ from .heal_workload import Recorder, seed, verify_seed
+ from .inspector import verify as verify_inspector
+except ImportError:
+ from heal_probe import Probe, canonical, compare_inventories, require, save
+ from heal_workload import Recorder, seed, verify_seed
+ from inspector import verify as verify_inspector
+
+
+NODE_SOURCE = Path(__file__).with_name("fleet_node.py")
+ROOT = Path(__file__).resolve().parents[2]
+BASE_OPTIONS = {"recursive": False, "remove": False, "recreate": False, "scanMode": 2,
+ "dryRun": False, "updateParity": False, "nolock": False, "readRepair": False, "pool": 0, "set": 0}
+
+
+class ReplacementAdmissionBlocked(ValueError):
+ """Replacement cases cannot start until endpoint mounts pass admission."""
+
+ def __init__(self, evidence):
+ self.evidence = evidence
+ super().__init__("replacement mount admission preflight failed; deployment and fixture seed were skipped")
+
+
+def validate_rejection(observed, expected_code):
+ require(observed.get("httpStatus") == 400, "invalid Heal request was not rejected with HTTP 400")
+ body = observed.get("response")
+ if isinstance(body, str):
+ code = next((element.text for element in ET.fromstring(body).iter()
+ if element.tag.rsplit("}", 1)[-1] == "Code"), None)
+ elif isinstance(body, dict):
+ code = body.get("Code") or body.get("code")
+ require(not body.get("clientToken"), "invalid request admitted a Heal task")
+ else:
+ code = None
+ require(code == expected_code, f"invalid Heal request expected {expected_code}; observed {code}")
+
+
+def invalid_requests(probe, fleet, bucket, before, baseline):
+ path = "/rustfs/admin/v3/heal/" + quote(bucket, safe="") + "/objects/0000.bin"
+ variants = [
+ ("duplicate-query", BASE_OPTIONS, [("clientToken", "a"), ("clientToken", "b")], "InvalidArgument", None),
+ ("invalid-bool", BASE_OPTIONS, [("forceStop", "not-a-bool")], "InvalidArgument", None),
+ ("invalid-scan", {**BASE_OPTIONS, "scanMode": 99}, [], "InvalidRequest", None),
+ ("conflicting-flags", BASE_OPTIONS, [("forceStart", "true"), ("forceStop", "true")], "InvalidRequest", None),
+ ("invalid-selector", {**BASE_OPTIONS, "pool": -1}, [], "InvalidRequest", None),
+ ("oversize-body", BASE_OPTIONS, [], "EntityTooLarge", b" " * (1024**2 + 1)),
+ ]
+ branches = {}
+ for name, options, query, code, raw in variants:
+ observed = fleet.record("trigger-" + name, probe.admin(path, query, raw if raw is not None else json.dumps(options).encode()))
+ validate_rejection(observed, code)
+ for endpoint in probe.binding["endpoints"]:
+ state = fleet.record("control-" + name, Probe(probe.binding, endpoint).admin(
+ "/rustfs/admin/v3/background-heal/status", [], b""))
+ require(state.get("httpStatus") == 200 and isinstance(state.get("response"), dict),
+ "invalid-request task isolation status is unavailable")
+ value = state["response"]
+ coverage = value.get("coverage") or {}
+ require(value.get("clusterStatusComplete") is True
+ and coverage.get("expected") == len(fleet.nodes) and coverage.get("responded") == len(fleet.nodes)
+ and coverage.get("unknown") == 0 and value.get("healActiveTasks") == 0 and value.get("healQueueLength") == 0,
+ "invalid request left active responsibility or incomplete peer coverage")
+ branches[name] = {"verdict": "PASS", "httpStatus": 400, "code": code, "noActiveResponsibility": True}
+ after = fleet.snapshot("physical-after-negatives")
+ require(physical_content(before) == physical_content(after), "invalid requests changed the physical fixture")
+ inventory = fleet.record("s3-after-negatives", probe.inventory(bucket, unversioned_current_tags=True))
+ fleet.record("negative-isolation-proof", {"physicalUnchanged": True, "s3": compare_inventories(baseline, inventory)})
+ return branches
+
+
+def physical_content(snapshot):
+ return {node: {key: value[key] for key in ("files", "formats")} for node, value in snapshot.items()}
+
+
+def validate_topology(snapshot, topology, *, expected_sets=1):
+ require(topology in {"H1", "H2"} and len(snapshot) == (1 if topology == "H1" else 4),
+ "wrong node count for Heal topology")
+ records = [(node, value) for node, entry in snapshot.items() for value in entry["formats"]]
+ require(len(records) == (4 if topology == "H1" else 16), "wrong physical disk count")
+ values = [item["value"] for _, item in records]
+ sets = values[0]["xl"]["sets"]
+ require(type(expected_sets) is int and (expected_sets == 1 or topology == "H2" and expected_sets == 2),
+ "unsupported explicit set layout")
+ identities = [identity for group in sets for identity in group]
+ require(len(sets) == expected_sets and all(len(group) == len(records) // expected_sets for group in sets)
+ and len(identities) == len(set(identities)) == len(records),
+ "explicit topology lacks complete independently formatted erasure sets")
+ require(all(value["format"] == "xl" and value["xl"]["sets"] == sets
+ and value["id"] == values[0]["id"] for value in values)
+ and {value["xl"]["this"] for value in values} == set(identities),
+ "physical disk UUID, pool and set mapping differs")
+ if topology == "H2":
+ require(all(len({item["value"]["xl"]["this"] for owner, item in records if owner == node
+ and item["value"]["xl"]["this"] in group}) == 4 // expected_sets
+ for node in snapshot for group in sets), "each H2 set must span every original physical node")
+ return {"topology": topology, "nodes": len(snapshot), "independentDrives": len(records),
+ "poolIndex": 0, "setIndex": 0 if expected_sets == 1 else None, "diskUUIDs": identities,
+ "setCount": expected_sets, "actualSets": sets}
+
+
+def validate_healthy(status, bucket, key, disposition):
+ outcome = status.get("outcome") or {}
+ require(status.get("summary") == "finished" and status.get("truncated", False) is False
+ and outcome.get("execution", {}).get("state") == "completed"
+ and outcome.get("coverage") == "complete" and outcome.get("objectsTruncated") is False,
+ "Heal did not prove complete terminal coverage")
+ counters = outcome.get("counters", {})
+ expected = {"processed": 1, "healed": 0, "unchanged": 1, "skipped": 0,
+ "failed": 0, "unknown": 0, "attemptFailures": 0, "overflowed": False}
+ require(all(type(counters.get(name)) is type(value) and counters[name] == value
+ for name, value in expected.items()), "healthy object counters do not prove exactly one unchanged object")
+ objects = outcome.get("objects", [])
+ require(len(objects) == 1, "healthy task returned incomplete or additional object receipts")
+ identity = objects[0].get("identity", {})
+ require(identity.get("bucket") == bucket and identity.get("object") == key
+ and type(identity.get("poolIndex")) is int and identity["poolIndex"] == 0
+ and type(identity.get("setIndex")) is int and identity["setIndex"] == 0
+ and bool(identity.get("bucketIncarnationId")), "healthy receipt lacks the exact object responsibility identity")
+ require(objects[0].get("disposition", {}).get("state") == disposition,
+ f"expected {disposition} receipt; observed {objects[0].get('disposition')}")
+
+
+class Fleet:
+ def __init__(self, nodes, evidence, run_id, tool_directory=None, *, runtime_profile="manual", resource_profile="standard",
+ require_replacement_admission=False):
+ require(runtime_profile in {"lock-timeout", "lock-successor", "frozen-race-legacy", "outcome-legacy", "outcome-grace", "queue-pressure", "orphan-preserve", "orphan-before", "orphan-after", "manual", "authority-split", "multi-pool", "bounded-capacity", "journal-bounded", "lock-quorum", "offline-replay", "durable-mrf", "mrf-crash", "flapping", "read-repair", "scanner", "disabled", "overlap", "root-restart", "stale-rejoin", "replacement", "schema-refusal", "scope-isolation", "incarnation-checkpoint", "incarnation-mrf"}, "unsupported Heal runtime profile")
+ require(resource_profile in {"standard", "cursor", "full-restart"}, "unsupported Heal resource profile")
+ self.nodes, self.evidence, self.run_id = nodes, evidence, run_id
+ self.tool_directory = tool_directory
+ self.runtime_profile = runtime_profile
+ self.require_replacement_admission = bool(require_replacement_admission)
+ self.node_runtime_profiles = {}
+ self.resource_profile = resource_profile
+ self.sequence = 0
+ self.reserved = []
+ self.faults = []
+ self.bucket_record_faults = []
+ self.drive_gates = set()
+ self.quorum_gates = set()
+ self.network_gates = set()
+ self.generation_faults = []
+ self.orphan_faults = []
+ self.journal_faults = []
+ self.incarnation_replays = []
+ self.secret_values = set()
+ self.frozen_processes = {}
+ self.snapshot_buckets = None
+
+ def record(self, label, value):
+ self.sequence += 1
+ def sanitized(item):
+ if isinstance(item, str):
+ for secret in sorted(self.secret_values, key=len, reverse=True):
+ item = item.replace(secret, "[REDACTED]")
+ return item
+ if isinstance(item, dict):
+ return {sanitized(key): sanitized(entry) for key, entry in item.items()}
+ if isinstance(item, list):
+ return [sanitized(entry) for entry in item]
+ return item
+ save(self.evidence / f"{self.sequence:05d}-{label}.json", sanitized(value))
+ return value
+
+ def ssh(self, node):
+ return ["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=10", "-o", "StrictHostKeyChecking=accept-new",
+ "-p", os.environ.get("RUSTFS_SSH_PORT", "22"),
+ os.environ.get("RUSTFS_SSH_USER", "azureuser") + "@" + node]
+
+ def remote(self, node, action, **params):
+ if action == "snapshot" and self.snapshot_buckets is not None:
+ require("buckets" not in params, "physical bucket scope must use the frozen fleet binding")
+ params["buckets"] = self.snapshot_buckets
+ value = {"runId": self.run_id, "action": action, **params}
+ source = NODE_SOURCE.read_text()
+ if action in {"caller-socket", "process-metrics"}:
+ source += "\nimport types\ncaller_socket_backend = types.ModuleType('caller_socket')\nexec(" + repr(
+ NODE_SOURCE.with_name("caller_socket.py").read_text()) + ", caller_socket_backend.__dict__)\n"
+ if action in {"mpu-race-backups", "version-race-backups"}:
+ source += "\nimport types\nmpu_race_backups_backend = types.ModuleType('mpu_race_backups')\nexec(" + repr(
+ NODE_SOURCE.with_name("mpu_race_backups.py").read_text()) + ", mpu_race_backups_backend.__dict__)\n"
+ if action in {"race-io-cap", "race-io-observe"}:
+ source += "\nimport types\nrace_io_backend = types.ModuleType('race_io')\nexec(" + repr(
+ NODE_SOURCE.with_name("race_io.py").read_text()) + ", race_io_backend.__dict__)\n"
+ if action == "mpu-race-scene":
+ source += "\nimport types\nmpu_race_backend = types.ModuleType('mpu_race_physical')\nexec(" + repr(
+ NODE_SOURCE.with_name("mpu_race_physical.py").read_text()) + ", mpu_race_backend.__dict__)\n"
+ if action == "network-delete-records":
+ source += "\nimport types\nnetwork_delete_records_backend = types.ModuleType('network_delete_records')\nexec(" + repr(
+ NODE_SOURCE.with_name("network_delete_records.py").read_text()) + ", network_delete_records_backend.__dict__)\n"
+ if action == "mpu-scene" or action.startswith("retained-scene-"):
+ source += "\nimport types\nmpu_scene_backend = types.ModuleType('mpu_scene')\nexec(" + repr(
+ NODE_SOURCE.with_name("mpu_scene.py").read_text()) + ", mpu_scene_backend.__dict__)\n"
+ if action.startswith(("retained-scene-", "owner-markers-")):
+ source += "\nimport types\nretained_scene_backend = types.ModuleType('retained_scene')\nexec(" + repr(
+ NODE_SOURCE.with_name("retained_scene.py").read_text()) + ", retained_scene_backend.__dict__)\n"
+ if action.startswith("owner-markers-"):
+ source += "\nimport types\nowner_recovery_backend = types.ModuleType('owner_recovery')\nexec(" + repr(
+ NODE_SOURCE.with_name("owner_recovery.py").read_text()) + ", owner_recovery_backend.__dict__)\n"
+ if action.startswith("network-") or action in {"cleanup", "quota-audit-finalize", "journal-rejection-audit", "journal-rejection-audit-finalize", "journal-retained-audit", "journal-retained-audit-finalize"}:
+ network_source = NODE_SOURCE.with_name("network_gate.py").read_text()
+ source += "\nimport types\nnetwork_gate_backend = types.ModuleType('network_gate')\nexec(" + repr(
+ network_source) + ", network_gate_backend.__dict__)\nNETWORK_GATE_SOURCE = " + repr(network_source) + "\n"
+ if self.runtime_profile in {"bounded-capacity", "journal-bounded"}:
+ source += "\nimport types\nbounded_volume_backend = types.ModuleType('bounded_volume')\nexec(" + repr(
+ NODE_SOURCE.with_name("bounded_volume.py").read_text()) + ", bounded_volume_backend.__dict__)\n"
+ if action == "mrf-records" or action.startswith(("journal-", "incarnation-")):
+ source += "\nimport types\nmrf = types.ModuleType('mrf_records')\nexec(" + repr(
+ NODE_SOURCE.with_name("mrf_records.py").read_text()) + ", mrf.__dict__)\n"
+ if action == "replacement-records":
+ source += "\nimport types\nreplacement_journal = types.ModuleType('replacement_journal')\nexec(" + repr(
+ NODE_SOURCE.with_name("replacement_journal.py").read_text()) + ", replacement_journal.__dict__)\n"
+ if action.startswith("generation-") or action.startswith("orphan-"):
+ source += "\nimport types\ngeneration_backend = types.ModuleType('generation_fault')\nexec(" + repr(
+ NODE_SOURCE.with_name("generation_fault.py").read_text()) + ", generation_backend.__dict__)\n"
+ if action in {"apply-fault", "recover-fault", "verify-part", "verify-parts", "verify-inline",
+ "bucket-record-snapshot", "bucket-record-apply", "bucket-record-recover", "format-schema-fault", "encryption-configure", "outcome-legacy-phase", "mpu-race-backups", "version-race-backups", "start"} or action.startswith("generation-") or action.startswith("orphan-") or action.startswith(("journal-", "incarnation-", "owner-markers-")):
+ source += "\nimport types\nfault = types.ModuleType('heal_fault')\nexec(" + repr(
+ NODE_SOURCE.with_name("heal_fault.py").read_text()) + ", fault.__dict__)\n"
+ if action.startswith(("journal-", "incarnation-")):
+ source += "\nimport types\njournal_fault_backend = types.ModuleType('journal_fault')\nexec(" + repr(
+ NODE_SOURCE.with_name("journal_fault.py").read_text()) + ", journal_fault_backend.__dict__)\n"
+ if action.startswith("incarnation-"):
+ source += "\nimport types\nincarnation_replay_backend = types.ModuleType('incarnation_replay')\nexec(" + repr(
+ NODE_SOURCE.with_name("incarnation_replay.py").read_text()) + ", incarnation_replay_backend.__dict__)\n"
+ if action.startswith("orphan-"):
+ source += "\nimport types\norphan_backend = types.ModuleType('orphan_fault')\nexec(" + repr(
+ NODE_SOURCE.with_name("orphan_fault.py").read_text()) + ", orphan_backend.__dict__)\n"
+ if action.startswith("bucket-record-"):
+ source += "\nimport types\nbucket_records = types.ModuleType('bucket_records')\nexec(" + repr(
+ NODE_SOURCE.with_name("bucket_records.py").read_text()) + ", bucket_records.__dict__)\n"
+ if action == "format-schema-fault":
+ source += "\nimport types\nschema_faults = types.ModuleType('schema_faults')\nexec(" + repr(
+ NODE_SOURCE.with_name("schema_faults.py").read_text()) + ", schema_faults.__dict__)\n"
+ if action.startswith("retained-scene-") or action in {"encryption-configure", "snapshot", "audit-finalize", "quota-audit-finalize", "journal-rejection-audit", "journal-rejection-audit-finalize", "journal-retained-audit", "journal-retained-audit-finalize"}:
+ source += "\nimport types\nencryption_runtime = types.ModuleType('encryption_runtime')\nexec(" + repr(
+ NODE_SOURCE.with_name("encryption_runtime.py").read_text()) + ", encryption_runtime.__dict__)\n"
+ replacement_actions = {"start", "cleanup", "audit-finalize", "quota-audit-finalize", "journal-rejection-audit", "journal-rejection-audit-finalize", "journal-retained-audit", "journal-retained-audit-finalize", "replace-drive", "replacement-state", "replacement-probe", "foreign-format", "restore-replacement", "snapshot", "format-schema-fault"}
+ if action in {"start", "cleanup", "audit-finalize", "quota-audit-finalize", "journal-rejection-audit", "journal-rejection-audit-finalize", "journal-retained-audit", "journal-retained-audit-finalize", "drive-gate", "drive-gate-arm-live", "replace-drive", "quorum-gate", "quorum-gate-probe"}:
+ source += "\nimport types\nquorum_gate_backend = types.ModuleType('quorum_gate')\nexec(" + repr(
+ NODE_SOURCE.with_name("quorum_gate.py").read_text()) + ", quorum_gate_backend.__dict__)\n"
+ if action in {"drive-gate", "drive-gate-arm-live", "drive-gate-probe", "drive-gate-activate", "gate-state", "drive-gate-release-live"} | replacement_actions:
+ source += "\nimport types\ngate = types.ModuleType('drive_gate')\nexec(" + repr(
+ NODE_SOURCE.with_name("drive_gate.py").read_text()) + ", gate.__dict__)\n"
+ if action in replacement_actions:
+ source += "\nimport sys\nsys.modules['drive_gate'] = gate\nreplacement = types.ModuleType('replace_drive')\nexec(" + repr(
+ NODE_SOURCE.with_name("replace_drive.py").read_text()) + ", replacement.__dict__)\n"
+ source += "\nmain(" + repr(value) + ")\n"
+ timeout = 600 if self.resource_profile in {"cursor", "full-restart"} and action == "snapshot" else 180
+ if action in {"bounded-volumes-create", "bounded-volumes-fill"}:
+ timeout = 600
+ result = subprocess.run([*self.ssh(node), "sudo -n python3 -"],
+ input=source, capture_output=True, text=True, timeout=timeout)
+ require(result.returncode == 0, f"Heal node {node} {action} failed: {result.stderr[-1200:]}")
+ response = json.loads(result.stdout)
+ if action in {"journal-observe", "journal-capture"} and isinstance(response, dict) and "observationUnavailable" in response:
+ message = response["observationUnavailable"]
+ require(set(response) == {"observationUnavailable"} and isinstance(message, str)
+ and message.startswith("NOT_OBSERVED:"), "invalid durable-record observation refusal")
+ raise ValueError(message)
+ return response
+
+ def snapshot(self, label, *, quiescent=False, metadata_faults=None, buckets=None):
+ return self.record(label, {node: self.remote(node, "snapshot", quiescent=quiescent,
+ metadataFaults=(metadata_faults or {}).get(node, {}),
+ **({"buckets": buckets} if buckets is not None else {})) for node in self.nodes})
+
+ def stop(self):
+ with ThreadPoolExecutor(max_workers=len(self.nodes)) as executor:
+ results = list(executor.map(lambda node: self.remote(node, "stop"), self.nodes))
+ return self.record("quiescent", dict(zip(self.nodes, results, strict=True)))
+
+ def start(self):
+ with ThreadPoolExecutor(max_workers=len(self.nodes)) as executor:
+ results = list(executor.map(lambda node: self.remote(node, "start"), self.nodes))
+ self.record("started", dict(zip(self.nodes, results, strict=True)))
+ self.wait_ready()
+ for node in sorted(self.drive_gates):
+ gate = self.remote(node, "gate-state")
+ if gate.get("mode") == "permission-denied" and gate.get("phase") == "armed":
+ self.record("live-permission-injection", {"node": node, **self.remote(node, "drive-gate-activate")})
+
+ def deploy(self, binary_sha, bucket, access, secret, *, before_start=None):
+ self.secret_values.update({access, secret})
+ if self.require_replacement_admission:
+ admission = {node: self.remote(node, "replacement-admission-preflight") for node in self.nodes}
+ self.record("replacement-admission-preflight", admission)
+ if not all(value.get("readOnly") is True and value.get("mutated") is False
+ and value.get("canonicalPassClaimed") is False and value.get("admitted") is True
+ for value in admission.values()):
+ raise ReplacementAdmissionBlocked(admission)
+ checked = {node: self.remote(node, "preflight", resourceProfile=self.resource_profile,
+ **({'runtimeProfile': self.runtime_profile}
+ if self.runtime_profile in {'lock-timeout', 'lock-successor'} else {}))
+ for node in self.nodes}
+ self.record("preflight", checked)
+ require(len({value["hostname"] for value in checked.values()}) == len(self.nodes),
+ "Heal node aliases must resolve to distinct actual hosts")
+ paths = checked[self.nodes[0]]["dataRoots"]
+ volumes = paths if len(self.nodes) == 1 else [f"http://rustfs-node{index}:9000{path}"
+ for index in range(1, 5) for path in paths]
+ if self.runtime_profile == "multi-pool":
+ require(len(self.nodes) == 4, "H5 requires all four current Heal nodes")
+ try: from .multi_pool_layout import volumes as pool_volumes
+ except ImportError: from multi_pool_layout import volumes as pool_volumes
+ volumes = pool_volumes(self.run_id)
+ if self.runtime_profile == "scope-isolation":
+ require(len(self.nodes) == 4, "two-set isolation requires the four-node topology")
+ volumes = [f"http://rustfs-node{index}:9000{path}" for path in paths for index in range(1, 5)]
+ self.record("integrity-fleet-attestation", {"scope": "this run's private H1/H2 endpoint set",
+ "allParticipatingNodesUseCandidate": True, "binarySha256": binary_sha,
+ "nodes": self.nodes, "volumes": volumes, "mixedVersionsAllowed": False,
+ "precondition": "reserve verifies the candidate copy on every peer before any service starts",
+ "newFixtureWrites": "protected independent shard commitments",
+ "legacyDefaultResult": "retained separately; unknown is never a successful positive receipt"})
+ for node in self.nodes:
+ # Reserve may partially mutate before an SSH failure. Preserve its
+ # namespace unless the root-owned marker proves safe finalization.
+ self.reserved.append(node)
+ inspector_sha = hashlib.sha256((self.tool_directory / "heal-meta-inspect").read_bytes()).hexdigest() \
+ if self.tool_directory else None
+ value = self.remote(node, "reserve", hostname=checked[node]["hostname"], mounts=checked[node]["mounts"],
+ binarySha256=binary_sha, bucket=bucket, volumes=volumes, access=access, secret=secret,
+ inspectorSha256=inspector_sha, runtimeProfile=self.node_runtime_profiles.get(node, self.runtime_profile),
+ resourceProfile=self.resource_profile)
+ self.record("reserved", {"node": node, **value})
+ if self.tool_directory:
+ tool = self.tool_directory / "heal-meta-inspect"
+ target = "/var/lib/rustfs-auto-testing/heal/" + self.run_id + "/heal-meta-inspect"
+ transfer = subprocess.run([*self.ssh(node), "sudo -n dd of=" + target
+ + " conv=excl,fsync oflag=nofollow bs=1M status=none"],
+ input=tool.read_bytes(), capture_output=True, timeout=120)
+ require(transfer.returncode == 0, "exclusive inspector upload failed: " + transfer.stderr.decode()[-800:])
+ self.record("inspector-upload", {"node": node, **self.remote(node, "install-inspector")})
+ if before_start is not None:
+ before_start()
+ self.start()
+
+ def wait_ready(self):
+ opener = request.build_opener(request.ProxyHandler({}))
+ deadline = time.monotonic() + 180
+ pending = {f"http://rustfs-node{index}:9000" for index in range(1, len(self.nodes) + 1)}
+ while pending and time.monotonic() < deadline:
+ for endpoint in tuple(pending):
+ try:
+ with opener.open(endpoint + "/health/ready", timeout=5) as response:
+ if response.status == 200:
+ pending.remove(endpoint)
+ except OSError:
+ pass
+ if pending:
+ time.sleep(2)
+ require(not pending, "Heal deployment did not make every node ready")
+
+ def cleanup(self, *, preserve_namespaces=False):
+ failures = [{"state": "DIRTY", "namespacesRetained": True,
+ "error": "unresolved C11 S3 mutation requires retained physical evidence"}] if preserve_namespaces else []
+ if self.incarnation_replays:
+ try:
+ self.stop()
+ for node, params in tuple(self.incarnation_replays):
+ receipt = self.record("C11-original-replay-evidence-finalization", {"node": node,
+ **self.remote(node, "incarnation-finalize", **params)})
+ require(receipt.get("originalEvidenceExported") is True
+ and receipt.get("namespaceFinalizationAllowed") is True
+ and receipt.get("backupRescueUsed") is False
+ and receipt.get("productRepairClaimed") is False,
+ "C11 original replay evidence was not exported before namespace finalization")
+ self.incarnation_replays.remove((node, params))
+ except Exception as exc:
+ failures.append({"error": str(exc), "state": "DIRTY", "namespacesRetained": True})
+
+ if self.journal_faults:
+ try:
+ self.stop()
+ if self.runtime_profile == "journal-bounded":
+ for node in self.reserved:
+ self.record("bounded-journal-filler-recovery", {"node": node, **self.remote(
+ node, "bounded-volumes-release", quiescent=True)})
+ for node, params in tuple(self.journal_faults):
+ receipt = self.record("durable-record-recovery", {"node": node, **self.remote(node, "journal-recover", **params)})
+ require(receipt.get("backupRetained") is True and receipt.get("originalResponsibilityRecreated") is False
+ and receipt.get("productRepairClaimed") is False,
+ "durable-record rescue lost the original evidence or claims product repair")
+ self.journal_faults.remove((node, params))
+ except Exception as exc:
+ failures.append({"error": str(exc), "state": "DIRTY", "namespacesRetained": True})
+ if self.orphan_faults:
+ try:
+ self.stop()
+ for node, params in tuple(self.orphan_faults):
+ receipt = self.record("orphan-last-ACK-recovery", {"node": node, **self.remote(node, "orphan-recover", **params)})
+ require(receipt.get("state") == "CLEAR" and receipt.get("lastACKAuthorityRestored") is True,
+ "C10 lacks complete last-ACK authority restoration")
+ self.orphan_faults.remove((node, params))
+ except Exception as exc:
+ failures.append({"error": str(exc), "state": "DIRTY", "namespacesRetained": True})
+ if self.generation_faults:
+ try:
+ self.stop()
+ for node, params in tuple(self.generation_faults):
+ receipt = self.record("generation-last-ACK-recovery", {"node": node, **self.remote(node, "generation-recover", **params)})
+ require(receipt.get("state") == "CLEAR" and receipt.get("originalLastACKGenerationRestored") is True,
+ "generation replay lacks complete last-ACK restoration")
+ self.generation_faults.remove((node, params))
+ except Exception as exc:
+ failures.append({"error": str(exc), "state": "DIRTY", "namespacesRetained": True})
+ if self.network_gates:
+ try:
+ self.stop()
+ for node in tuple(self.network_gates):
+ self.record("network-gate-recovery", {"node": node, **self.remote(node, "network-release")})
+ self.network_gates.remove(node)
+ except Exception as exc:
+ failures.append({"error": str(exc), "state": "DIRTY", "namespacesRetained": True})
+ for node, identity in tuple(self.frozen_processes.items()):
+ try:
+ current = self.remote(node, "process")
+ if current["pid"]:
+ self.record("resume-for-finalization", {"node": node, **self.remote(node, "process-signal",
+ signal="SIGCONT", expected=identity)})
+ del self.frozen_processes[node]
+ except Exception as exc:
+ failures.append({"node": node, "error": str(exc), "state": "DIRTY", "namespacesRetained": True})
+ if self.drive_gates:
+ try:
+ self.stop()
+ for node in tuple(self.drive_gates):
+ self.record("gate-recovery", {"node": node, **self.remote(node, "drive-gate", mode="clear")})
+ self.drive_gates.remove(node)
+ except Exception as exc:
+ failures.append({"error": str(exc), "state": "DIRTY", "namespacesRetained": True})
+ if self.quorum_gates:
+ try:
+ self.stop()
+ for node in tuple(self.quorum_gates):
+ self.record("quorum-gate-recovery", {"node": node, **self.remote(node, "quorum-gate", mode="clear")})
+ self.quorum_gates.remove(node)
+ except Exception as exc:
+ failures.append({"error": str(exc), "state": "DIRTY", "namespacesRetained": True})
+ for node in self.reserved:
+ try:
+ self.record("logs", {"node": node, **self.remote(node, "logs")})
+ except Exception as exc:
+ self.record("logs-error", {"node": node, "error": str(exc)})
+ if (self.faults or self.bucket_record_faults) and not failures:
+ try:
+ self.stop()
+ if self.runtime_profile in {"bounded-capacity", "journal-bounded"}:
+ for node in self.reserved:
+ self.record("bounded-filler-recovery", {"node": node, **self.remote(
+ node, "bounded-volumes-release", quiescent=True)})
+ for node, params in self.faults:
+ self.record("fault-recovery", {"node": node, **self.remote(node, "recover-fault", **params)})
+ for node, params in self.bucket_record_faults:
+ self.record("bucket-record-rescue", {"node": node, **self.remote(node, "bucket-record-recover", **params)})
+ except Exception as exc:
+ failures.append({"error": str(exc), "state": "DIRTY", "namespacesRetained": True})
+ if not failures:
+ for node in self.reserved:
+ try:
+ self.record("cleanup", {"node": node, **self.remote(node, "cleanup")})
+ except Exception as exc:
+ failures.append({"node": node, "error": str(exc)})
+ self.record("recovery", {"cleanup": "FAIL" if failures else "PASS", "errors": failures})
+ return not failures
+
+
+def run_topology(args, topology, nodes):
+ directory = args.evidence / topology
+ directory.mkdir()
+ run_id = f"gh-{os.environ['GITHUB_RUN_ID']}-{os.environ.get('GITHUB_RUN_ATTEMPT', '1')}-{topology.lower()}"
+ fleet = Fleet(nodes, directory, run_id, args.tool_directory)
+ bucket = "rv-heal-" + hashlib.sha256(run_id.encode()).hexdigest()[:16]
+ access, secret = secrets.token_hex(16), secrets.token_hex(24)
+ os.environ.update(HEAL_ACCESS_KEY=access, HEAL_SECRET_KEY=secret)
+ candidate = json.loads(args.candidate.read_text())
+ if args.tool_directory:
+ fleet.record("inspector-build", verify_inspector(args.tool_directory, candidate))
+ binding = {"schemaVersion": 1, "runId": run_id, "jumpHostOnly": True, "exclusiveHealService": True,
+ "leaseExpiresAt": (datetime.now(timezone.utc) + timedelta(minutes=30)).isoformat(),
+ "bucketPrefix": bucket, "endpoints": {f"node{index}": f"http://rustfs-node{index}:9000"
+ for index in range(1, len(nodes) + 1)}, "defaultEndpoint": "node1",
+ "candidateCommit": candidate["source_sha"], "harnessCommit": args.testing_commit,
+ "candidatePackageSha256": candidate["package_sha256"], "candidateBinarySha256": args.binary_sha,
+ "rcBinarySha256": hashlib.sha256(args.rc_bin.read_bytes()).hexdigest(),
+ "maxInventoryObjects": 128, "maxInventoryBytes": 512 * 1024**2}
+ fleet.record("binding", binding)
+ result = {"topology": topology, "verdict": "RUNNING", "branches": {}, "cleanup": "PENDING"}
+ active = []
+ probe = Probe(binding)
+ try:
+ fleet.deploy(args.binary_sha, bucket, access, secret)
+ initial = fleet.snapshot("deployed-topology")
+ fleet.record("topology-proof", validate_topology(initial, topology))
+ recorder = Recorder(probe, directory / "fixture")
+ expected = seed(recorder, bucket, "DB", 128,
+ [0, 1, 4095, 4096, 4097, 1024**2 - 1, 1024**2, 1024**2 + 1, 5 * 1024**2 + 123])
+ baseline = fleet.record("s3-baseline", probe.inventory(bucket, unversioned_current_tags=True))
+ fleet.record("fixture-manifest", {"expected": expected, **verify_seed(expected, baseline)})
+ before = fleet.snapshot("physical-baseline")
+ fleet.record("fault", {"fault": "F0", "injected": False, "backgroundRepairDisabled": True})
+ if args.case_id == "heal.admin-invalid-request":
+ result["branches"] = invalid_requests(probe, fleet, bucket, before, baseline)
+ if args.case_id == "heal.admin-authorization":
+ try:
+ from .authorization import authorization_requests
+ except ImportError:
+ from authorization import authorization_requests
+ result["branches"] = authorization_requests(probe, fleet, bucket, before, baseline, active, BASE_OPTIONS)
+ healthy_branches = (("normal", 1, "metadata_healthy"), ("deep", 2, "verified_healthy")) \
+ if args.case_id == "heal.admin-healthy-noop" else ()
+ for branch, mode, disposition in healthy_branches:
+ receipts = []
+ for entry in expected:
+ key = entry["key"]
+ path = "/rustfs/admin/v3/heal/" + quote(bucket, safe="") + "/" + quote(key, safe="/")
+ options = {**BASE_OPTIONS, "scanMode": mode}
+ started = fleet.record("trigger-" + branch, probe.admin(path, [], json.dumps(options).encode()))
+ require(started.get("httpStatus") == 200 and isinstance(started.get("response"), dict),
+ f"healthy Heal task admission failed (HTTP {started.get('httpStatus')})")
+ token = started["response"].get("clientToken")
+ require(isinstance(token, str) and bool(token), "healthy Heal task admission omitted its token")
+ active.append((path, token))
+ deadline = time.monotonic() + 120
+ while True:
+ observed = fleet.record("status-" + branch, probe.admin(path, [("clientToken", token)], b""))
+ require(observed.get("httpStatus") == 200 and isinstance(observed.get("response"), dict),
+ "Heal status request failed")
+ terminal = observed.get("response") or {}
+ if terminal.get("summary") != "running":
+ break
+ require(time.monotonic() < deadline, "healthy Heal task exceeded deadline")
+ time.sleep(0.2)
+ validate_healthy(terminal, bucket, key, disposition)
+ active.remove((path, token))
+ for _ in range(2):
+ repeated = fleet.record("repeat-" + branch, probe.admin(path, [("clientToken", token)], b""))
+ require(repeated.get("httpStatus") == 200, "repeated Heal status request failed")
+ validate_healthy(repeated.get("response") or {}, bucket, key, disposition)
+ require(canonical(repeated["response"]["outcome"]) == canonical(terminal["outcome"]),
+ "terminal responsibility changed on repeated status")
+ receipts.append({"key": key, "token": token, "disposition": disposition})
+ after = fleet.snapshot("physical-after-" + branch)
+ require(physical_content(before) == physical_content(after), "healthy Heal changed physical fixture or format")
+ inventory = fleet.record("s3-after-" + branch, probe.inventory(bucket, unversioned_current_tags=True))
+ comparison = compare_inventories(baseline, inventory)
+ result["branches"][branch] = {"verdict": "PASS", "receipts": receipts,
+ "physicalUnchanged": True, "s3": comparison}
+ result["verdict"] = "PASS"
+ except (Exception, KeyboardInterrupt) as exc:
+ result.update(verdict="FAIL", failure={"type": type(exc).__name__, "message": str(exc)})
+ if fleet.reserved:
+ try:
+ fleet.snapshot("physical-failure-scene")
+ except Exception as capture:
+ fleet.record("failure-scene-error", {"error": str(capture)})
+ finally:
+ for path, token in active:
+ try:
+ fleet.record("stop-unresolved", probe.admin(path, [("clientToken", token), ("forceStop", "true")], b""))
+ except Exception as exc:
+ fleet.record("stop-error", {"error": str(exc)})
+ result["cleanup"] = "PASS" if fleet.cleanup() else "FAIL"
+ fleet.record("topology-result", result)
+ return result
+
+
+def main():
+ def interrupted(signum, frame):
+ raise KeyboardInterrupt(f"Heal workflow interrupted by signal {signum}")
+ signal.signal(signal.SIGTERM, interrupted)
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--candidate", type=Path, required=True)
+ parser.add_argument("--case-id", choices=("heal.admin-healthy-noop", "heal.admin-invalid-request", "heal.admin-authorization"),
+ default="heal.admin-healthy-noop")
+ parser.add_argument("--testing-commit", required=True)
+ parser.add_argument("--binary-sha", required=True)
+ parser.add_argument("--rc-bin", type=Path, required=True)
+ parser.add_argument("--evidence", type=Path, required=True)
+ parser.add_argument("--tool-directory", type=Path)
+ args = parser.parse_args()
+ nodes = os.environ.get("RUSTFS_NODES", "vm000 vm001 vm002 vm003").split()
+ require(len(nodes) >= 4 and len(set(nodes)) == len(nodes)
+ and all(re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", node) for node in nodes),
+ "A01 H1/H2 requires the four current auto-testing Heal nodes")
+ require(bool(re.fullmatch(r"[0-9a-f]{64}", args.binary_sha)), "invalid installed binary digest")
+ require(bool(re.fullmatch(r"[1-9][0-9]*", os.environ.get("GITHUB_RUN_ID", ""))), "workflow fleet identity required")
+ results = []
+ try:
+ for topology, selected in (("H1", nodes[:1]), ("H2", nodes[:4])):
+ result = run_topology(args, topology, selected)
+ results.append(result)
+ if result["cleanup"] != "PASS":
+ break
+ finally:
+ save(args.evidence / "healthy-summary.json", {"caseId": args.case_id, "results": results,
+ "cleanup": "PASS" if results and all(value["cleanup"] == "PASS" for value in results) else "FAIL",
+ "complete": len(results) == 2 and all(value["verdict"] == "PASS" and value["cleanup"] == "PASS" for value in results)})
+ require(len(results) == 2 and all(value["verdict"] == "PASS" and value["cleanup"] == "PASS" for value in results),
+ "Heal case requires every H1/H2 branch; inspect topology-result evidence")
+
+
+if __name__ == "__main__":
+ try:
+ main()
+ except Exception as exc:
+ print(f"Heal validation failed: {exc}", file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_heal/incarnation.py b/scripts/validation_heal/incarnation.py
new file mode 100644
index 00000000..708b2e7b
--- /dev/null
+++ b/scripts/validation_heal/incarnation.py
@@ -0,0 +1,704 @@
+"""C11: same-name bucket recreation with original metadata and journal replay."""
+
+import argparse
+from datetime import datetime, timedelta, timezone
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import secrets
+import signal
+import sys
+import time
+from urllib.parse import quote
+import uuid
+
+try:
+ from .heal_probe import Probe, compare_inventories, payload, require, save
+ from .heal_workload import Recorder, seed, verify_seed
+ from .healthy import Fleet, physical_content, validate_topology
+ from .fleet_node import PROCESS_FIELDS
+ from .bucket_metadata import system_snapshot, system_files
+ from .inspector import verify as verify_inspector
+ from .manager_fixture import verify as verify_manager
+ from .incarnation_native import verify as verify_storage
+ from .matrix import select
+ from .shards import files, map_target, run_task, verify_cohort
+ from . import checkpoint as checkpoints, journal_fault as journal, mrf_replay as mrf
+ from .mrf_records import canonical_uuid, verify_reference as verify_mrf_reference
+ from .persistence import exact_records
+ from .scanner_repair import JournalWindow
+ from .stop import idle_fleet, status
+ from .root_restart import recovery_status
+ from .versions import public_version_id, validate_version_receipts, verify_quorum_contract, version_identity
+except ImportError:
+ from heal_probe import Probe, compare_inventories, payload, require, save
+ from heal_workload import Recorder, seed, verify_seed
+ from healthy import Fleet, physical_content, validate_topology
+ from fleet_node import PROCESS_FIELDS
+ from bucket_metadata import system_snapshot, system_files
+ from inspector import verify as verify_inspector
+ from manager_fixture import verify as verify_manager
+ from incarnation_native import verify as verify_storage
+ from matrix import select
+ from shards import files, map_target, run_task, verify_cohort
+ import checkpoint as checkpoints, journal_fault as journal, mrf_replay as mrf
+ from mrf_records import canonical_uuid, verify_reference as verify_mrf_reference
+ from persistence import exact_records
+ from scanner_repair import JournalWindow
+ from stop import idle_fleet, status
+ from root_restart import recovery_status
+ from versions import public_version_id, validate_version_receipts, verify_quorum_contract, version_identity
+
+
+CASE_ID = "heal.bucket-incarnation-recreation"
+BRANCHES = ("old-metadata-rejoin", "old-checkpoint-replay", "old-mrf-replay", "native-incarnation-fence")
+SIZE = 5 * 1024**2 + 123
+
+
+def old_manifest(expected):
+ wanted = {version_identity(entry): entry for entry in expected}
+ require(wanted and len(wanted) == len(expected)
+ and all(isinstance(pair[0], str) and pair[0] and isinstance(pair[1], str)
+ and str(uuid.UUID(pair[1])) == pair[1] and uuid.UUID(pair[1]).int != 0
+ and type(entry.get("deleteMarker")) is bool for pair, entry in wanted.items()),
+ "C11 old manifest lacks distinct acknowledged data-version/marker identities")
+ require(any(entry["deleteMarker"] for entry in expected) and any(not entry["deleteMarker"] for entry in expected),
+ "C11 old DV fixture must contain both actual data versions and markers")
+ return wanted
+
+
+def delete_old_bucket(recorder, bucket, expected):
+ """Only normal S3 deletion can establish the bucket deletion boundary."""
+ wanted = old_manifest(expected)
+ verify_seed(expected, recorder.probe.inventory(bucket))
+ removed = []
+ for (key, version), entry in wanted.items():
+ ack = recorder.call("delete_object", {"Bucket": bucket, "Key": key, "VersionId": version})
+ require(ack.get("ResponseMetadata", {}).get("HTTPStatusCode") == 204
+ and ack.get("VersionId") == version
+ and ack.get("DeleteMarker", False) is entry["deleteMarker"],
+ "C11 explicit DELETE did not acknowledge the exact old data/marker kind and identity")
+ removed.append({"key": key, "versionId": version, "deleteMarker": entry["deleteMarker"]})
+ empty = recorder.probe.inventory(bucket)
+ uploads = recorder.call("list_multipart_uploads", {"Bucket": bucket, "MaxUploads": 1})
+ require(empty.get("entries") == [] and uploads.get("IsTruncated") is False and uploads.get("Uploads", []) == [],
+ "C11 old bucket still contains a version, marker or pending upload")
+ ack = recorder.call("delete_bucket", {"Bucket": bucket})
+ require(ack.get("ResponseMetadata", {}).get("HTTPStatusCode") == 204,
+ "C11 normal S3 delete_bucket did not succeed")
+ absent = recorder.probe.s3("head_bucket", {"Bucket": bucket})
+ require(absent.get("acknowledged") is False and absent.get("resultUnknown") is False
+ and absent.get("response", {}).get("ResponseMetadata", {}).get("HTTPStatusCode") == 404,
+ "C11 successful delete_bucket has no observed absence boundary")
+ return {"deleted": removed, "deleteBucketAcknowledged": True, "absenceObserved": True,
+ "internalNamespaceRemovalUsed": False}
+
+
+def seed_successor(recorder, bucket, old):
+ old_ids = set(old_manifest(old))
+ recorder.call("create_bucket", {"Bucket": bucket})
+ recorder.call("put_bucket_versioning", {"Bucket": bucket, "VersioningConfiguration": {"Status": "Enabled"}})
+ recorder.call("put_bucket_tagging", {"Bucket": bucket, "Tagging": {
+ "TagSet": [{"Key": "validation-generation", "Value": "successor"}]}})
+ expected = []
+ for index, key in enumerate(sorted({entry["key"] for entry in old})):
+ for generation in range(3):
+ body = payload(f"{bucket}/{key}/successor/{generation}", SIZE + generation + 71)
+ ack = recorder.call("put_object", {"Bucket": bucket, "Key": key,
+ "Metadata": {"heal-fixture": "C11", "incarnation": "successor", "generation": str(generation)},
+ "Tagging": "suite=heal&incarnation=successor"}, body)
+ version = ack.get("VersionId")
+ require(isinstance(version, str) and str(uuid.UUID(version)) == version and uuid.UUID(version).int != 0
+ and (key, version) not in old_ids, "C11 successor reused an old or missing version ACK")
+ expected.append({"key": key, "versionId": version, "deleteMarker": False,
+ "isLatest": generation == 2, "payload": {"size": len(body), "sha256": hashlib.sha256(body).hexdigest()}})
+ if index % 2 == 0:
+ ack = recorder.call("delete_object", {"Bucket": bucket, "Key": key})
+ require(ack.get("DeleteMarker") is True and isinstance(ack.get("VersionId"), str)
+ and str(uuid.UUID(ack["VersionId"])) == ack["VersionId"] and uuid.UUID(ack["VersionId"]).int != 0
+ and (key, ack["VersionId"]) not in old_ids, "C11 successor marker reused an old identity")
+ expected[-1]["isLatest"] = False
+ expected.append({"key": key, "versionId": ack["VersionId"], "deleteMarker": True, "isLatest": True})
+ require(not (old_ids & set(old_manifest(expected)))
+ and {entry["payload"]["sha256"] for entry in old if not entry["deleteMarker"]}.isdisjoint(
+ entry["payload"]["sha256"] for entry in expected if not entry["deleteMarker"]),
+ "C11 successor bytes/identities do not distinguish the deleted generation")
+ return expected
+
+
+def stale_member_unchanged(before, after, member):
+ require({node: row["formats"] for node, row in before.items()} ==
+ {node: row["formats"] for node, row in after.items()}, "C11 recreation changed disk identity or topology")
+ left, right = files(before), files(after)
+ original = {identity: row for identity, row in left.items() if identity[:2] == member}
+ require(original and original == {identity: row for identity, row in right.items() if identity[:2] == member},
+ "C11 gated old metadata member changed before its actual rejoin")
+ return {"node": member[0], "disk": member[1], "originalOldMemberBytesUnchanged": True}
+
+
+def successor_physical(before, after, bucket, expected, old, member):
+ wanted = {}
+ for entry in expected:
+ wanted.setdefault(entry["key"], set()).add((entry["versionId"], entry["deleteMarker"], entry["isLatest"]))
+ old_ids, prior = set(old_manifest(old)), files(before)
+ current = files(after)
+ require({identity: row for identity, row in prior.items() if not identity[2].startswith(bucket + "/")} ==
+ {identity: row for identity, row in current.items() if not identity[2].startswith(bucket + "/")},
+ "C11 recreation or rejoin changed a control bucket's physical content")
+ require({node: row["formats"] for node, row in before.items()} ==
+ {node: row["formats"] for node, row in after.items()}, "C11 rejoin changed disk formats")
+ for node, census in after.items():
+ for disk in range(4):
+ actual = {}
+ for item in census["files"]:
+ meta = item.get("metadata")
+ if item["disk"] != disk or not meta: continue
+ if meta.get("bucket") != bucket: continue
+ require(meta.get("bucket") == bucket and meta.get("key") in wanted,
+ "C11 final metadata escaped successor ACK scope")
+ rows = meta.get("versions")
+ require(isinstance(rows, list), "C11 final metadata omitted decoded versions")
+ identities = {(public_version_id(row.get("versionId")), row["deleted"], row["isLatest"]) for row in rows}
+ require(len(identities) == len(rows) and meta["key"] not in actual
+ and not any((meta["key"], version) in old_ids for version, _, _ in identities),
+ "C11 final metadata resurrected an old identity or duplicated successor responsibility")
+ actual[meta["key"]] = identities
+ require(actual == wanted, "C11 rejoined member lacks complete successor versions/markers")
+ cohorts = []
+ for entry in expected:
+ if entry["deleteMarker"]: continue
+ target = map_target(after, bucket, entry["key"], "data", 1, entry["versionId"], latest_required=False)
+ for shard in target["cohort"]:
+ if (shard["node"], shard["disk"]) == member: continue
+ original = prior.get((shard["node"], shard["disk"], shard["relativePath"]))
+ require(original and original["sha256"] == shard["sha256"] and original["size"] == shard["size"],
+ "C11 old member overwrote a surviving successor shard")
+ cohorts.append(target)
+ return cohorts
+
+
+def config(probe, bucket):
+ versioning = probe.s3("get_bucket_versioning", {"Bucket": bucket})
+ tags = probe.s3("get_bucket_tagging", {"Bucket": bucket})
+ require(versioning.get("acknowledged") is True and versioning.get("resultUnknown") is False
+ and versioning.get("response", {}).get("Status") == "Enabled"
+ and tags.get("acknowledged") is True and tags.get("resultUnknown") is False
+ and tags.get("response", {}).get("TagSet") == [{"Key": "validation-generation", "Value": "successor"}],
+ "C11 old bucket configuration polluted the acknowledged successor configuration")
+ return {"versioning": versioning, "tagging": tags}
+
+
+def run_metadata_rejoin(args, topology, nodes, candidate):
+ directory = args.evidence / topology / "old-metadata-rejoin"
+ directory.mkdir(parents=True)
+ run_id = f"gh-{os.environ['GITHUB_RUN_ID']}-{os.environ.get('GITHUB_RUN_ATTEMPT', '1')}-{topology.lower()}"
+ fleet = Fleet(nodes, directory, run_id, args.tool_directory)
+ bucket = "rv-heal-" + hashlib.sha256(run_id.encode()).hexdigest()[:16]
+ control = bucket + "-control"
+ fleet.snapshot_buckets = [bucket, control]
+ access, secret = secrets.token_hex(16), secrets.token_hex(24)
+ os.environ.update(HEAL_ACCESS_KEY=access, HEAL_SECRET_KEY=secret)
+ binding = {"schemaVersion": 1, "runId": run_id, "jumpHostOnly": True, "exclusiveHealService": True,
+ "leaseExpiresAt": (datetime.now(timezone.utc) + timedelta(minutes=30)).isoformat(), "bucketPrefix": bucket,
+ "endpoints": {f"node{index}": f"http://rustfs-node{index}:9000" for index in range(1, len(nodes) + 1)},
+ "defaultEndpoint": "node1", "candidateCommit": candidate["source_sha"], "harnessCommit": args.testing_commit,
+ "candidatePackageSha256": candidate["package_sha256"], "candidateBinarySha256": args.binary_sha,
+ "rcBinarySha256": hashlib.sha256(args.rc_bin.read_bytes()).hexdigest(),
+ "maxInventoryObjects": 128, "maxInventoryBytes": 512 * 1024**2}
+ fleet.record("binding", binding)
+ tool = fleet.record("inspector-build", verify_inspector(args.tool_directory, candidate))
+ verify_quorum_contract(tool, candidate)
+ probe, active, pending = Probe(binding), [], None
+ result = {"topology": topology, "fixture": "DV", "branch": "old-metadata-rejoin", "verdict": "RUNNING", "cleanup": "PENDING"}
+ try:
+ fleet.deploy(args.binary_sha, bucket, access, secret)
+ fleet.record("actual-topology", validate_topology(fleet.snapshot("deployed-topology"), topology))
+ recorder = Recorder(probe, directory / "fixture")
+ pending = {"operation": "seed-old-bucket"}
+ controls = seed(recorder, control, "DB", 1, [SIZE])
+ old = seed(recorder, bucket, "DV", 3, [SIZE])
+ old_manifest(old)
+ fleet.record("old-ACK-manifest", {"expected": old, **verify_seed(old, probe.inventory(bucket))})
+ pending = None
+ initial = run_task(probe, fleet, {"bucket": bucket, "key": ""}, 2, active, option_overrides={"recursive": True})
+ old_incarnation = validate_version_receipts(initial, bucket, old, 2)["bucketIncarnationId"]
+ idle_fleet(fleet, binding)
+ fleet.stop()
+ original = fleet.snapshot("old-generation-before-gate", quiescent=True)
+ system_snapshot(fleet, [bucket, control], "old-generation-physical-bucket-records")
+ entry = next(entry for entry in old if not entry["deleteMarker"])
+ target = map_target(original, bucket, entry["key"], "data", 1, entry["versionId"], latest_required=False)
+ shard = next(row for row in reversed(target["cohort"]) if row["node"] == nodes[-1])
+ member = shard["node"], shard["disk"]
+ fleet.drive_gates.add(member[0])
+ fleet.record("old-member-gate", fleet.remote(member[0], "drive-gate", disk=member[1], mode="permission-denied"))
+ fleet.start()
+ denied = fleet.record("old-member-service-denial", fleet.remote(member[0], "drive-gate-probe"))
+ require(denied.get("readProbe", {}).get("errno") == 13 and denied.get("errno") == 13
+ and denied.get("dacBypassRemoved") is True, "C11 isolated member has no actual read/write denial")
+ pending = {"operation": "normal-S3-delete-and-recreate"}
+ result["deletedOriginal"] = fleet.record("normal-S3-bucket-deletion", delete_old_bucket(recorder, bucket, old))
+ successor = seed_successor(recorder, bucket, old)
+ baseline = fleet.record("successor-ACK-bound-S3", probe.inventory(bucket))
+ verify_seed(successor, baseline)
+ fleet.record("successor-config-ACK", config(probe, bucket))
+ pending = None
+ authority = fleet.snapshot("old-and-successor-physical-scopes-before-rejoin")
+ result["staleMember"] = stale_member_unchanged(original, authority, member)
+ processes = {node: fleet.remote(node, "process") for node in nodes}
+ release = fleet.record("same-process-old-member-rejoin", fleet.remote(member[0], "drive-gate-release-live"))
+ require(release.get("sameOriginalProcess") is True and release.get("serviceRestarted") is False,
+ "C11 old member rejoin replaced its original process")
+ fleet.drive_gates.remove(member[0])
+ rejoined = {node: fleet.remote(node, "process") for node in nodes}
+ require(all(all(processes[node].get(name) == rejoined[node].get(name) for name in PROCESS_FIELDS) for node in nodes),
+ "C11 rejoin changed the original candidate/process/unit")
+ fleet.snapshot("actual-rejoined-old-member-before-successor-task")
+ status = run_task(probe, fleet, {"bucket": bucket, "key": ""}, 2, active,
+ option_overrides={"recursive": True}, status_endpoints=tuple(binding["endpoints"])[1:])
+ responsibility = validate_version_receipts(status, bucket, successor, 2,
+ required_repairs={version_identity(entry) for entry in successor if not entry["deleteMarker"]})
+ require(responsibility["bucketIncarnationId"] != old_incarnation, "C11 new bucket rebound the deleted incarnation")
+ result["incarnations"] = {"old": old_incarnation, "successor": responsibility["bucketIncarnationId"]}
+ result["successorResponsibility"] = responsibility
+ idle_fleet(fleet, binding)
+ fleet.stop()
+ after = fleet.snapshot("successor-physical-before-business-reads", quiescent=True)
+ successor_system = system_snapshot(fleet, [bucket, control], "successor-physical-bucket-records-before-reads")
+ cohorts = successor_physical(authority, after, bucket, successor, old, member)
+ result["streaming"] = [verify_cohort(fleet, target, "successor-full-shard-integrity") for target in cohorts]
+ fleet.start()
+ for endpoint in binding["endpoints"]:
+ peer = Probe(binding, endpoint)
+ inventory = fleet.record("all-peer-successor-S3", {"endpoint": endpoint, **peer.inventory(bucket)})
+ verify_seed(successor, inventory)
+ compare_inventories(baseline, inventory)
+ control_inventory = fleet.record("all-peer-control-bucket-S3", {"endpoint": endpoint,
+ **peer.inventory(control, unversioned_current_tags=True)})
+ verify_seed(controls, control_inventory)
+ fleet.record("all-peer-successor-config", {"endpoint": endpoint, **config(peer, bucket)})
+ for (key, version), entry in old_manifest(old).items():
+ missing = fleet.record("all-peer-deleted-incarnation-version", peer.s3("head_object", {
+ "Bucket": bucket, "Key": key, "VersionId": version}))
+ require(missing.get("acknowledged") is False and missing.get("resultUnknown") is False
+ and missing.get("response", {}).get("ResponseMetadata", {}).get("HTTPStatusCode") == 404,
+ "C11 old version/marker reappeared in the successor namespace")
+ fleet.stop()
+ require(physical_content(after) == physical_content(fleet.snapshot("after-successor-business-reads", quiescent=True)),
+ "C11 business reads repaired data not proved before reads")
+ require(system_files(successor_system) == system_files(system_snapshot(fleet, [bucket, control],
+ "successor-physical-bucket-records-after-reads")), "C11 reads changed acknowledged successor bucket records")
+ result.update(verdict="PASS", successorManifestVerified=True, oldBackupRestorationUsed=False,
+ checkpointReplayClaimed=False, mrfReplayClaimed=False, nativeCommitFenceClaimed=False)
+ except (Exception, KeyboardInterrupt) as exc:
+ result.update(verdict="FAIL", failure={"type": type(exc).__name__, "message": str(exc)})
+ try:
+ fleet.stop()
+ fleet.snapshot("original-recreation-failure-scene", quiescent=True)
+ except Exception as error: result["failureEvidenceError"] = str(error)
+ finally:
+ if pending is not None:
+ result["unresolvedMutation"] = pending
+ fleet.record("unresolved-S3-mutation", pending)
+ clean = fleet.cleanup(preserve_namespaces=True)
+ result["cleanup"] = "FAIL"
+ result["environmentCleanupSucceeded"] = clean
+ else:
+ result["cleanup"] = "PASS" if fleet.cleanup() else "FAIL"
+ fleet.record("incarnation-branch-result", result)
+ return result
+
+
+
+def record_digests(rows):
+ return [{name: row[name] for name in ("disk", "relativePath", "size", "sha256")}
+ for row in rows if row["present"]]
+
+
+def checkpoint_rejection(current, frozen):
+ outcome = current.get("outcome") or {}
+ original = frozen["value"]["admin"]["outcome"]
+ require(current.get("summary") == "stopped" and journal.same_json(current.get("settings"), checkpoints.OPTIONS)
+ and isinstance(current.get("detail"), str) and current["detail"].startswith("stale_bucket_incarnation:")
+ and outcome.get("execution") == {"state": "aborted", "reason": "untraversable"}
+ and outcome.get("coverage") == "partial" and current.get("truncated") is False,
+ "C11 old checkpoint has no explicit original stale-incarnation failure")
+ require(all(journal.same_json(outcome.get(name), original.get(name))
+ for name in ("counters", "objects", "objectsTruncated")),
+ "C11 old checkpoint processed successor work or rewrote original acknowledged progress")
+ return {"originalTaskId": frozen["taskId"], "bucketIncarnationId": frozen["bucketIncarnationId"],
+ "staleIncarnationRejected": True, "originalProgressRetained": True, "successorWorkClaimed": False}
+
+
+def checkpoint_terminal(observed, frozen, current):
+ token = frozen["taskId"]
+ names = journal.names("root", token)
+ records = exact_records(observed, token, "root")
+ require(set(records) == set(names[1:]), "C11 old checkpoint lacks an exact durable terminal/report or is still runnable")
+ owner = frozen["rootAdmission"]
+ require(all((row["node"], row["disk"]) == (owner["node"], owner["disk"]) for row in records.values()),
+ "C11 old checkpoint terminal moved original durable ownership")
+ terminal = journal.decode_json(checkpoints.bytes_of(records[names[1]]))
+ report = journal.decode_json(checkpoints.bytes_of(records[names[2]]))
+ require(type(terminal.get("schema")) is int and terminal["schema"] == 1 and terminal.get("task_id") == token
+ and journal.same_json(terminal.get("heal_type"), owner["value"]["heal_type"])
+ and journal.same_json(terminal.get("options"), owner["value"]["options"])
+ and terminal.get("status") == {"Failed": {"error": current["detail"]}}
+ and type(report.get("schema")) is int and report["schema"] == 1
+ and journal.same_json(report.get("terminal"), terminal)
+ and journal.same_json(report.get("outcome"), current["outcome"]),
+ "C11 old checkpoint durable rejection differs from original token/scope/outcome")
+ return {"originalTaskId": token, "sameOriginalOwner": True, "durableFailureRetained": True}
+
+
+def retained_mrf(checkpoints_by_node, obligations, bindings, successor_incarnation):
+ require(successor_incarnation and all(owner["bucketIncarnationId"] != successor_incarnation
+ for owners in bindings.values() for owner in owners), "C11 successor reused the old MRF incarnation")
+ retained = []
+ for obligation in obligations:
+ wanted = mrf.identity(obligation)
+ for owner in bindings[wanted]:
+ value = checkpoints_by_node[owner["node"]]
+ require(value.get("present") is True and value["owner"] != owner["checkpointOwner"]
+ and value["sequence"] > owner["sequence"], "C11 old MRF responsibility was lost or not durably reclassified")
+ canonical_uuid(value["owner"])
+ matches = [item for item in value["intents"] if mrf.identity(item) == wanted
+ and item.get("intentDigest") == owner["intentDigest"]]
+ require(len(matches) == 1, "C11 exact old MRF ACK responsibility disappeared or duplicated")
+ lifecycle = matches[0].get("lifecycle", {})
+ state = lifecycle.get("state", {})
+ require(lifecycle.get("responsibility_id") == owner["responsibilityId"]
+ and lifecycle.get("source_bucket_incarnation_id") == owner["bucketIncarnationId"]
+ and lifecycle.get("last_operator_acceptance") is None
+ and state.get("state") == "bucket_incarnation_changed"
+ and set(state) == {"state", "source_bucket_incarnation_id", "observed_bucket_incarnation_id", "detected_at_ms"}
+ and state["source_bucket_incarnation_id"] == owner["bucketIncarnationId"]
+ and state["observed_bucket_incarnation_id"] == successor_incarnation
+ and type(state["detected_at_ms"]) is int and 0 < state["detected_at_ms"] <= int(time.time() * 1000) + 5000,
+ "C11 MRF responsibility was retired, accepted, rebound or lacks durable incarnation-change diagnostics")
+ retained.append({"node": owner["node"], "identity": obligation,
+ "originalCheckpointOwner": owner["checkpointOwner"], "currentCheckpointOwner": value["owner"],
+ "responsibilityId": owner["responsibilityId"], "state": state})
+ return {"retainedResponsibilities": retained, "operatorAcceptanceUsed": False,
+ "oldResponsibilityRetired": False, "successorRepairClaimed": False}
+
+
+def replay_isolation(before, after):
+ require(physical_content(before) == physical_content(after),
+ "C11 old persisted work changed successor/control metadata, shards, integrity or disk formats")
+ return {"allSuccessorAndControlPhysicalBytesUnchanged": True, "formatUnchanged": True}
+
+
+def successor_receipts(current, bucket, successor):
+ successor_incarnations = {row.get("identity", {}).get("bucketIncarnationId")
+ for row in current.get("outcome", {}).get("objects", [])}
+ require(len(successor_incarnations) == 1 and None not in successor_incarnations,
+ "C11 successor diagnostics mix or omit bucket generations")
+ successor_incarnation = successor_incarnations.pop()
+ # Complete independent manifests cover diagnostics capped at 128 objects.
+ wanted = old_manifest(successor)
+ journal.task_id(successor_incarnation)
+ outcome = current.get("outcome") or {}
+ require(current.get("summary") == "finished" and type(current.get("truncated")) is bool
+ and current["truncated"] is (len(wanted) > 128) and outcome.get("execution") == {"state": "completed"}
+ and outcome.get("coverage") == "complete" and outcome.get("objectsTruncated") is (len(wanted) > 128)
+ and len(outcome.get("objects", [])) == min(128, len(wanted)),
+ "C11 successor scan lacks truthful bounded diagnostic coverage")
+ counts = {"processed": len(wanted), "healed": 0, "unchanged": len(wanted), "skipped": 0,
+ "failed": 0, "unknown": 0, "attemptFailures": 0, "overflowed": False}
+ require(all(type(outcome.get("counters", {}).get(key)) is type(value)
+ and outcome["counters"][key] == value for key, value in counts.items()),
+ "C11 successor scan counters differ from its complete ACK manifest")
+ seen = set()
+ for row in outcome["objects"]:
+ item = row.get("identity", {})
+ pair = item.get("object"), public_version_id(item.get("versionId"))
+ require(pair in wanted and pair not in seen and item.get("bucket") == bucket
+ and item.get("bucketIncarnationId") == successor_incarnation
+ and type(item.get("poolIndex")) is int and item["poolIndex"] == 0
+ and type(item.get("setIndex")) is int and item["setIndex"] == 0
+ and row.get("disposition", {}).get("state") == ("metadata_healthy" if wanted[pair]["deleteMarker"] else "verified_healthy"),
+ "C11 successor diagnostic changed an ACK, kind, generation or successful disposition")
+ seen.add(pair)
+ return {"bucketIncarnationId": successor_incarnation,
+ "completeIndependentManifestsRequired": True, "boundedDiagnosticsAreCompleteManifest": len(wanted) <= 128}
+
+
+def run_replay(args, topology, nodes, branch, candidate):
+ require(branch in {"old-checkpoint-replay", "old-mrf-replay"}, "unregistered C11 replay branch")
+ family = "checkpoint" if branch == "old-checkpoint-replay" else "mrf"
+ directory = args.evidence / topology / branch
+ directory.mkdir(parents=True)
+ run_id = f"gh-{os.environ['GITHUB_RUN_ID']}-{os.environ.get('GITHUB_RUN_ATTEMPT', '1')}-{topology.lower()}"
+ fleet = Fleet(nodes, directory, run_id, args.tool_directory, runtime_profile="incarnation-" + family,
+ resource_profile="cursor" if family == "checkpoint" else "standard")
+ bucket = "rv-heal-" + hashlib.sha256(run_id.encode()).hexdigest()[:16]
+ control = bucket + "-control"
+ fleet.snapshot_buckets = [bucket, control]
+ access, secret = secrets.token_hex(16), secrets.token_hex(24)
+ os.environ.update(HEAL_ACCESS_KEY=access, HEAL_SECRET_KEY=secret)
+ binding = {"schemaVersion": 1, "runId": run_id, "jumpHostOnly": True, "exclusiveHealService": True,
+ "leaseExpiresAt": (datetime.now(timezone.utc) + timedelta(minutes=45)).isoformat(), "bucketPrefix": bucket,
+ "endpoints": {f"node{index}": f"http://rustfs-node{index}:9000" for index in range(1, len(nodes) + 1)},
+ "defaultEndpoint": "node1", "candidateCommit": candidate["source_sha"], "harnessCommit": args.testing_commit,
+ "candidatePackageSha256": candidate["package_sha256"], "candidateBinarySha256": args.binary_sha,
+ "rcBinarySha256": hashlib.sha256(args.rc_bin.read_bytes()).hexdigest(),
+ "maxInventoryObjects": 512, "maxInventoryBytes": 2 * 1024**3}
+ fleet.record("binding", binding)
+ tool = fleet.record("inspector-build", verify_inspector(args.tool_directory, candidate))
+ verify_quorum_contract(tool, candidate)
+ if family == "mrf": fleet.record("MRF-reviewed-source-contract", verify_mrf_reference(candidate))
+ probe, active, pending = Probe(binding), [], None
+ result = {"topology": topology, "fixture": "DV", "branch": branch, "verdict": "RUNNING", "cleanup": "PENDING",
+ "canonicalCasePassClaimed": False}
+ try:
+ fleet.deploy(args.binary_sha, bucket, access, secret)
+ fleet.record("actual-topology", validate_topology(fleet.snapshot("deployed-topology"), topology))
+ recorder = Recorder(probe, directory / "fixture")
+ pending = {"operation": "seed-old-DV"}
+ old = seed(recorder, bucket, "DV", 80 if family == "checkpoint" else 3, [1024**2 + 123])
+ old_manifest(old)
+ fleet.record("old-ACK-manifest", {"expected": old, **verify_seed(old, probe.inventory(bucket))})
+ pending = None
+ frozen, bindings, obligations, gate_member = None, None, None, None
+ if family == "checkpoint":
+ token = checkpoints.admit(probe, fleet, active)
+ frozen, process_identity = checkpoints.freeze(fleet, token, bucket)
+ frozen = checkpoints.stop_for_fault(fleet, token, bucket, frozen, process_identity)
+ old_incarnation = frozen["bucketIncarnationId"]
+ fleet.record("original-old-incarnation-checkpoint", frozen)
+ else:
+ initial = run_task(probe, fleet, {"bucket": bucket, "key": ""}, 2, active,
+ option_overrides={"recursive": True})
+ old_incarnation = validate_version_receipts(initial, bucket, old, 2)["bucketIncarnationId"]
+ idle_fleet(fleet, binding)
+ fleet.stop()
+ original = fleet.snapshot("old-before-partial-write-gate", quiescent=True)
+ entry = next(entry for entry in old if not entry["deleteMarker"])
+ target = map_target(original, bucket, entry["key"], "data", 1, entry["versionId"], latest_required=False)
+ shard = next(row for row in reversed(target["cohort"]) if row["node"] == nodes[-1])
+ gate_member = shard["node"], shard["disk"]
+ fleet.drive_gates.add(gate_member[0])
+ fleet.record("old-MRF-member-gate", fleet.remote(gate_member[0], "drive-gate", disk=gate_member[1], mode="permission-denied"))
+ fleet.start()
+ denial = fleet.record("old-MRF-member-denial", fleet.remote(gate_member[0], "drive-gate-probe"))
+ require(denial.get("readProbe", {}).get("errno") == 13 and denial.get("errno") == 13
+ and denial.get("dacBypassRemoved") is True, "C11 MRF partial write has no actual read/write denial")
+ earliest = int(time.time() * 1000) - 5000
+ pending = {"operation": "old-MRF-partial-write-ACKs"}
+ old, obligations, _ = mrf.acknowledge(recorder, bucket, "offline-put", old)
+ verify_seed(old, probe.inventory(bucket))
+ pending = None
+ deadline = time.monotonic() + mrf.PERSIST_SECONDS
+ while True:
+ try:
+ bindings = mrf.pending_bindings(mrf.read_checkpoints(fleet, nodes, "C11-old-committed-MRF"), obligations, earliest)
+ break
+ except ValueError as exc:
+ if not str(exc).startswith("NOT_OBSERVED:"): raise
+ require(time.monotonic() < deadline, "C11 old ACK-bound MRF persistence window not observed")
+ time.sleep(.5)
+ require(all(owner["bucketIncarnationId"] == old_incarnation for owners in bindings.values() for owner in owners),
+ "C11 MRF ACKs belong to another old generation")
+ fleet.record("original-old-MRF-ACK-bindings", {"obligations": obligations,
+ "bindings": [{"identity": list(key), "owners": value} for key, value in bindings.items()]})
+ fleet.stop()
+ bindings = mrf.pending_bindings(mrf.read_checkpoints(fleet, nodes, "C11-old-MRF-stopped", quiescent=True), obligations, earliest)
+ token = None
+ params = {"family": family, "taskId": token, "bucketIncarnationId": old_incarnation}
+ for node in nodes:
+ if family == "checkpoint":
+ observed = [*fleet.remote(node, "journal-census", family="checkpoint", taskId=token)["records"],
+ *fleet.remote(node, "journal-census", family="root", taskId=token)["records"]]
+ # The adapter uses disk-major order over both exact families.
+ observed.sort(key=lambda row: (row["disk"], [*journal.names("checkpoint", token), *journal.names("root", token)].index(row["relativePath"])))
+ else:
+ observed = fleet.remote(node, "incarnation-census", **params)["records"]
+ fleet.record("C11-independent-original-record-census", {"node": node, **params, "records": observed})
+ fleet.incarnation_replays.append((node, params.copy()))
+ receipt = fleet.record("archive-and-detach-original-replay", {"node": node, **fleet.remote(
+ node, "incarnation-capture", **params, expectedRecords=record_digests(observed))})
+ require(receipt.get("detached") is True and receipt.get("originalProductBytes") is True
+ and receipt.get("replacementBytesAccepted") is False, "C11 original durable record detachment is incomplete")
+ detached = fleet.record("C11-independent-detached-record-census", {"node": node, **fleet.remote(
+ node, "incarnation-census", **params)})
+ require(all(not row["present"] for row in detached["records"]), "C11 independent observer still sees original replay sources")
+ fleet.record("C11-transition-runtime", {"node": node, **fleet.remote(node, "incarnation-phase", phase="transition")})
+ if gate_member is not None:
+ fleet.record("C11-old-MRF-member-release", fleet.remote(gate_member[0], "drive-gate", mode="clear"))
+ fleet.drive_gates.remove(gate_member[0])
+ fleet.start()
+ pending = {"operation": "normal-S3-delete-and-successor-recreate"}
+ result["deletedOriginal"] = fleet.record("normal-S3-bucket-deletion", delete_old_bucket(recorder, bucket, old))
+ successor = seed_successor(recorder, bucket, old)
+ controls = seed(recorder, control, "DB", 1, [SIZE])
+ baseline = fleet.record("successor-ACK-bound-S3", probe.inventory(bucket))
+ verify_seed(successor, baseline)
+ fleet.record("successor-config-ACK", config(probe, bucket))
+ pending = None
+ successor_status = run_task(probe, fleet, {"bucket": bucket, "key": ""}, 2, active,
+ option_overrides={"recursive": True})
+ successor_responsibility = successor_receipts(successor_status, bucket, successor)
+ require(successor_responsibility["bucketIncarnationId"] != old_incarnation,
+ "C11 successor generation was not distinct before any original replay")
+ idle_fleet(fleet, binding)
+ fleet.stop()
+ authority = fleet.snapshot("complete-successor-physical-before-original-replay", quiescent=True)
+ successor_physical(authority, authority, bucket, successor, old, (None, None))
+ for node in nodes:
+ receipt = fleet.record("install-exact-original-replay", {"node": node, **fleet.remote(node, "incarnation-install", **params)})
+ require(receipt.get("installed") is True and receipt.get("originalArchiveRetained") is True,
+ "C11 original record transaction was not completely installed")
+ installed = fleet.record("C11-independent-installed-record-census", {"node": node, **fleet.remote(
+ node, "incarnation-census", **params)})
+ require(journal.same_json(installed["records"], receipt["records"]),
+ "C11 independent observer differs from the exact installed original bytes")
+ fleet.record("C11-replay-runtime", {"node": node, **fleet.remote(node, "incarnation-phase", phase="replay")})
+ authority = fleet.snapshot("successor-physical-before-original-replay", quiescent=True)
+ system_before = system_snapshot(fleet, [bucket, control], "successor-system-before-original-replay")
+ window = JournalWindow(fleet, "C11-original-replay", event_targets={"rustfs::heal::manager", "rustfs::heal::task", "rustfs::heal::mrf"})
+ fleet.start()
+ if family == "checkpoint":
+ deadline = time.monotonic() + 180
+ while True:
+ current = recovery_status(probe, fleet, token)
+ if current.get("summary") != "running": break
+ require(time.monotonic() < deadline, "C11 original checkpoint has no bounded terminal")
+ time.sleep(.2)
+ result["originalCheckpointRejection"] = checkpoint_rejection(current, frozen)
+ for endpoint in binding["endpoints"]:
+ peer = status(Probe(binding, endpoint), fleet, checkpoints.PATH, token, "C11-original-rejection-peer")
+ require(journal.same_json(peer.get("outcome"), current.get("outcome"))
+ and peer.get("summary") == current.get("summary") and peer.get("detail") == current.get("detail"),
+ "C11 peer lost the original rejected checkpoint outcome")
+ active.remove((checkpoints.PATH, token))
+ else:
+ deadline = time.monotonic() + 180
+ while True:
+ try:
+ result["retainedOldMrfResponsibility"] = retained_mrf(
+ mrf.read_checkpoints(fleet, nodes, "C11-old-MRF-replay-retained"), obligations, bindings,
+ successor_responsibility["bucketIncarnationId"])
+ break
+ except ValueError as exc:
+ fleet.record("C11-MRF-rejection-not-yet-observed", {"error": str(exc)})
+ require(time.monotonic() < deadline, "C11 MRF durable stale-generation diagnostic not observed: " + str(exc))
+ time.sleep(1)
+ result["originalMrfReplayEvents"] = window.capture()
+ fleet.stop()
+ replay_isolation(authority, fleet.snapshot("successor-physical-after-original-replay-before-reads", quiescent=True))
+ require(system_files(system_before) == system_files(system_snapshot(fleet, [bucket, control], "successor-system-after-original-replay")),
+ "C11 original replay changed successor bucket configuration/incarnation bytes")
+ if family == "checkpoint":
+ result["durableOriginalRejection"] = checkpoint_terminal(checkpoints.records(
+ fleet, token, "C11-durable-original-terminal", quiescent=True, family="root"), frozen, current)
+ fleet.start()
+ responsibility = successor_responsibility
+ if family == "mrf":
+ deadline = time.monotonic() + 180
+ while True:
+ try:
+ result["retainedOldMrfResponsibility"] = retained_mrf(mrf.read_checkpoints(fleet, nodes, "C11-retained-old-MRF"),
+ obligations, bindings, responsibility["bucketIncarnationId"])
+ break
+ except ValueError as exc:
+ fleet.record("C11-MRF-rejection-not-yet-observed", {"error": str(exc)})
+ require(time.monotonic() < deadline, "C11 MRF durable stale-generation diagnostic not observed: " + str(exc))
+ time.sleep(1)
+ result["originalMrfReplayEvents"] = window.capture()
+ idle_fleet(fleet, binding)
+ fleet.stop()
+ final = fleet.snapshot("successor-physical-before-business-reads", quiescent=True)
+ # Persisted old work has no authority to change any surviving successor bytes.
+ replay_isolation(authority, final)
+ cohorts = successor_physical(final, final, bucket, successor, old, (None, None))
+ cohorts.extend(map_target(final, control, entry["key"], "data", 1, entry["versionId"], latest_required=False)
+ for entry in controls if not entry["deleteMarker"])
+ result["fullShardIntegrity"] = [verify_cohort(fleet, target, "successor-full-shard-integrity") for target in cohorts]
+ fleet.start()
+ for endpoint in binding["endpoints"]:
+ peer = Probe(binding, endpoint)
+ inventory = fleet.record("all-peer-successor-S3", {"endpoint": endpoint, **peer.inventory(bucket)})
+ verify_seed(successor, inventory)
+ compare_inventories(baseline, inventory)
+ verify_seed(controls, fleet.record("all-peer-control-S3", {"endpoint": endpoint, **peer.inventory(control, unversioned_current_tags=True)}))
+ fleet.record("all-peer-successor-config", {"endpoint": endpoint, **config(peer, bucket)})
+ for key, version in old_manifest(old):
+ missing = fleet.record("all-peer-old-incarnation-version", peer.s3("head_object", {"Bucket": bucket, "Key": key, "VersionId": version}))
+ require(missing.get("acknowledged") is False and missing.get("resultUnknown") is False
+ and missing.get("response", {}).get("ResponseMetadata", {}).get("HTTPStatusCode") == 404,
+ "C11 original version/marker reappeared in the successor namespace")
+ fleet.stop()
+ replay_isolation(final, fleet.snapshot("successor-after-business-reads", quiescent=True))
+ require(system_files(system_before) == system_files(system_snapshot(fleet, [bucket, control], "successor-system-after-business-reads")),
+ "C11 business reads changed acknowledged successor bucket records")
+ if family == "checkpoint":
+ checkpoint_terminal(checkpoints.records(fleet, token, "C11-retained-terminal-after-business-reads", quiescent=True, family="root"), frozen, current)
+ else:
+ retained_mrf(mrf.read_checkpoints(fleet, nodes, "C11-retained-MRF-after-business-reads", quiescent=True),
+ obligations, bindings, responsibility["bucketIncarnationId"])
+ result.update(verdict="PASS", checkpointReplayClaimed=family == "checkpoint", mrfReplayClaimed=family == "mrf",
+ successorResponsibility=responsibility, oldBackupRestorationUsed=False, nativeCommitFenceClaimed=False)
+ except (Exception, KeyboardInterrupt) as exc:
+ result.update(verdict="FAIL", failure={"type": type(exc).__name__, "message": str(exc)})
+ try:
+ fleet.stop()
+ fleet.snapshot("original-replay-failure-scene", quiescent=True)
+ except Exception as error: result["failureEvidenceError"] = str(error)
+ finally:
+ clean = fleet.cleanup(preserve_namespaces=pending is not None)
+ result["cleanup"] = "PASS" if clean and pending is None else "FAIL"
+ if pending is not None:
+ result.update(unresolvedMutation=pending, environmentCleanupSucceeded=clean)
+ fleet.record("incarnation-replay-result", result)
+ return result
+
+
+def main():
+ def interrupted(signum, frame): raise KeyboardInterrupt(f"C11 interrupted by signal {signum}")
+ signal.signal(signal.SIGTERM, interrupted)
+ parser = argparse.ArgumentParser(description=__doc__)
+ for name in ("candidate", "rc-bin", "evidence", "tool-directory"): parser.add_argument("--" + name, type=Path, required=True)
+ parser.add_argument("--case-id", choices=(CASE_ID,), required=True)
+ parser.add_argument("--testing-commit", required=True)
+ parser.add_argument("--binary-sha", required=True)
+ args = parser.parse_args()
+ require(select(CASE_ID)[0]["variants"] == list(BRANCHES), "C11 source branch inventory changed")
+ candidate = json.loads(args.candidate.read_text())
+ native = verify_manager(os.environ.get("VALIDATION_HEAL_MANAGER_DIRECTORY", ""), candidate, CASE_ID)
+ save(args.evidence / "independent-native-incarnation-prerequisites.json", native)
+ storage = verify_storage(os.environ.get("VALIDATION_HEAL_NATIVE_DIRECTORY", ""), candidate, args.testing_commit)
+ save(args.evidence / "independent-native-storage-commit.json", storage)
+ require(re.fullmatch(r"[0-9a-f]{40}", args.testing_commit) and re.fullmatch(r"[0-9a-f]{64}", args.binary_sha)
+ and re.fullmatch(r"[1-9][0-9]*", os.environ.get("GITHUB_RUN_ID", "")), "C11 immutable workflow identities required")
+ nodes = os.environ.get("RUSTFS_NODES", "vm000 vm001 vm002 vm003").split()
+ require(len(nodes) == len(set(nodes)) == 4 and all(re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", node) for node in nodes),
+ "C11 requires the four distinct existing auto-testing Heal nodes")
+ results = []
+ try:
+ for topology, selected in (("H1", nodes[:1]), ("H2", nodes)):
+ for branch in BRANCHES:
+ if branch == "old-metadata-rejoin":
+ result = run_metadata_rejoin(args, topology, selected, candidate)
+ elif branch == "native-incarnation-fence":
+ result = {"requestedTopology": topology, "topology": None, "fixture": "DV", "branch": branch,
+ "verdict": "PREREQUISITE_PASS", "cleanup": "PASS", "fleetBranchExecuted": False,
+ "evidenceScope": "independent exact-candidate native storage regression",
+ "nativeProof": storage, "canonicalCasePassClaimed": False}
+ else:
+ result = run_replay(args, topology, selected, branch, candidate)
+ results.append(result)
+ if result["cleanup"] != "PASS": return 1
+ finally:
+ diagnostic_complete = len(results) == 8 and all(row["verdict"] == (
+ "PREREQUISITE_PASS" if row["branch"] == "native-incarnation-fence" else "PASS") for row in results)
+ save(args.evidence / "healthy-summary.json", {"caseId": CASE_ID, "results": results, "complete": False,
+ "diagnosticBranchesPassed": diagnostic_complete,
+ "cleanup": "PASS" if results and all(row["cleanup"] == "PASS" for row in results) else "FAIL",
+ "canonicalCasePassClaimed": False, "reason": "C11 full case review and canonical dispatch remain gated"})
+ return 1
+
+
+if __name__ == "__main__": sys.exit(main())
diff --git a/scripts/validation_heal/incarnation_native.py b/scripts/validation_heal/incarnation_native.py
new file mode 100644
index 00000000..0b8799dc
--- /dev/null
+++ b/scripts/validation_heal/incarnation_native.py
@@ -0,0 +1,49 @@
+"""Verify C11's candidate-bound physical storage and final-rename prerequisites."""
+
+import argparse
+import json
+from pathlib import Path
+import re
+
+try:
+ from . import native_regressions as suite
+except ImportError:
+ import native_regressions as suite
+
+
+CASE_ID = "heal.bucket-incarnation-recreation"
+
+
+def verify(directory, candidate, helper_sha):
+ directory = Path(directory)
+ api = json.loads((directory / "tool-run.json").read_text())
+ suite.require(type(api.get("id")) is int and api["id"] > 0
+ and type(api.get("run_attempt")) is int and api["run_attempt"] > 0
+ and api.get("status") == "completed" and api.get("conclusion") == "success"
+ and isinstance(api.get("head_sha"), str) and re.fullmatch(r"[0-9a-f]{40}", api["head_sha"])
+ and api.get("path") in {".github/workflows/rustfs-functional-chain.yml", ".github/workflows/rustfs-validation-tools.yml"},
+ "incarnation artifact has no exact successful hosted build attempt")
+ suite.require(candidate.get("source_sha") == suite.INCARNATION_CANDIDATE,
+ "incarnation commit barrier requires its exact reviewed source")
+ tools, evidence = directory / "native-tools", directory / "native-evidence"
+ manifest = json.loads((tools / "native-regressions.json").read_text())
+ proof = suite.verify_evidence(tools, evidence, candidate, api["id"], api["run_attempt"], helper_sha)
+ suite.require(proof["observedBarrierEvents"].get("incarnation-final-rename") == suite.INCARNATION_EVENTS,
+ "actual final rename, cancellation, physical drain and successor refusal were not observed")
+ return {**proof, "caseId": CASE_ID, "runId": api["id"], "runAttempt": api["run_attempt"],
+ "driverCommit": api["head_sha"], "collectorCommit": manifest["testingCommit"],
+ "casePrerequisiteTests": [{"regression": key, **value} for key, value in suite.INCARNATION_REGRESSIONS.items()],
+ "exactStorageCommitBarrierObserved": True, "canonicalFleetBranchesExecuted": False,
+ "checkpointMrfReplayClaimed": False, "physicalPowerLossClaimed": False}
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--directory", type=Path, required=True)
+ parser.add_argument("--candidate", type=Path, required=True)
+ parser.add_argument("--testing-commit", required=True)
+ args = parser.parse_args()
+ print(json.dumps(verify(args.directory, json.loads(args.candidate.read_text()), args.testing_commit), indent=2))
+
+
+if __name__ == "__main__": main()
diff --git a/scripts/validation_heal/incarnation_replay.py b/scripts/validation_heal/incarnation_replay.py
new file mode 100644
index 00000000..6e60ea2a
--- /dev/null
+++ b/scripts/validation_heal/incarnation_replay.py
@@ -0,0 +1,218 @@
+"""C11: archive and replay only original, candidate-owned recovery records.
+
+The stopped adapter accepts identities and digests, never caller-supplied
+replacement bytes. The archive survives every mutation and is exported before
+the private namespace can be removed.
+"""
+
+import base64
+import hashlib
+import json
+import os
+
+
+LEGACY_MRF_PATHS = (".rustfs.sys/buckets/.heal/mrf/journal.bin",
+ ".rustfs.sys/buckets/.heal/mrf/journal-scoped.bin")
+
+
+def require(condition, message):
+ if not condition: raise ValueError(message)
+
+
+def profile(value):
+ return value in {"incarnation-checkpoint", "incarnation-mrf"}
+
+
+def archive_name(relative):
+ return "C11-replay-" + hashlib.sha256(relative.encode()).hexdigest() + ".original"
+
+
+def identity(owner, request, journal):
+ require(profile(owner.get("runtimeProfile")) and request.get("runId") == owner["runId"],
+ "C11 replay requires its exact private recovery runtime")
+ family = "checkpoint" if owner["runtimeProfile"] == "incarnation-checkpoint" else "mrf"
+ require(request.get("family") == family, "C11 replay family differs from the bound runtime")
+ incarnation = journal.task_id(request.get("bucketIncarnationId"))
+ token = journal.task_id(request.get("taskId")) if family == "checkpoint" else None
+ require(family == "checkpoint" or request.get("taskId") is None, "MRF cannot invent a root task binding")
+ return {"runId": owner["runId"], "binarySha256": owner["binarySha256"], "bucket": owner["bucket"],
+ "family": family, "taskId": token, "bucketIncarnationId": incarnation}
+
+
+def paths(binding, journal):
+ if binding["family"] == "mrf": return [*journal.names("mrf"), *LEGACY_MRF_PATHS]
+ return [*journal.names("checkpoint", binding["taskId"]), *journal.names("root", binding["taskId"])]
+
+
+def read_optional(fd, relative, fault, limit):
+ try:
+ with fault.parent(fd, ["data", *relative.split("/")[:-1]]) as directory:
+ return fault.read_file(directory, relative.split("/")[-1], limit)
+ except FileNotFoundError:
+ return None
+
+
+def census(roots, binding, journal, fault):
+ rows = []
+ for disk, root in enumerate(roots):
+ with fault.directory(root) as fd:
+ for relative in paths(binding, journal):
+ raw = read_optional(fd, relative, fault, journal.LIMIT * 4)
+ row = {"disk": disk, "relativePath": relative, "present": raw is not None}
+ if raw is not None:
+ row.update(journal.entry(raw), rawBase64=base64.b64encode(raw).decode())
+ rows.append(row)
+ return rows
+
+
+def capture(state, roots, owner, request, journal, fault, mrf, checked):
+ require(owner.get("incarnationPhase", "capture") == "capture", "C11 cannot capture a later replay phase")
+ binding = identity(owner, request, journal)
+ rows = census(roots, binding, journal, fault)
+ actual = [{name: row[name] for name in ("disk", "relativePath", "size", "sha256")}
+ for row in rows if row["present"]]
+ require(journal.same_json(actual, request.get("expectedRecords")),
+ "C11 original stopped record census differs from its independently bound digests")
+ if binding["family"] == "checkpoint":
+ allowed = {journal.names("checkpoint", binding["taskId"])[0],
+ journal.names("checkpoint", binding["taskId"])[1],
+ journal.names("root", binding["taskId"])[0]}
+ require(all(row["relativePath"] in allowed for row in actual),
+ "C11 checkpoint capture includes a terminal, blocked or digest record")
+ if actual:
+ names = [row["relativePath"] for row in actual]
+ require(len(names) == len(set(names)), "C11 checkpoint has duplicated physical record owners")
+ require((journal.names("checkpoint", binding["taskId"])[0] in names)
+ == (journal.names("checkpoint", binding["taskId"])[1] in names),
+ "C11 original checkpoint/resume pair is incomplete")
+ for row in rows:
+ if not row["present"]: continue
+ raw = base64.b64decode(row["rawBase64"], validate=True)
+ value = journal.decode_json(raw)
+ require(value.get("task_id") == binding["taskId"], "C11 captured a foreign original task")
+ if row["relativePath"] == journal.names("checkpoint", binding["taskId"])[0]:
+ value = journal.checkpoint_value(raw, binding["taskId"], binding["bucket"])
+ require(value["admin"]["buckets"] == {binding["bucket"]: binding["bucketIncarnationId"]},
+ "C11 captured checkpoint belongs to another bucket generation")
+ elif row["relativePath"] == journal.names("checkpoint", binding["taskId"])[1]:
+ require(value.get("schema_version") == 7 and value.get("task_type") == "admin_erasure_set"
+ and value.get("set_disk_id") == "pool_0_set_0" and value.get("completed") is False,
+ "C11 captured companion is not a runnable Admin resume state")
+ else:
+ require(value.get("schema") == 3 and value.get("heal_type") == {
+ "type": "erasure_set", "buckets": [], "set_disk_id": "pool_0_set_0", "all_buckets": True},
+ "C11 captured root admission changed its original Admin erasure-set scope")
+ else:
+ committed = mrf.checkpoint(mrf.read_records(state, roots, owner, checked))
+ require((not committed.get("present")) or committed.get("present") is True
+ and all(item["bucket"] == binding["bucket"] and item["kind"] == 3
+ and item.get("lifecycle", {}).get("source_bucket_incarnation_id") == binding["bucketIncarnationId"]
+ and item["lifecycle"].get("state") == {"state": "active"}
+ and item["lifecycle"].get("last_operator_acceptance") is None for item in committed["intents"]),
+ "C11 MRF archive lacks committed active responsibility for the exact old incarnation")
+ digests = {item["intentDigest"] for item in committed["intents"]}
+ for row in rows:
+ if not row["present"] or not ("snapshot." in row["relativePath"] or row["relativePath"] in LEGACY_MRF_PATHS):
+ continue
+ raw = base64.b64decode(row["rawBase64"], validate=True)
+ intents = mrf.decode_journal(raw)
+ require(all(item["kind"] == 3 and item["intentDigest"] in digests for item in intents),
+ "C11 fallback or older snapshot contains an unbound original responsibility")
+ require(row["relativePath"] != LEGACY_MRF_PATHS[0] or not intents,
+ "C11 scoped durable intents cannot appear in the v1 compatibility mirror")
+ manifest = {**binding, "records": rows, "originalProductBytes": True, "replacementBytesAccepted": False}
+ require(len(fault.encode(manifest)) <= journal.LIMIT * 32, "C11 original archive exceeds the bounded export limit")
+ # Archive the entire local transaction before removing any runnable record.
+ for row in rows:
+ if not row["present"]: continue
+ with fault.directory(roots[row["disk"]]) as fd, fault.parent(fd, ["quarantine"]) as directory:
+ fault.new_file(directory, archive_name(row["relativePath"]), base64.b64decode(row["rawBase64"], validate=True))
+ with fault.directory(state) as fd:
+ fault.new_file(fd, "C11-replay.json", fault.encode(manifest))
+ require(journal.same_json(census(roots, binding, journal, fault), rows),
+ "C11 record changed after archiving; refusing partial replay detachment")
+ for row in rows:
+ if not row["present"]: continue
+ with fault.directory(roots[row["disk"]]) as fd, fault.parent(
+ fd, ["data", *row["relativePath"].split("/")[:-1]]) as directory:
+ require(fault.read_file(directory, row["relativePath"].split("/")[-1], journal.LIMIT * 4)
+ == base64.b64decode(row["rawBase64"], validate=True), "C11 record drifted immediately before detachment")
+ os.unlink(row["relativePath"].split("/")[-1], dir_fd=directory)
+ os.fsync(directory)
+ require(all(not row["present"] for row in census(roots, binding, journal, fault)),
+ "C11 runnable record remains after complete detachment")
+ with fault.directory(state) as fd:
+ fault.new_file(fd, "C11-replay-detached.json", fault.encode(binding))
+ return {**manifest, "detached": True, "checkpointCursorClaimed": False,
+ "backupRescueUsed": False, "canonicalCasePassClaimed": False}
+
+
+def archived(state, roots, owner, request, journal, fault):
+ binding = identity(owner, request, journal)
+ with fault.directory(state) as fd:
+ manifest = journal.decode_json(fault.read_file(fd, "C11-replay.json", journal.LIMIT * 32))
+ detached = journal.decode_json(fault.read_file(fd, "C11-replay-detached.json", 16384))
+ require(journal.same_json(detached, binding) and all(journal.same_json(manifest.get(key), value)
+ for key, value in binding.items()), "C11 archive changed its run/candidate/task/incarnation binding")
+ require(set(manifest) == {*binding, "records", "originalProductBytes", "replacementBytesAccepted"}
+ and manifest["originalProductBytes"] is True and manifest["replacementBytesAccepted"] is False,
+ "C11 archive contains unknown or unproved provenance")
+ expected = {(disk, relative) for disk in range(4) for relative in paths(binding, journal)}
+ rows = manifest.get("records")
+ require(isinstance(rows, list) and len(rows) == len(expected)
+ and {(row.get("disk"), row.get("relativePath")) for row in rows} == expected,
+ "C11 archive omitted or duplicated a physical record")
+ for row in rows:
+ require(type(row.get("present")) is bool and type(row.get("disk")) is int, "C11 archive record presence is unknown")
+ require(set(row) == {"disk", "relativePath", "present", *(
+ ("size", "sha256", "rawBase64") if row["present"] else ())}, "C11 archive record has unknown fields")
+ if not row["present"]: continue
+ with fault.directory(roots[row["disk"]]) as fd, fault.parent(fd, ["quarantine"]) as directory:
+ raw = fault.read_file(directory, archive_name(row["relativePath"]), journal.LIMIT * 4)
+ require(journal.entry(raw) == {key: row.get(key) for key in ("size", "sha256")}
+ and raw == base64.b64decode(row["rawBase64"], validate=True), "C11 immutable original archive changed")
+ return manifest
+
+
+def install(state, roots, owner, request, journal, fault):
+ manifest = archived(state, roots, owner, request, journal, fault)
+ observed = census(roots, manifest, journal, fault)
+ require(owner.get("incarnationPhase") == "transition", "C11 replay installation requires the transition phase")
+ empty_mirrors = [row for row in observed if row["present"] and manifest["family"] == "mrf"
+ and row["relativePath"] in LEGACY_MRF_PATHS and row["size"] == 0]
+ require(all(not row["present"] or row in empty_mirrors for row in observed),
+ "C11 replay would overwrite product progress or an existing terminal/responsibility")
+ # Healthy runtime flushes may publish empty fallback mirrors. They carry no
+ # responsibility; preserve the read receipt and never discard nonempty work.
+ for row in empty_mirrors:
+ with fault.directory(roots[row["disk"]]) as fd, fault.parent(
+ fd, ["data", *row["relativePath"].split("/")[:-1]]) as directory:
+ require(fault.read_file(directory, row["relativePath"].split("/")[-1], journal.LIMIT) == b"",
+ "C11 transition mirror gained product responsibility before installation")
+ os.unlink(row["relativePath"].split("/")[-1], dir_fd=directory)
+ os.fsync(directory)
+ for row in manifest["records"]:
+ if not row["present"]: continue
+ with fault.directory(roots[row["disk"]]) as fd, fault.parent(
+ fd, ["data", *row["relativePath"].split("/")[:-1]]) as directory:
+ fault.new_file(directory, row["relativePath"].split("/")[-1], base64.b64decode(row["rawBase64"], validate=True))
+ require(journal.same_json(census(roots, manifest, journal, fault), manifest["records"]),
+ "C11 replay installation differs from the original exact record set")
+ with fault.directory(state) as fd:
+ fault.new_file(fd, "C11-replay-installed.json", fault.encode({key: manifest[key] for key in
+ ("runId", "binarySha256", "bucket", "family", "taskId", "bucketIncarnationId")}))
+ return {**manifest, "installed": True, "originalArchiveRetained": True,
+ "discardedEmptyTransitionJournals": empty_mirrors,
+ "productRepairClaimed": False, "canonicalCasePassClaimed": False}
+
+
+def finalize(state, roots, owner, request, journal, fault):
+ manifest = archived(state, roots, owner, request, journal, fault)
+ with fault.directory(state) as fd:
+ installed = journal.decode_json(fault.read_file(fd, "C11-replay-installed.json", 16384))
+ require(all(journal.same_json(installed.get(key), manifest[key]) for key in installed)
+ and set(installed) == {"runId", "binarySha256", "bucket", "family", "taskId", "bucketIncarnationId"},
+ "C11 replay installation is incomplete; preserve every private namespace")
+ return {"originalArchive": manifest, "currentRecords": census(roots, manifest, journal, fault),
+ "originalEvidenceExported": True, "namespaceFinalizationAllowed": True,
+ "backupRescueUsed": False, "productRepairClaimed": False, "canonicalCasePassClaimed": False}
diff --git a/scripts/validation_heal/inspector.py b/scripts/validation_heal/inspector.py
new file mode 100644
index 00000000..a9c20e13
--- /dev/null
+++ b/scripts/validation_heal/inspector.py
@@ -0,0 +1,48 @@
+#!/usr/bin/env python3
+"""Verify a source-bound Linux Heal metadata decoder before distributing it."""
+
+import argparse
+import hashlib
+import json
+from pathlib import Path
+import re
+import stat
+
+
+def verify(directory: Path, candidate: dict) -> dict:
+ root = directory.resolve(strict=True)
+ paths = {name: root / name for name in ("heal-meta-inspect", "heal-meta-inspect.json", "inspector-Cargo.lock")}
+ for name, path in paths.items():
+ info = path.lstat()
+ if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1 or info.st_size > 256 * 1024**2:
+ raise ValueError(f"invalid inspector artifact file: {name}")
+ manifest = json.loads(paths["heal-meta-inspect.json"].read_text())
+ if (manifest.get("built") is not True or manifest.get("candidateCommit") != candidate["source_sha"]
+ or manifest.get("target") != "x86_64-unknown-linux-gnu"
+ or manifest.get("role") != "read-only decoder; not the RustFS service under test"):
+ raise ValueError("inspector candidate or build role differs")
+ helper = Path(__file__).with_name("tools") / "heal_meta_inspect.rs"
+ digests = {"helperSourceSha256": hashlib.sha256(helper.read_bytes()).hexdigest(),
+ "binarySha256": hashlib.sha256(paths["heal-meta-inspect"].read_bytes()).hexdigest(),
+ "inspectorLockSha256": hashlib.sha256(paths["inspector-Cargo.lock"].read_bytes()).hexdigest()}
+ if any(manifest.get(name) != value for name, value in digests.items()):
+ raise ValueError("inspector source, binary or lock digest differs")
+ if not re.fullmatch(r"[0-9a-f]{64}", manifest.get("candidateLockSha256", "")):
+ raise ValueError("inspector omitted the candidate lock identity")
+ with paths["heal-meta-inspect"].open("rb") as stream:
+ header = stream.read(20)
+ if header[:6] != b"\x7fELF\x02\x01" or header[18:20] != b"\x3e\x00":
+ raise ValueError("inspector is not Linux x86_64 ELF")
+ return manifest
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--directory", type=Path, required=True)
+ parser.add_argument("--candidate", type=Path, required=True)
+ args = parser.parse_args()
+ print(json.dumps(verify(args.directory, json.loads(args.candidate.read_text())), indent=2))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/validation_heal/interrupted_race.py b/scripts/validation_heal/interrupted_race.py
new file mode 100644
index 00000000..d92c1ce6
--- /dev/null
+++ b/scripts/validation_heal/interrupted_race.py
@@ -0,0 +1,212 @@
+"""Isolate only G06's interrupted H1 COPY after complete last-ACK authority.
+
+This is an environment recovery boundary, never a completed canonical result.
+Original request/ACK bytes and the stopped scene must still agree. No backup
+restoration, new SDK request or inferred acknowledgement is permitted.
+"""
+
+from collections import Counter
+import hashlib
+import json
+import re
+from types import SimpleNamespace
+
+try:
+ from . import race, race_retention
+ from .heal_probe import require
+ from .healthy import physical_content
+except ImportError:
+ import race
+ import race_retention
+ from heal_probe import require
+ from healthy import physical_content
+
+
+CASE = "heal.race-copy-and-metadata-update"
+
+
+class Replay:
+ """Consume every original SDK operation once, without invoking a client."""
+
+ def __init__(self, folder, run_id, bucket):
+ self.folder, self.run_id, self.sequence = folder, run_id, 0
+ self.probe = SimpleNamespace(binding={"bucketPrefix": bucket})
+ self.files = {}
+ require(folder.is_dir() and all(re.fullmatch(r"[0-9]{6}-(request|ack)\.json", p.name)
+ and p.is_file() and not p.is_symlink() for p in folder.iterdir()),
+ "interrupted COPY journal has an unknown member")
+
+ def call(self, operation, params, body=None):
+ self.sequence += 1
+ request_path = self.folder / f"{self.sequence:06d}-request.json"
+ ack_path = self.folder / f"{self.sequence:06d}-ack.json"
+ request_raw, ack_raw = request_path.read_bytes(), ack_path.read_bytes()
+ require(max(len(request_raw), len(ack_raw)) <= 1024**2, "interrupted COPY journal exceeds its bound")
+ request, ack = json.loads(request_raw), json.loads(ack_raw)
+ observation = ack.get("observation") or {}
+ require(request.get("schemaVersion") == ack.get("schemaVersion") == 2
+ and request.get("runId") == ack.get("runId") == self.run_id
+ and request.get("sequence") == ack.get("sequence") == self.sequence
+ and request.get("operation") == observation.get("operation") == operation
+ and request.get("params") == observation.get("params") == params
+ and ack.get("requestFile") == request_path.name
+ and ack.get("requestSha256") == hashlib.sha256(request_raw).hexdigest()
+ and ack.get("operationCheck") == "PASS"
+ and observation.get("acknowledged") is True and observation.get("resultUnknown") is False
+ and observation.get("clientCallInvoked") is True,
+ "interrupted COPY has an unresolved, changed or foreign SDK operation")
+ require(request.get("payload") == (None if body is None else {
+ "size": len(body), "sha256": hashlib.sha256(body).hexdigest()}),
+ "interrupted COPY deterministic seed body differs")
+ for path, raw in ((request_path, request_raw), (ack_path, ack_raw)):
+ self.files[self.folder.name + "/" + path.name] = hashlib.sha256(raw).hexdigest()
+ return observation["response"]
+
+ def finish(self, count):
+ require(self.sequence == count and len(list(self.folder.iterdir())) == 2 * count,
+ "interrupted COPY journal has an omitted or additional SDK operation")
+ return self.files
+
+
+def archived_plan(artifact, failed_run, candidate):
+ results = list(artifact.glob("*/run/*/cases/*.json"))
+ require(len(results) == 1 and results[0].name == CASE + ".json", "interrupted COPY requires one unmixed case")
+ raw = results[0].read_bytes()
+ result = json.loads(raw)
+ require(result.get("case_id") == CASE and result.get("verdict") == "RUNNING"
+ and result.get("cleanup_status") == "PENDING" and not result.get("provenance"),
+ "interrupted COPY original result must remain unfinished")
+ require(json.loads((results[0].parents[3] / "candidate.json").read_text()) == candidate,
+ "interrupted COPY candidate differs")
+ evidence = results[0].parents[1] / "evidence" / CASE
+ require(not (evidence / "healthy-summary.json").exists()
+ and {p.relative_to(evidence).as_posix() for p in evidence.glob("*/*") if p.is_dir()}
+ == {"H1/copy-source", "H1/copy-destination"},
+ "interrupted COPY cannot omit another unfinished branch")
+ previous = evidence / "H1/copy-source"
+ def one(folder, label):
+ paths = list(folder.glob("*-" + label + ".json"))
+ require(len(paths) == 1, "interrupted COPY lacks one original " + label)
+ return json.loads(paths[0].read_text())
+ prior = one(previous, "branch-result")
+ cleanup = one(previous, "cleanup")
+ require(prior.get("topology") == "H1" and prior.get("branch") == "copy-source"
+ and prior.get("verdict") in {"PASS", "NOT_OBSERVED", "FAIL"} and prior.get("cleanup") == "PASS"
+ and cleanup.get("node") == "vm000" and cleanup.get("cleanup") == "PASS"
+ and cleanup.get("unitStopped") is True and cleanup.get("rootsRemoved") is True
+ and one(previous, "recovery") == {"cleanup": "PASS", "errors": []},
+ "interrupted COPY prior shared namespace was not finalized")
+ folder = evidence / "H1/copy-destination"
+ require({p.name for p in folder.iterdir() if p.is_dir()} == {"seed-ACKs", "foreground-ACKs"}
+ and all(p.is_dir() or re.fullmatch(r"[0-9]{5}-.+\.json", p.name) for p in folder.iterdir()),
+ "interrupted COPY has an unknown operation")
+ files = sorted(folder.glob("*.json"))
+ labels = Counter(p.name[6:-5] for p in files)
+ expected = {"binding": 1, "inspector-build": 1, "source-contract": 1, "preflight": 1,
+ "reserved": 1, "inspector-upload": 1, "started": 3, "deployed-topology": 1,
+ "topology-proof": 1, "complete-baseline-S3": 1, "frozen-foreground-plan": 1,
+ "quiescent": 2, "complete-baseline-physical": 1, "baseline-streaming": 9,
+ "exact-missing-part": 257, "complete-fault-physical": 1, "fault-batch-proof": 1,
+ "race-io-cap-intent": 2, "race-io-process": 2, "race-io-cap-applied": 2,
+ "attribution-before-START": 1, "prepared-Admin-request": 1, "original-Admin-start": 1,
+ "target-missing-before-foreground": 1, "read-repair-overlap-proof": 1,
+ "public-call-overlap-proof": 1, "ACK-derived-final-manifest": 1,
+ "product-physical-before-GET-or-rescue": 1, "last-ACK-physical-proof": 1,
+ "final-streaming": 9, "obsolete-target-fault-retired": 1, "integrity-fleet-attestation": 1}
+ statuses = labels.pop("original-token-status", 0)
+ require(labels == Counter(expected) and 3 <= statuses <= 2000
+ and all(p.name.startswith(f"{i:05d}-") for i, p in enumerate(files, 1))
+ and files[-1].name.endswith("-started.json")
+ and files[-2].name.endswith("-obsolete-target-fault-retired.json"),
+ "interrupted COPY is outside its reviewed after-authority boundary")
+ binding = one(folder, "binding")
+ require(re.fullmatch(rf"gh-{failed_run}-[1-9][0-9]*-h1", binding.get("runId", ""))
+ and binding.get("candidateCommit") == candidate["source_sha"]
+ and binding.get("candidatePackageSha256") == candidate["package_sha256"],
+ "interrupted COPY run or candidate binding differs")
+ reservation = one(folder, "reserved")
+ require(reservation.get("node") == "vm000" and reservation.get("reserved") is True,
+ "interrupted COPY original owner is incomplete")
+ owner = reservation["owner"]
+ run_id, bucket = binding["runId"], binding["bucketPrefix"]
+ seed = Replay(folder / "seed-ACKs", run_id, bucket)
+ expected_seed = race.seed_fixture(seed, bucket, "copy-destination")
+ ack_files = seed.finish(258)
+ baseline = one(folder, "complete-baseline-S3")
+ race.verify_seed(expected_seed, baseline)
+ plan = race.operation_plan(bucket, "copy-destination", expected_seed)
+ for step in plan:
+ step["branch"] = "copy-destination"
+ require(one(folder, "frozen-foreground-plan") == plan, "interrupted COPY changed its frozen operation")
+ writer = Replay(folder / "foreground-ACKs", run_id, bucket)
+ responses = [writer.call(step["operation"], step["params"]) for step in plan]
+ ack_files.update(writer.finish(1))
+ final = race.final_inventory(baseline, "copy-destination", plan, responses)
+ require(final == one(folder, "ACK-derived-final-manifest"), "interrupted COPY final ACK authority differs")
+ before, after = (one(folder, label) for label in (
+ "complete-baseline-physical", "product-physical-before-GET-or-rescue"))
+ for scene in (before, after):
+ require(set(scene) == {"vm000"} and scene["vm000"].get("quiescent") is True
+ and type(scene["vm000"].get("pid")) is int and scene["vm000"]["pid"] == 0
+ and scene["vm000"].get("binarySha256") == binding["candidateBinarySha256"]
+ and scene["vm000"].get("hostname") == owner["hostname"]
+ and scene["vm000"].get("unit") == owner["unit"],
+ "interrupted COPY scene is not the stopped exact owner")
+ mapped = race.map_fixture(before, bucket, expected_seed)
+ survivors, proof = race.physical_transition(before, after, bucket, mapped, final, "copy-destination")
+ require(proof == one(folder, "last-ACK-physical-proof"), "interrupted COPY physical authority differs")
+ # Reuse the complete streaming oracle against archived receipts, without SSH.
+ class ArchivedParts:
+ nodes = ["vm000"]
+ def __init__(self):
+ self.proofs = iter(json.loads(p.read_text()) for p in sorted(folder.glob("*-final-streaming.json")))
+ def remote(self, node, action, *, parts):
+ observed = next(self.proofs)
+ require(node == observed.get("node") and action == "verify-parts"
+ and [{k: p[k] for k in ("disk", "relativePath", "versionId", "partNumber")}
+ for p in observed["parts"]] == parts, "interrupted COPY streaming batch differs")
+ return observed
+ def record(self, label, value):
+ return value
+ stream = ArchivedParts()
+ race.verify_part_manifest(stream, list(survivors.values()), "final-streaming", count=257)
+ require(next(stream.proofs, None) is None, "interrupted COPY omitted a streaming batch")
+ retired = one(folder, "obsolete-target-fault-retired")
+ require(retired.get("oldBytesRestored") is False and retired.get("backupRetainedUntilCleanup") is True
+ and json.loads(files[-1].read_text()).get("vm000", {}).get("unit") == owner["unit"]
+ and json.loads(files[-1].read_text()).get("vm000", {}).get("started") is True,
+ "interrupted COPY retirement or final start differs")
+ isolation = {"kind": "unresolved-g06-race", "runId": run_id, "nodes": ["vm000"], "bucket": bucket,
+ "owners": {"vm000": owner}, "candidateCommit": candidate["source_sha"],
+ "harnessCommit": binding["harnessCommit"], "binarySha256": binding["candidateBinarySha256"],
+ "businessSceneSha256": {"vm000": race_retention.digest(physical_content(after)["vm000"])},
+ "ackFilesSha256": race_retention.digest(ack_files)}
+ race_retention.validate(isolation)
+ return [{"kind": "interrupted-g06-after-authority", "isolation": isolation,
+ "runId": run_id, "nodes": ["vm000"], "originalCaseSha256": hashlib.sha256(raw).hexdigest()}]
+
+
+def isolate(fleet, plan):
+ isolation = plan["isolation"]
+ race_retention.validate(isolation)
+ require(plan["kind"] == "interrupted-g06-after-authority" and fleet.run_id == isolation["runId"]
+ and list(fleet.nodes) == isolation["nodes"], "interrupted COPY recovery fleet differs")
+ states = [fleet.record("interrupted-COPY-namespace", {"node": node,
+ **fleet.remote(node, "namespace-state")}) for node in fleet.nodes]
+ for state in states:
+ if state.get("retained", {}).get("state") or any(state.get("retained", {}).get("roots", [])):
+ continue # Existing durable isolation intents are revalidated by isolate().
+ owner_sha = race_retention.digest(isolation["owners"][state["node"]])
+ entries = [state["state"], *state["roots"]]
+ require(len(entries) == 5 and all(entry.get("exists") is True
+ and entry.get("ownerEvidence", {}).get("uid") == 0
+ and entry.get("ownerEvidence", {}).get("canonicalSha256") == owner_sha for entry in entries),
+ "interrupted COPY original owner drifted; no service stopped")
+ for state in states:
+ if not (state.get("retained", {}).get("state") or any(state.get("retained", {}).get("roots", []))):
+ stopped = fleet.record("interrupted-COPY-stop", {"node": state["node"],
+ **fleet.remote(state["node"], "stop")})
+ require(stopped.get("unit") == isolation["owners"][state["node"]]["unit"]
+ and stopped.get("quiescent") is True, "interrupted COPY exact service did not stop")
+ return {**race_retention.isolate(fleet, isolation), "originalCaseSha256": plan["originalCaseSha256"],
+ "interruptedAfterLastACKAuthority": True, "canonicalCaseComplete": False}
diff --git a/scripts/validation_heal/interrupted_recovery.py b/scripts/validation_heal/interrupted_recovery.py
new file mode 100644
index 00000000..58b958f1
--- /dev/null
+++ b/scripts/validation_heal/interrupted_recovery.py
@@ -0,0 +1,243 @@
+"""Restore and retain only the bounded F07 cancellation before Admin admission."""
+
+from collections import Counter
+import hashlib
+import json
+import re
+
+try:
+ from .healthy import physical_content
+ from .heal_probe import require
+except ImportError:
+ from healthy import physical_content
+ from heal_probe import require
+
+
+CASE = "heal.checkpoint-and-mrf-corruption"
+BRANCHES = ("checkpoint-truncate", "future-schema", "mrf-checksum", "stale-generation", "journal-write-failure")
+
+
+def digest(value):
+ return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
+
+
+def checkpoint_plan(artifact, failed_run, candidate):
+ results = list(artifact.glob("*/run/*/cases/*.json"))
+ require(len(results) == 1 and results[0].name == CASE + ".json", "interrupted recovery requires one unmixed case")
+ raw = results[0].read_bytes()
+ result = json.loads(raw)
+ require(result.get("case_id") == CASE and result.get("verdict") == "RUNNING"
+ and result.get("cleanup_status") == "PENDING" and not result.get("provenance"),
+ "interrupted recovery requires the unchanged unfinished result")
+ root = results[0].parents[3]
+ require(json.loads((root / "candidate.json").read_text()) == candidate, "interrupted candidate differs")
+ evidence = results[0].parents[1] / "evidence" / CASE
+ require(not (evidence / "healthy-summary.json").exists(), "interrupted case already has an aggregate result")
+ finished = {(fixture, topology, branch) for fixture in ("DB", "DV") for topology in ("H1", "H2")
+ for branch in BRANCHES} - {("DV", "H2", "stale-generation"), ("DV", "H2", "journal-write-failure")}
+ observed = list(evidence.glob("*/*/*/*branch-result.json"))
+ require(len(observed) == len(finished) == 18
+ and {tuple(path.parent.relative_to(evidence).parts) for path in observed} == finished,
+ "interrupted recovery cannot omit another unfinished branch")
+ for path in observed:
+ row = json.loads(path.read_text())
+ fixture, topology, branch = path.parent.relative_to(evidence).parts
+ require(row.get("fixture") == fixture and row.get("topology") == topology and row.get("branch") == branch
+ and row.get("verdict") in {"PASS", "FAIL"} and row.get("cleanup") == "PASS",
+ "prior interrupted branch was not finalized")
+ cleanup = [json.loads(p.read_text()) for p in path.parent.glob("*-cleanup.json")]
+ nodes = {"vm000"} if topology == "H1" else {f"vm00{i}" for i in range(4)}
+ require(len(cleanup) == len(nodes) and {r.get("node") for r in cleanup} == nodes
+ and all(r.get("cleanup") == "PASS" and r.get("unitStopped") is True and r.get("rootsRemoved") is True
+ for r in cleanup), "prior branch lacks its full stopped and removed fleet")
+ recovery = [p for p in path.parent.glob("*-recovery.json") if re.fullmatch(r"\d{5}-recovery.json", p.name)]
+ require(len(recovery) == 1 and json.loads(recovery[0].read_text()) == {"cleanup": "PASS", "errors": []},
+ "prior branch has an unresolved recovery")
+ expected_folders = finished | {("DV", "H2", "stale-generation")}
+ require({tuple(path.relative_to(evidence).parts) for path in evidence.glob("*/*/*") if path.is_dir()} == expected_folders,
+ "interrupted evidence contains another unfinished branch")
+ folder = evidence / "DV/H2/stale-generation"
+ require(not (evidence / "DV/H2/journal-write-failure").exists(), "later interrupted branch already started")
+ require(all(path.name == "fixture" or re.fullmatch(r"[0-9]{5}-.+\.json", path.name)
+ for path in folder.iterdir()), "interrupted folder contains an unknown operation")
+ require(all(re.fullmatch(r"[0-9]{6}-(request|ack)\.json", path.name)
+ for path in (folder / "fixture").iterdir()), "interrupted fixture contains an unknown operation")
+ files = sorted(folder.glob("*.json"))
+ require(len(files) == 97 and all(path.name.startswith(f"{i:05d}-") for i, path in enumerate(files, 1)),
+ "interrupted recovery requires its exact pre-admission boundary")
+ labels = Counter(path.name[6:-5] for path in files)
+ require(labels == Counter({"binding": 1, "candidate-bound-inspector": 1, "candidate-terminal-options-contract": 1,
+ "preflight": 1, "integrity-fleet-attestation": 1, "reserved": 4, "inspector-upload": 4, "started": 2,
+ "deployed-topology": 1, "topology-proof": 1, "ACK-bound-complete-S3": 1, "complete-ACK-manifest": 1,
+ "task-exit-background": 4, "quiescent": 1, "healthy-physical-baseline": 1,
+ "baseline-full-cohort-bitrot": 64, "missing-original-shard": 4, "actual-missing-shard": 4})
+ and files[-1].name == "00097-started.json", "interrupted scene has a task, durable fault or unknown action")
+ def one(label):
+ return json.loads(next(p for p in files if p.name[6:-5] == label).read_text())
+ binding = one("binding")
+ run_id, bucket = binding.get("runId", ""), binding.get("bucketPrefix", "")
+ require(re.fullmatch(rf"gh-{failed_run}-[1-9][0-9]*-h2", run_id)
+ and re.fullmatch(r"rv-heal-[0-9a-f]{16}", bucket)
+ and binding.get("candidateCommit") == candidate["source_sha"]
+ and binding.get("candidatePackageSha256") == candidate["package_sha256"]
+ and re.fullmatch(r"[0-9a-f]{64}", binding.get("candidateBinarySha256", ""))
+ and re.fullmatch(r"[0-9a-f]{40}", binding.get("harnessCommit", "")), "interrupted binding differs")
+ nodes = [f"vm00{i}" for i in range(4)]
+ owners = {}
+ for path in files:
+ if path.name[6:-5] != "reserved": continue
+ receipt = json.loads(path.read_text()); owner = receipt.get("owner") or {}
+ node = receipt.get("node")
+ require(receipt.get("reserved") is True and node in nodes and node not in owners
+ and owner.get("runId") == run_id and owner.get("bucket") == bucket
+ and owner.get("binarySha256") == binding["candidateBinarySha256"]
+ and owner.get("unit") == "rustfs-validation-heal-" + run_id + ".service"
+ and owner.get("runtimeProfile") == "root-restart" and len(owner.get("mounts", [])) == 4,
+ "interrupted owner receipt differs")
+ owners[node] = owner
+ require(set(owners) == set(nodes), "interrupted fleet owner is incomplete")
+ requests, acks = sorted((folder / "fixture").glob("*-request.json")), sorted((folder / "fixture").glob("*-ack.json"))
+ require(len(requests) == len(acks) == 282, "interrupted fixture has unresolved SDK operations")
+ operations = Counter()
+ for seq, (request_path, ack_path) in enumerate(zip(requests, acks, strict=True), 1):
+ request, ack = json.loads(request_path.read_text()), json.loads(ack_path.read_text())
+ observation = ack.get("observation") or {}
+ operation, params = request.get("operation"), request.get("params") or {}
+ require(type(request.get("sequence")) is int and type(ack.get("sequence")) is int
+ and request["sequence"] == ack["sequence"] == seq
+ and request.get("runId") == ack.get("runId") == run_id
+ and ack.get("requestFile") == request_path.name
+ and ack.get("requestSha256") == hashlib.sha256(request_path.read_bytes()).hexdigest()
+ and ack.get("operationCheck") == "PASS" and observation.get("operation") == operation
+ and observation.get("params") == params and params.get("Bucket") == bucket
+ and observation.get("acknowledged") is True and observation.get("clientCallInvoked") is True
+ and observation.get("resultUnknown") is False
+ and operation in {"create_bucket", "put_bucket_versioning", "put_object", "delete_object"},
+ "interrupted SDK operation is unacknowledged, changed or outside its fixture")
+ if operation in {"put_object", "delete_object"}:
+ require(re.fullmatch(r"objects/[0-9]{4}\.bin", params.get("Key", "")) and "VersionId" not in params,
+ "interrupted SDK object operation differs")
+ operations[operation] += 1
+ require(operations == Counter(create_bucket=1, put_bucket_versioning=1, put_object=240, delete_object=40),
+ "interrupted fixture operation census differs")
+ before = one("healthy-physical-baseline")
+ def indexed(scene):
+ require(set(scene) == set(nodes), "interrupted physical fleet differs")
+ out = {}
+ for node, value in scene.items():
+ require(value.get("quiescent") is True and type(value.get("pid")) is int and value["pid"] == 0
+ and value.get("binarySha256") == binding["candidateBinarySha256"]
+ and len(value.get("formats", [])) == 4, "interrupted scene is not the stopped exact candidate")
+ rows = value["files"]
+ out[node] = {(entry["disk"], entry["path"]): entry for entry in rows}
+ require(len(out[node]) == len(rows), "interrupted physical scene has duplicate files")
+ return out
+ previous = before; left = indexed(previous); faults = []
+ for index, receipt_path in enumerate(sorted(folder.glob("*-missing-original-shard.json"))):
+ receipt = json.loads(receipt_path.read_text())
+ number = int(receipt_path.name[:5])
+ after_path = folder / f"{number+1:05d}-actual-missing-shard.json"
+ require(after_path in files, "interrupted injection has no immediate physical snapshot")
+ after = json.loads(after_path.read_text()); right = indexed(after)
+ removed = [(node, *key) for node in nodes for key in set(left[node]) - set(right[node])]
+ require(len(removed) == 1, "interrupted injection is not one exact missing part")
+ node, disk, relative = removed[0]; entry = left[node][(disk, relative)]
+ require(type(disk) is int and 0 <= disk < 4
+ and re.fullmatch(re.escape(bucket) + r"/objects/[0-9]{4}\.bin/[0-9a-f-]{36}/part\.1", relative)
+ and receipt.get("schemaVersion") == 1 and receipt.get("runId") == run_id
+ and receipt.get("faultId") == f"F07-shard-{index}" and receipt.get("relativePath") == relative
+ and receipt.get("kind") == "missing" and receipt.get("fault") is None and receipt.get("offset") is None
+ and receipt.get("before") == {key: entry[key] for key in ("size", "sha256")}
+ and receipt.get("backup") == f"F07-shard-{index}.original"
+ and receipt.get("backend") == "logical-file-fault" and receipt.get("requiresQuiescentService") is True,
+ "interrupted missing-part receipt differs")
+ del left[node][(disk, relative)]
+ require(left == right and all(previous[n]["formats"] == after[n]["formats"] for n in nodes),
+ "interrupted injection changed other files or disk formats")
+ faults.append({"node": node, "disk": disk, "faultId": receipt["faultId"], "receipt": receipt})
+ previous, left = after, right
+ require(len(faults) == 4, "interrupted fault census differs")
+ started = json.loads(files[-1].read_text())
+ require(set(started) == set(nodes) and all(value == {"unit": owners[n]["unit"], "started": True}
+ for n, value in started.items()), "interrupted final service start differs")
+ return [{"kind": "interrupted-checkpoint-before-admission", "runId": run_id, "nodes": nodes,
+ "bucket": bucket, "binarySha256": binding["candidateBinarySha256"], "owners": owners,
+ "before": before, "failure": previous, "faults": faults,
+ "originalCaseSha256": hashlib.sha256(raw).hexdigest(), "originalHarnessCommit": binding["harnessCommit"]}]
+
+
+def restore_and_isolate(fleet, plan, states, scene_matches):
+ """Preserve all backups and bytes, including when recovery is interrupted."""
+ require(len(states) == len(plan["nodes"]) and len(states) in {1, 4}
+ and {item["node"] for item in states} == set(plan["nodes"]), "interrupted current fleet differs")
+ active = []
+ for item in states:
+ retained = item.get("retained") or {}
+ if retained.get("state") or any(retained.get("roots", [])):
+ continue
+ require(item["state"]["exists"] and len(item["roots"]) == 4 and all(root["exists"] for root in item["roots"]),
+ "interrupted fixture lost an original root; retain remaining scene")
+ active.append(item["node"])
+ if plan.get("kind") == "interrupted-j02-opaque":
+ for node in active:
+ params = dict(expectedOwner=plan["owners"][node], before=plan["before"][node],
+ faults=[{k: v for k, v in f.items() if k != "node"} for f in plan["faults"] if f["node"] == node])
+ audit = fleet.record("interrupted-owner-markers-audit", {"node": node,
+ **fleet.remote(node, "owner-markers-audit", **params)})
+ require(audit.get("inspectionOnly") is True and audit.get("mutated") is False
+ and audit.get("canonicalPassClaimed") is False and type(audit.get("markerRecoveryRequired")) is bool,
+ "interrupted owner-marker audit is incomplete")
+ if audit.get("markerRecoveryRequired") is True:
+ proof = fleet.record("interrupted-owner-markers-recovery", {"node": node,
+ **fleet.remote(node, "owner-markers-recover", **params,
+ expectedAudit={k: v for k, v in audit.items() if k != "node"})})
+ require(proof.get("ownerMetadataRestored") is True and proof.get("businessBytesChanged") is False
+ and proof.get("canonicalPassClaimed") is False, "interrupted owner-marker recovery differs")
+ for node in active:
+ fleet.record("interrupted-recovery-stop", {"node": node, **fleet.remote(node, "stop")})
+ current = {node: fleet.remote(node, "snapshot", quiescent=True,
+ **({"metadataFaults": plan["metadataFaults"][node]} if plan.get("metadataFaults") else {})) for node in active}
+ fleet.record("interrupted-current-physical", current)
+ if active:
+ subset = {**plan, "before": {n: plan["before"][n] for n in active},
+ "failure": {n: plan["failure"][n] for n in active}}
+ require(all(value.get("quiescent") is True and type(value.get("pid")) is int and value["pid"] == 0
+ and value.get("binarySha256") == plan["binarySha256"] for value in current.values())
+ and scene_matches(current, subset), "interrupted current scene drifted; no backup restoration performed")
+ for fault in plan["faults"]:
+ if fault["node"] in active:
+ proof = fleet.record("interrupted-original-shard-rescue", {"node": fault["node"],
+ **fleet.remote(fault["node"], "recover-fault", disk=fault["disk"], faultId=fault["faultId"],
+ expectedReceiptSha256=digest({key: value for key, value in fault["receipt"].items()
+ if key != "witnessedAt"}))})
+ require(proof.get("runId") == plan["runId"] and proof.get("faultId") == fault["faultId"]
+ and proof.get("physicalRepairPassAllowed") is False, "interrupted rescue receipt differs")
+ restored = {node: fleet.remote(node, "snapshot", quiescent=True) for node in active}
+ fleet.record("interrupted-original-business-restored", restored)
+ require(physical_content(restored) == {n: physical_content(plan["before"])[n] for n in active},
+ "interrupted fixture did not restore its full original business scene")
+ audits = {}
+ def params(node):
+ return {"expectedOwner": plan["owners"][node],
+ "expectedOriginalSceneSha256": digest(physical_content(plan["before"])[node])}
+ for node in plan["nodes"]:
+ audits[node] = fleet.record("interrupted-archive-preflight", {"node": node,
+ **fleet.remote(node, "retained-scene-audit", **params(node))})
+ require(audits[node].get("inspectionOnly") is True and audits[node].get("mutated") is False
+ and audits[node].get("baseline", {}).get("originalSceneSha256") == params(node)["expectedOriginalSceneSha256"],
+ "interrupted restored archive preflight differs")
+ for node in plan["nodes"]:
+ proof = fleet.record("interrupted-archive-isolation", {"node": node,
+ **fleet.remote(node, "retained-scene-isolate", **params(node), expectedBaseline=audits[node]["baseline"])})
+ require(proof.get("activeNamespaceIsolated") is True and proof.get("canonicalPassClaimed") is False
+ and proof.get("baseline") == audits[node]["baseline"], "interrupted archive isolation differs")
+ for node in plan["nodes"]:
+ proof = fleet.record("interrupted-archive-post-audit", {"node": node,
+ **fleet.remote(node, "retained-scene-audit", **params(node))})
+ require(proof.get("activeNamespaceIsolated") is True and proof.get("inspectionOnly") is True
+ and proof.get("mutated") is False and proof.get("baseline") == audits[node]["baseline"],
+ "interrupted archive changed after isolation")
+ return {"runId": plan["runId"], "cleanup": "ISOLATED", "activeNamespaceIsolated": True,
+ "originalBusinessFixtureRestored": True, "originalSceneRestored": False,
+ "canonicalPassClaimed": False, "originalVerdictRewritten": False,
+ "originalCaseSha256": plan["originalCaseSha256"]}
diff --git a/scripts/validation_heal/journal_fault.py b/scripts/validation_heal/journal_fault.py
new file mode 100644
index 00000000..46bf2a18
--- /dev/null
+++ b/scripts/validation_heal/journal_fault.py
@@ -0,0 +1,349 @@
+"""Bounded F07 faults on exact owned durable records; never search metadata."""
+
+import base64
+import hashlib
+import json
+import os
+import re
+import struct
+import uuid
+
+
+LIMIT = 4 * 1024**2
+
+
+def require(value, message):
+ if not value: raise ValueError(message)
+
+
+def task_id(value):
+ require(isinstance(value, str) and str(uuid.UUID(value)) == value and uuid.UUID(value).int != 0,
+ "journal task must be a canonical nonzero UUID")
+ return value
+
+
+def names(family, token=None):
+ if family == "mrf":
+ require(token is None, "MRF census cannot invent a task-file binding")
+ return [f".rustfs.sys/.heal-mrf-{kind}.{slot}.bin" for slot in (0, 1) for kind in ("snapshot", "commit", "lifecycle")]
+ task_id(token)
+ if family == "root":
+ return [f".rustfs.sys/{prefix}{token}.json" for prefix in ("root-heal-", "terminal-root-heal-", "heal-terminal-report-")]
+ require(family == "checkpoint", "unknown durable-record family")
+ return [f".rustfs.sys/buckets/{token}_{suffix}" for suffix in
+ ("ahm_checkpoint.json", "ahm_resume_state.json", "ahm_checkpoint.blocked", "ahm_checkpoint.sha256")]
+
+
+def relative(request):
+ path = request.get("relativePath")
+ require(path in names(request.get("family"), request.get("taskId")), "journal target is outside the exact record allowlist")
+ return path.split("/")
+
+
+def entry(raw):
+ return {"size": len(raw), "sha256": hashlib.sha256(raw).hexdigest()}
+
+
+def same_json(left, right):
+ # JSON scope identities distinguish booleans, integers and floating-point values.
+ def canonical(value):
+ return json.dumps(value, sort_keys=True, ensure_ascii=False, separators=(",", ":"), allow_nan=False)
+ return canonical(left) == canonical(right)
+
+
+def read(root_fd, path, fault):
+ with fault.parent(root_fd, ["data", *path[:-1]]) as directory:
+ return fault.read_file(directory, path[-1], LIMIT * (4 if "lifecycle" in path[-1] else 1))
+
+
+def census(roots, request, fault, *, quiescent=True):
+ paths = names(request.get("family"), request.get("taskId"))
+ rows = []
+ for disk, root in enumerate(roots):
+ with fault.directory(root) as fd:
+ for path in paths:
+ row = {"disk": disk, "relativePath": path, "present": False}
+ try: raw = read(fd, path.split("/"), fault)
+ except FileNotFoundError: rows.append(row); continue
+ row.update(present=True, **entry(raw), rawBase64=base64.b64encode(raw).decode())
+ rows.append(row)
+ return {"readOnly": True, "quiescent": quiescent, "family": request["family"],
+ "taskId": request.get("taskId"), "records": rows, "directoryEnumerationUsed": False}
+
+
+def stable_census(roots, request, fault):
+ try:
+ first = census(roots, request, fault, quiescent=False)
+ second = census(roots, request, fault, quiescent=False)
+ except ValueError as exc:
+ if str(exc) != "file changed while reading": raise
+ raise ValueError("NOT_OBSERVED: exact durable record changed during a bounded read") from exc
+ require(first == second, "NOT_OBSERVED: exact durable records changed between the two bounded reads")
+ return {**second, "stable": True, "independentReads": 2}
+
+
+def decode_json(raw):
+ def unique_pairs(pairs):
+ result = {}
+ for key, value in pairs:
+ require(key not in result, "durable record contains duplicate JSON fields")
+ result[key] = value
+ return result
+ def invalid_constant(value):
+ raise ValueError("durable record contains a non-finite JSON value")
+ value = json.loads(raw, object_pairs_hook=unique_pairs, parse_constant=invalid_constant)
+ require(isinstance(value, dict), "durable record is not a JSON object")
+ return value
+
+
+def checkpoint_digest(value, *, sort_keys):
+ unsigned = dict(value)
+ unsigned["integrity_digest"] = None
+ for field in ("processed_objects", "failed_objects", "skipped_objects"):
+ require(isinstance(unsigned.get(field), list) and all(isinstance(item, str) for item in unsigned[field])
+ and len(set(unsigned[field])) == len(unsigned[field]),
+ "checkpoint ledger has an unknown canonical representation")
+ unsigned[field] = sorted(unsigned[field])
+ # serde_json::Value sorts keys by default; the full server's unified
+ # preserve_order feature instead retains the serialized struct/map order.
+ # Both builds sort the three ledger arrays before hashing UTF-8 JSON.
+ return base64.b64encode(hashlib.sha256(json.dumps(unsigned, sort_keys=sort_keys,
+ ensure_ascii=False, separators=(",", ":"), allow_nan=False).encode()).digest()).decode()
+
+
+def checkpoint_value(raw, token, bucket):
+ value = decode_json(raw)
+ require(value.get("task_id") == task_id(token) and type(value.get("schema_version")) is int and value["schema_version"] == 8,
+ "checkpoint does not belong to the exact original Admin task")
+ admin = value.get("admin") or {}
+ buckets = admin.get("buckets")
+ require(admin.get("completed") is False and isinstance(buckets, dict) and set(buckets) == {bucket}
+ and isinstance(buckets[bucket], str) and str(uuid.UUID(buckets[bucket])) == buckets[bucket]
+ and uuid.UUID(buckets[bucket]).int != 0, "checkpoint has no original owned bucket incarnation")
+ digests = {checkpoint_digest(value, sort_keys=sorted_keys) for sorted_keys in (False, True)}
+ require(value.get("integrity_digest") in digests, "original checkpoint has no valid candidate integrity digest")
+ return value
+
+
+def owned_mrf_intents(intents, bucket):
+ """Bind owned business work while preserving its known internal companions."""
+ owned = [item for item in intents if item.get("bucket") == bucket]
+ system_keys = {"buckets/.rustfs.sys/.metadata.bin", "buckets/.rustfs.sys/.bucket-incarnation"}
+ require(owned and all(item.get("kind") in {3, 4} and item.get("poolIndex") == 0
+ and item.get("setIndex") == 0 for item in owned)
+ and all(item in owned or item.get("bucket") == ".rustfs.sys" and item.get("kind") == 3
+ and item.get("versionId") == "null" and item.get("poolIndex") == 0 and item.get("setIndex") == 0
+ and (item.get("key") in system_keys or re.fullmatch(r"scanner/durable-dirty-producer-replay/[0-9a-f]{64}\.json",
+ item.get("key", "")) is not None) for item in intents),
+ "MRF payload includes a foreign or unregistered responsibility")
+ return owned
+
+
+def active_mrf_identity(committed, bucket):
+ """Bind one original committed payload to its active bucket generation."""
+ business = owned_mrf_intents(committed.get("intents", []), bucket)
+ require(committed.get("present") is True
+ and all(item.get("lifecycle", {}).get("state") == {"state": "active"}
+ and item["lifecycle"].get("source_bucket_incarnation_id")
+ and item["lifecycle"].get("last_operator_acceptance") is None for item in business),
+ "MRF fault lacks an actual committed active owned responsibility on this exact slot")
+ incarnations = {item["lifecycle"]["source_bucket_incarnation_id"] for item in business}
+ require(len(incarnations) == 1, "MRF fault mixed bucket incarnations")
+ incarnation = next(iter(incarnations))
+ task_id(incarnation)
+ return {"owner": committed["owner"], "sequence": committed["sequence"],
+ "payloadSha256": committed["payloadSha256"], "bucketIncarnationId": incarnation}
+
+
+def corrupt_owned_mrf_crc(raw, intents, bucket):
+ owned_mrf_intents(intents, bucket)
+ cursor, selected = 0, None
+ for intent in intents:
+ start = cursor
+ _, layout, kind, _, _, has_version = struct.unpack_from("= counters["failed"],
+ "stale checkpoint counters do not account for original durable dispositions")
+ require(old["checkpoint_time"] <= value["checkpoint_time"]
+ and (old["current_bucket_index"], old["current_object_index"]) <=
+ (value["current_bucket_index"], value["current_object_index"]),
+ "earlier checkpoint has a future or incomparable scan position")
+ return earlier
+ elif family == "root":
+ require(path[-1] == "root-heal-" + task_id(request["taskId"]) + ".json" and kind == "truncate",
+ "only the exact runnable root admission may be truncated")
+ value = json.loads(raw)
+ require(value.get("task_id") == request["taskId"] and value.get("schema") == 3
+ and value.get("heal_type") == {"type": "bucket", "bucket": owner["bucket"]}
+ and isinstance(value.get("bucket_incarnation_id"), str), "root admission does not belong to this owned bucket")
+ task_id(value["bucket_incarnation_id"])
+ else:
+ require(family == "mrf" and kind == "checksum" and re.fullmatch(r"\.heal-mrf-snapshot\.[01]\.bin", path[-1]),
+ "MRF mutation is restricted to the exact actual payload checksum")
+ intents = mrf.decode_journal(raw)
+ return corrupt_owned_mrf_crc(raw, intents, owner["bucket"])
+ if kind == "truncate":
+ require(len(raw) > 8, "cannot truncate an empty/unknown record")
+ return raw[:len(raw) // 2]
+ # Preserve the candidate's digest representation so an unknown schema is
+ # independently testable without also introducing checksum corruption.
+ sort_keys = value["integrity_digest"] != checkpoint_digest(value, sort_keys=False)
+ value["schema_version"] = 9
+ value["integrity_digest"] = checkpoint_digest(value, sort_keys=sort_keys)
+ return json.dumps(value, sort_keys=sort_keys, ensure_ascii=False, separators=(",", ":"), allow_nan=False).encode()
+
+
+def binding(root_fd, request, owner, fault):
+ require(owner.get("runtimeProfile") in {"root-restart", "mrf-crash", "journal-bounded"}, "journal mutation requires an owned recovery runtime")
+ require(owner["runId"] == request["runId"] and type(request.get("disk")) is int and 0 <= request["disk"] < 4,
+ "journal run or disk identity differs")
+ identifier = request.get("faultId")
+ require(isinstance(identifier, str) and re.fullmatch(r"F07-[a-z0-9-]{1,48}", identifier), "journal fault identity is invalid")
+ path = relative(request)
+ require(owner.get("runtimeProfile") != "journal-bounded" or request["family"] == "checkpoint",
+ "bounded journal mutation cannot target root admissions or MRF payloads")
+ require(request["family"] == "checkpoint" and path[-1] == task_id(request["taskId"]) + "_ahm_checkpoint.json"
+ or request["family"] == "root" and path[-1] == "root-heal-" + task_id(request["taskId"]) + ".json"
+ or request["family"] == "mrf" and re.fullmatch(r"\.heal-mrf-snapshot\.[01]\.bin", path[-1]),
+ "journal mutation cannot replace a terminal, resume state, digest, blocked fence or commit record")
+ return {"schemaVersion": 1, "runId": owner["runId"], "disk": request["disk"], "family": request["family"],
+ "taskId": request.get("taskId"), "relativePath": "/".join(path), "faultId": identifier,
+ "binarySha256": owner["binarySha256"], "device": os.fstat(root_fd).st_dev,
+ "mountId": fault.mount_id(root_fd), "productRepairClaimed": False}
+
+
+def capture(root_fd, request, owner, fault):
+ require(request.get("family") == "checkpoint", "only an actual Admin checkpoint has an earlier progress capture")
+ identity = binding(root_fd, request, owner, fault)
+ raw = read(root_fd, relative(request), fault)
+ checkpoint_value(raw, request["taskId"], owner["bucket"])
+ require(same_json(request.get("expectedBefore"), entry(raw)), "NOT_OBSERVED: checkpoint changed before its earlier capture")
+ proof = {**identity, "captured": entry(raw), "originalProductBytes": True, "productMetadataMutated": False}
+ with fault.parent(root_fd, ["quarantine"]) as backup:
+ # Capture identity and bytes are immutable and distinct from fault backups.
+ fault.new_file(backup, identity["faultId"] + ".early.intent.json", fault.encode(proof))
+ fault.new_file(backup, identity["faultId"] + ".early.checkpoint", raw)
+ return proof
+
+
+def captured(root_fd, request, owner, fault):
+ identity = binding(root_fd, request, owner, fault)
+ identifier = request.get("captureId")
+ require(isinstance(identifier, str) and re.fullmatch(r"F07-[a-z0-9-]{1,48}", identifier)
+ and identifier != identity["faultId"] and request["family"] == "checkpoint",
+ "stale-generation capture has no separate original snapshot identity")
+ with fault.parent(root_fd, ["quarantine"]) as backup:
+ proof = decode_json(fault.read_file(backup, identifier + ".early.intent.json", 16384))
+ require(all(same_json(proof.get(name), value) for name, value in {**identity, "faultId": identifier}.items())
+ and proof.get("originalProductBytes") is True and proof.get("productMetadataMutated") is False,
+ "earlier checkpoint capture changed task, run, candidate, physical owner or record identity")
+ raw = fault.read_file(backup, identifier + ".early.checkpoint", LIMIT)
+ require(same_json(entry(raw), proof.get("captured")), "captured earlier product checkpoint bytes changed")
+ checkpoint_value(raw, request["taskId"], owner["bucket"])
+ return raw
+
+
+def replace(root_fd, path, raw, identifier, fault, *, rescue=False):
+ with fault.parent(root_fd, ["data", *path[:-1]]) as directory:
+ temporary = ".F07-" + identifier + (".rescue" if rescue else ".inject")
+ try: fault.new_file(directory, temporary, raw)
+ except FileExistsError:
+ require(fault.read_file(directory, temporary, LIMIT * 4) == raw,
+ "interrupted durable-record temporary differs; retain scene")
+ os.rename(temporary, path[-1], src_dir_fd=directory, dst_dir_fd=directory)
+ os.fsync(directory)
+ require(read(root_fd, path, fault) == raw, "durable-record replacement readback failed")
+
+
+def install(root_fd, request, owner, fault, mrf):
+ identity = binding(root_fd, request, owner, fault)
+ path = relative(request)
+ before = read(root_fd, path, fault)
+ require(same_json(request.get("expectedBefore"), entry(before)), "journal bytes drifted from their archived pre-fault identity")
+ earlier = captured(root_fd, request, owner, fault) if request.get("kind") == "stale-generation" else None
+ require(earlier is not None or "captureId" not in request, "only a stale-generation fault consumes an earlier checkpoint")
+ injected = transform(before, request, owner, mrf, earlier=earlier)
+ intent = {**identity, "kind": request["kind"], "before": entry(before), "injected": entry(injected)}
+ if earlier is not None:
+ intent.update(captureId=request["captureId"], capturedGeneration=entry(earlier))
+ with fault.parent(root_fd, ["quarantine"]) as backup:
+ fault.new_file(backup, identity["faultId"] + ".original", before)
+ fault.new_file(backup, identity["faultId"] + ".intent.json", fault.encode(intent))
+ # The candidate is stopped by the node adapter; validate once more immediately before replacement.
+ require(read(root_fd, path, fault) == before, "durable record changed after backup; preserve the intent")
+ replace(root_fd, path, injected, identity["faultId"], fault)
+ fault.new_file(backup, identity["faultId"] + ".applied.json", fault.encode(intent))
+ return {**intent, "applied": True, "originalBackupRetained": True}
+
+
+def recover(root_fd, request, owner, fault):
+ identity = binding(root_fd, request, owner, fault)
+ with fault.parent(root_fd, ["quarantine"]) as backup:
+ intent = decode_json(fault.read_file(backup, identity["faultId"] + ".intent.json", 16384))
+ require(all(same_json(intent.get(key), value) for key, value in identity.items()), "journal recovery lost original run/candidate/device/record identity")
+ before = fault.read_file(backup, identity["faultId"] + ".original", LIMIT * 4)
+ require(entry(before) == intent["before"], "journal original backup is corrupt")
+ if intent["kind"] == "stale-generation":
+ require(intent.get("captureId") == request.get("captureId"), "stale recovery changed its original earlier snapshot")
+ require(entry(captured(root_fd, request, owner, fault)) == intent.get("capturedGeneration"),
+ "stale recovery lost original earlier generation evidence")
+ path = relative(request)
+ current = read(root_fd, path, fault)
+ require(entry(current) in (intent["before"], intent["injected"]),
+ "product changed or advanced the durable responsibility; refusing old-record resurrection")
+ restored = current != before
+ if restored:
+ failed = identity["faultId"] + ".failed-" + entry(current)["sha256"]
+ try: fault.new_file(backup, failed, current)
+ except FileExistsError: require(fault.read_file(backup, failed, LIMIT * 4) == current, "preserved journal differs")
+ replace(root_fd, path, before, identity["faultId"], fault, rescue=True)
+ require(read(root_fd, path, fault) == before, "journal recovery did not preserve exact original bytes")
+ return {**identity, "restoredFromBackup": restored, "backupRetained": True,
+ "originalResponsibilityRecreated": False, "productRepairClaimed": False}
diff --git a/scripts/validation_heal/lock_observer.py b/scripts/validation_heal/lock_observer.py
new file mode 100644
index 00000000..b3aec92d
--- /dev/null
+++ b/scripts/validation_heal/lock_observer.py
@@ -0,0 +1,292 @@
+#!/usr/bin/env python3
+"""Observe one I05 caller admission and drain; never certify the whole case."""
+
+import argparse
+import hashlib
+import json
+from pathlib import Path
+import re
+import time
+import uuid
+from urllib.parse import parse_qs, urlsplit
+
+try:
+ from .caller_abort import disconnect_get
+ from .fleet_node import PROCESS_FIELDS, runtime_config
+ from .heal_probe import CLOCK_DOMAIN, MAX_BODY, Probe, canonical, require, save, validate_binding, validate_s3_request
+ from .healthy import Fleet
+ from .scanner_repair import JournalWindow
+except ImportError:
+ from caller_abort import disconnect_get
+ from fleet_node import PROCESS_FIELDS, runtime_config
+ from heal_probe import CLOCK_DOMAIN, MAX_BODY, Probe, canonical, require, save, validate_binding, validate_s3_request
+ from healthy import Fleet
+ from scanner_repair import JournalWindow
+
+
+TARGET = 'rustfs_ecstore::object_lock_diag'
+CANDIDATE = '757b751482bb970bc321e92059690a7698554358'
+ADMISSION_SECONDS = 10
+DRAIN_SECONDS = 30
+
+
+def caller_attempt(rows, sent, bucket, key):
+ """Bind flattened production logs through the actual HTTP ancestor span.
+
+ External S3 request IDs are server-owned. The client User-Agent is only a
+ unique correlation marker; a client request-id header cannot stand in for
+ server admission. The exact historical version is bound by the HTTP URI;
+ an optional diagnostic version must agree with that same selector.
+ """
+ require(re.fullmatch(r'auto-testing-i05/[0-9a-f-]{36}', sent.get('callerMarker', '')),
+ 'missing caller correlation marker')
+ query = parse_qs(urlsplit(sent.get('requestTarget', '')).query, keep_blank_values=True)
+ require(set(query) == {'versionId'} and len(query['versionId']) == 1,
+ 'caller requires one exact historical version selector')
+ version = query['versionId'][0]
+ require(str(uuid.UUID(version)) == version and uuid.UUID(version).int,
+ 'caller historical version selector is invalid')
+ selected = []
+ for row in rows:
+ if row.get('target') != TARGET:
+ continue
+ spans = [row.get('span'), *row.get('spans', [])]
+ spans = [span for span in spans if isinstance(span, dict) and span.get('name') == 'http-request']
+ marked = [span for span in spans if span.get('user_agent') == sent['callerMarker']]
+ if not marked:
+ continue
+ require(len({canonical(span) for span in marked}) == 1, 'caller has ambiguous HTTP ancestor spans')
+ span = marked[0]
+ request_id = span.get('request_id')
+ require(isinstance(request_id, str) and str(uuid.UUID(request_id)) == request_id
+ and uuid.UUID(request_id).int != 0 and row.get('request_id') == request_id
+ and span.get('method') == 'GET' and span.get('uri') == sent['requestTarget'],
+ 'caller server request ID, method or exact version URI differs')
+ require(row.get('event') == 'object_namespace_lock' and row.get('component') == 'ecstore'
+ and row.get('subsystem') == 'object_lock' and row.get('op') == 'get_object'
+ and row.get('mode') == 'read' and row.get('bucket') == bucket and row.get('object') == key
+ and row.get('requested_version_id') in (None, version) and row.get('timeout_ms') == 1000,
+ 'caller lock diagnostic escaped its exact GET identity or runtime budget')
+ require(all(isinstance(row.get(name), str) and 0 < len(row[name]) <= 2048
+ for name in ('owner', 'namespace', 'lock_attempt_id'))
+ and str(uuid.UUID(row['lock_attempt_id'])) == row['lock_attempt_id']
+ and uuid.UUID(row['lock_attempt_id']).int != 0
+ and type(row.get('acquire_ms')) is int and row['acquire_ms'] >= 0,
+ 'caller lock owner, attempt or timing is incomplete')
+ if row.get('state') == 'failed':
+ require(row.get('level') == 'ERROR' and isinstance(row.get('failure'), str),
+ 'caller lock failure lost its error classification')
+ else:
+ require(row.get('level') == 'TRACE' and type(row.get('hold_ms')) is int and row['hold_ms'] >= 0,
+ 'caller lock lifecycle lost its diagnostic level or hold timing')
+ selected.append(row)
+ if not selected:
+ return None
+ identity = ('request_id', 'lock_attempt_id', 'owner', 'namespace')
+ require(len({tuple(row[name] for name in identity) for row in selected}) == 1,
+ 'caller marker maps to multiple requests or lock owners')
+ states = [row.get('state') for row in selected]
+ allowed = (['acquiring'], ['acquiring', 'acquired'], ['acquiring', 'acquired', 'guard_dropped'],
+ ['acquiring', 'failed'], ['acquiring', 'cancelled'])
+ require(states in allowed, 'caller lock lifecycle is missing, duplicated or out of order')
+ return {**{name: selected[0][name] for name in identity}, 'states': states, 'events': selected,
+ 'admitted': True, 'observedAcquiredWithoutDrop': states == ['acquiring', 'acquired'],
+ 'diagnosticGuardDropped': states == ['acquiring', 'acquired', 'guard_dropped'],
+ 'distributedReleaseProven': False, 'heldAtDisconnectProven': False}
+
+
+def same_process(observed, expected):
+ require(isinstance(observed, dict) and all(name in expected and observed.get(name) == expected[name]
+ for name in PROCESS_FIELDS), 'I05 observation crossed a service process birth or runtime change')
+
+
+def validate_scope(probe, fleet, bucket, entry):
+ """Refuse another service, source or target before any remote observation."""
+ validate_binding(probe.binding)
+ require(fleet.runtime_profile in {'lock-timeout', 'lock-successor'} and fleet.run_id == probe.binding['runId']
+ and probe.binding['candidateCommit'] == CANDIDATE,
+ 'caller observation requires its reviewed candidate and isolated runtime')
+ endpoints = {f'node{index}': f'http://rustfs-node{index}:9000' for index in range(1, len(fleet.nodes) + 1)}
+ expected_nodes = ['vm000'] if len(fleet.nodes) == 1 else ['vm000', 'vm001', 'vm002', 'vm003']
+ require(probe.binding['endpoints'] == endpoints and fleet.nodes == expected_nodes
+ and ((len(fleet.nodes) == 1 and fleet.run_id.endswith('-h1'))
+ or (len(fleet.nodes) == 4 and fleet.run_id.endswith('-h2'))),
+ 'I05 caller endpoints differ from the H1/H2 fleet')
+ require(probe.endpoint_name in endpoints
+ and probe.endpoint == endpoints[probe.endpoint_name].rstrip('/'),
+ 'I05 caller endpoint differs from its binding')
+ require(isinstance(entry, dict) and entry.get('deleteMarker') is False
+ and isinstance(entry.get('key'), str) and 0 < len(entry['key'].encode()) <= 1024
+ and isinstance(entry.get('versionId'), str)
+ and str(uuid.UUID(entry['versionId'])) == entry['versionId'] and uuid.UUID(entry['versionId']).int
+ and isinstance(entry.get('payload'), dict)
+ and set(entry['payload']) == {'size', 'sha256'} and type(entry['payload']['size']) is int
+ and 0 < entry['payload']['size'] <= min(MAX_BODY, probe.binding['maxInventoryBytes'])
+ and isinstance(entry['payload']['sha256'], str)
+ and re.fullmatch(r'[0-9a-f]{64}', entry['payload']['sha256']),
+ 'caller drain requires an acknowledged historical content manifest')
+ validate_s3_request(probe.binding, 'get_object', {
+ 'Bucket': bucket, 'Key': entry.get('key'), 'VersionId': entry['versionId']})
+
+
+def observe_disconnect(probe, fleet, bucket, entry, output, *, while_admitted=None,
+ receive_buffer_bytes=None, before_verification=None, after_disconnect=None):
+ """Use an already deployed exclusive I05 service; do not alter the fleet."""
+ validate_scope(probe, fleet, bucket, entry)
+ output = Path(output)
+ output.mkdir(exist_ok=False)
+ bound = canonical(probe.binding)
+ save(output / 'scope.json', {'candidateCommit': CANDIDATE, 'bindingSha256': hashlib.sha256(bound.encode()).hexdigest(),
+ 'runId': fleet.run_id, 'entry': entry, 'admissionSeconds': ADMISSION_SECONDS, 'drainSeconds': DRAIN_SECONDS,
+ 'canonicalCasePassClaimed': False, 'mutatesServiceConfiguration': False})
+ expected, baseline = {}, {}
+ def metrics(label):
+ values = {}
+ for node in fleet.nodes:
+ value = fleet.record(label, {'node': node, **fleet.remote(node, 'process-metrics')})
+ identity = value['identity']
+ require(identity.get('binarySha256') == probe.binding['candidateBinarySha256']
+ and all(identity.get('settings', {}).get(k) == v for k, v in runtime_config(fleet.runtime_profile).items()),
+ 'I05 resource sample has another candidate or process configuration')
+ if node in expected:
+ same_process(identity, expected[node])
+ else:
+ expected[node] = identity
+ require(type(value.get('fileDescriptors')) is int and value['fileDescriptors'] >= 0,
+ 'I05 descriptor count is incomplete')
+ values[node] = value
+ return values
+ baseline = metrics('I05-caller-baseline-resources')
+ window = JournalWindow(fleet, 'I05-caller', event_targets={TARGET}, strict_json=True)
+ node = fleet.nodes[int(probe.endpoint_name.removeprefix('node')) - 1]
+ admitted = None
+ admitted_work = None
+ network_namespace = None
+ def capture(sent):
+ validate_binding(probe.binding)
+ require(canonical(probe.binding) == bound, 'I05 caller binding drifted during observation')
+ events = window.capture()
+ attempts = {host: caller_attempt(rows, sent, bucket, entry['key']) for host, rows in events.items()}
+ require(all(value is None for host, value in attempts.items() if host != node),
+ 'caller marker appeared on a foreign endpoint')
+ return attempts[node]
+ def sockets(sent, label):
+ nonlocal network_namespace
+ value = fleet.record(label, {'node': node, **fleet.remote(node, 'caller-socket',
+ expectedProcess=expected[node], clientAddress=sent['localAddress'], serverAddress=sent['peerAddress'])})
+ same_process(value.get('process'), expected[node])
+ require(value.get('readOnly') is True and value.get('clockDomain') == expected[node]['bootId']
+ and len(value.get('samples', [])) == 2, 'caller socket census lacks its original process or samples')
+ namespace = value.get('networkNamespace')
+ require(isinstance(namespace, str) and re.fullmatch(r'net:\[[0-9]+\]', namespace),
+ 'caller socket census lost its network namespace')
+ require(network_namespace is None or namespace == network_namespace,
+ 'caller socket moved to another network namespace between admission and drain')
+ network_namespace = namespace
+ return value
+ def before_disconnect(sent):
+ nonlocal admitted, admitted_work
+ deadline = time.monotonic() + ADMISSION_SECONDS
+ while True:
+ observed = capture(sent)
+ if observed is not None:
+ require(observed['states'] in (['acquiring'], ['acquiring', 'acquired']),
+ 'NOT_OBSERVED: caller lock already ended before controlled disconnect')
+ if observed['observedAcquiredWithoutDrop']:
+ connection = sockets(sent, 'I05-caller-before-disconnect-socket')
+ require(all(sample['vanishedDescriptors'] == 0 and len(sample['ownedMatches']) == 1
+ and sample['ownedMatches'][0]['state'] == '01'
+ for sample in connection['samples']),
+ 'NOT_OBSERVED: admitted caller lacks its exact established service descriptor')
+ # Re-read the journal after the socket census. A later
+ # native fence must still prove the exact disconnect edge.
+ observed = capture(sent)
+ require(observed is not None and observed['observedAcquiredWithoutDrop'],
+ 'NOT_OBSERVED: caller lock ended while sampling its service descriptor')
+ metrics('I05-caller-admission-resources')
+ require(time.monotonic() <= deadline, 'NOT_OBSERVED: caller admission observation exceeded its budget')
+ admitted = {'node': node, 'attempt': observed, 'socket': connection}
+ save(output / 'admission.json', admitted)
+ if while_admitted is not None:
+ admitted_work = while_admitted(sent, admitted, window)
+ return
+ require(time.monotonic() < deadline, 'NOT_OBSERVED: caller lock acquisition window was not observed')
+ time.sleep(.1)
+ try:
+ transport_options = {'before_disconnect': before_disconnect}
+ if receive_buffer_bytes is not None:
+ transport_options['receive_buffer_bytes'] = receive_buffer_bytes
+ sent = disconnect_get(probe, bucket, entry['key'], entry['versionId'], output / 'transport',
+ **transport_options)
+ disconnected_work = after_disconnect(sent, admitted, window) if after_disconnect is not None else None
+ deadline = time.monotonic() + DRAIN_SECONDS
+ while True:
+ observed = capture(sent)
+ require(observed is not None and all(observed[name] == admitted['attempt'][name]
+ for name in ('request_id', 'lock_attempt_id', 'owner', 'namespace')),
+ 'caller drain lost its original service request or lock owner')
+ connection = sockets(sent, 'I05-caller-drain-socket')
+ resources = metrics('I05-caller-drain-resources')
+ require(time.monotonic() <= deadline, 'NOT_OBSERVED: caller drain observation exceeded its budget')
+ if (observed['diagnosticGuardDropped'] and all(sample['vanishedDescriptors'] == 0
+ and not sample['ownedMatches'] for sample in connection['samples'])
+ and all(resources[host]['fileDescriptors'] <= baseline[host]['fileDescriptors'] for host in fleet.nodes)):
+ break
+ require(time.monotonic() < deadline, 'NOT_OBSERVED: caller diagnostic, socket or FD count did not drain')
+ time.sleep(.2)
+ verification_precondition = before_verification() if before_verification is not None else None
+ read = fleet.record('I05-caller-final-exact-version', probe.s3('get_object',
+ {'Bucket': bucket, 'Key': entry['key'], 'VersionId': entry['versionId']}))
+ require(read.get('acknowledged') is True and read.get('resultUnknown') is False
+ and read.get('response', {}).get('VersionId') == entry['versionId'] and read.get('payload') == entry['payload'],
+ 'caller drain changed the acknowledged historical version or its complete content')
+ deadline = time.monotonic() + DRAIN_SECONDS
+ while True:
+ capture(sent)
+ verified_resources = metrics('I05-caller-after-verification-resources')
+ require(time.monotonic() <= deadline, 'NOT_OBSERVED: I05 verification resource observation exceeded its budget')
+ if all(verified_resources[host]['fileDescriptors'] <= baseline[host]['fileDescriptors'] for host in fleet.nodes):
+ break
+ require(time.monotonic() < deadline, 'NOT_OBSERVED: I05 verification did not return to its FD count baseline')
+ time.sleep(.2)
+ result = {'observationComplete': True, 'clockDomain': CLOCK_DOMAIN,
+ 'observationCompletedMonotonicNs': time.monotonic_ns(), 'baselineResources': baseline,
+ 'admission': admitted, 'finalAttempt': observed,
+ 'admittedWork': admitted_work,
+ 'disconnectedWork': disconnected_work,
+ 'verificationPrecondition': verification_precondition,
+ 'finalSocket': connection, 'finalResources': resources, 'afterVerificationResources': verified_resources,
+ 'exactHistoricalContentPreserved': True,
+ 'callerResultUnknown': True, 'heldAtDisconnectProven': False, 'distributedReleaseProven': False,
+ 'noGlobalFdLeakProven': False, 'canonicalCasePassClaimed': False,
+ 'remaining': ['exact native disconnect/timeout/stale-generation fences',
+ 'complete H1/H2 DL/DV ACK and physical closure', 'queued-successor lock and FD closure']}
+ save(output / 'observation.json', result)
+ return result
+ except BaseException as error:
+ save(output / 'failure.json', {'observationComplete': False, 'errorType': type(error).__name__,
+ 'state': 'NOT_OBSERVED' if str(error).startswith('NOT_OBSERVED:') else 'INCOMPLETE',
+ 'canonicalCasePassClaimed': False, 'originalTransportEvidenceRetained': True})
+ raise
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--binding', type=Path, required=True)
+ parser.add_argument('--nodes', nargs='+', required=True)
+ parser.add_argument('--bucket', required=True)
+ parser.add_argument('--entry', type=Path, required=True)
+ parser.add_argument('--output', type=Path, required=True)
+ parser.add_argument('--endpoint')
+ args = parser.parse_args()
+ probe = Probe(json.loads(args.binding.read_text()), args.endpoint)
+ args.output.mkdir(parents=True, exist_ok=False)
+ # The component uses an existing owned deployment and never calls cleanup.
+ # Keep its SSH/log evidence separate from the exclusive request directory.
+ fleet = Fleet(args.nodes, args.output, probe.binding['runId'], runtime_profile='lock-timeout')
+ fleet.secret_values.update(probe.credentials())
+ observe_disconnect(probe, fleet, args.bucket, json.loads(args.entry.read_text()), args.output / 'caller')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/scripts/validation_heal/lock_source.py b/scripts/validation_heal/lock_source.py
new file mode 100644
index 00000000..961eeacb
--- /dev/null
+++ b/scripts/validation_heal/lock_source.py
@@ -0,0 +1,225 @@
+#!/usr/bin/env python3
+"""Compose I05 source procedures without converting diagnostics to canonical PASS."""
+
+import copy
+import hashlib
+import json
+from pathlib import Path
+
+try:
+ from .heal_probe import CLOCK_DOMAIN, canonical, require, save
+ from .race_native_evidence import verify as verify_native
+except ImportError:
+ from heal_probe import CLOCK_DOMAIN, canonical, require, save
+ from race_native_evidence import verify as verify_native
+
+
+CASE_ID = 'heal.lock-timeout-and-caller-abort'
+GROUPS = {
+ CASE_ID: ('versioned-read-repair-lock-owner', 'cancelled-physical-unlink-owner',
+ 'stale-read-repair-final-rename'),
+ 'heal.race-versioned-put': ('versioned-read-repair-lock-owner', 'explicit-version-append-final-rename'),
+ 'heal.race-version-delete': ('explicit-version-delete-final-rename',),
+}
+IDENTITY = ('candidateCommit', 'candidatePackageSha256', 'candidateSourceTree',
+ 'candidateCargoLockSha256', 'binarySha256', 'runId', 'runAttempt',
+ 'driverCommit', 'testingCommit', 'selectedContractSha256')
+LIVE_BRANCHES = ('timeout', 'queued-successor')
+
+
+def verify_source_native(args, candidate):
+ """Reuse three reviewed subsets from one immutable successful artifact.
+
+ The old stale test asserts ETag only. The append/delete subsets additionally
+ bind real reconstruction barriers, foreground ACK/tail drain, exact typed
+ outcomes, full surviving bodies and physical generations in 4/2 and 16/4.
+ Their native geometry does not claim a fleet request hit the same barrier.
+ """
+ directory = getattr(args, 'native_directory', None)
+ require(directory is not None, 'I05 source-plan requires exact-source native evidence')
+ groups = {}
+ for case_id, labels in GROUPS.items():
+ proof = verify_native(directory, candidate, args.testing_commit, case_id)
+ require(proof.get('caseId') == case_id and proof.get('requiredRegressionLabels') == list(labels)
+ and set(proof.get('observedRegressions', {})) == set(labels)
+ and proof.get('requiredNativeSubsetPassed') is True
+ and all(proof.get(key) is False for key in (
+ 'wholeCasePassClaimed', 'fleetExecutionClaimed', 'canonicalCasePassClaimed')),
+ 'I05 source native proof lacks its exact reviewed subset or changes scope')
+ require(proof.get('candidateCommit') == candidate['source_sha']
+ and proof.get('candidatePackageSha256') == candidate['package_sha256']
+ and proof.get('testingCommit') == args.testing_commit,
+ 'I05 source native proof changed candidate or helper')
+ groups[case_id] = proof
+ first = groups[CASE_ID]
+ require(all(key in first and first[key] is not None for key in IDENTITY)
+ and all(type(proof.get(key)) is type(first[key]) and proof[key] == first[key]
+ for proof in groups.values() for key in IDENTITY),
+ 'I05 source native subsets mix candidates, binaries or workflow attempts')
+ return {**{key: first[key] for key in IDENTITY}, 'groups': groups,
+ 'nativeOwnerAndStaleFencesPassed': True,
+ 'staleContentAndPhysicalAuthorityScope': 'independent native append/delete in 4/2 and 16/4',
+ 'fleetExecutionClaimed': False, 'wholeCasePassClaimed': False,
+ 'canonicalCasePassClaimed': False}
+
+
+def link_fleet_evidence(directory, branch, binding, diagnostic):
+ """Bind durable requests to coordinator timing and actual server identities.
+
+ Controllers already verify original server logs, full peer inventories,
+ physical state and bounded resource drain. This joins their retained bytes;
+ it never compares a remote monotonic clock to the coordinator clock.
+ """
+ require(branch in LIVE_BRANCHES, 'I05 source evidence has an unknown live branch')
+ directory = Path(directory)
+ hashes = {}
+
+ def read(name):
+ path = directory / name
+ require(path.is_file() and not path.is_symlink() and path.stat().st_size < 16 * 1024**2,
+ 'I05 source chain lacks an original evidence file: ' + name)
+ raw = path.read_bytes()
+ hashes[name] = hashlib.sha256(raw).hexdigest()
+ return json.loads(raw)
+
+ prefix = branch + '/'
+ scope = read(prefix + 'scope.json')
+ caller_prefix = prefix + 'caller/'
+ intent = read(caller_prefix + 'transport/request.json')
+ written = read(caller_prefix + 'transport/request-written.json')
+ closed = read(caller_prefix + 'transport/disconnected.json')
+ admission = read(caller_prefix + 'admission.json')
+ caller = read(caller_prefix + 'observation.json')
+ stored = read(prefix + 'diagnostic.json')
+ require(stored == diagnostic and diagnostic.get('diagnosticComplete') is True
+ and caller == diagnostic.get('callerObservation') and caller.get('admission') == admission
+ and caller.get('observationComplete') is True and caller.get('exactHistoricalContentPreserved') is True
+ and caller.get('callerResultUnknown') is True
+ and all(caller.get(key) is False for key in (
+ 'heldAtDisconnectProven', 'distributedReleaseProven', 'canonicalCasePassClaimed')),
+ 'I05 source chain changed caller observation or invents ownership')
+ digest = hashlib.sha256(canonical(binding).encode()).hexdigest()
+ require(scope.get('runId') == intent.get('runId') == written.get('runId') == closed.get('runId') == binding['runId']
+ and scope.get('bindingSha256') == intent.get('bindingSha256') == digest
+ and all(intent.get(key) == binding[key] for key in (
+ 'candidateCommit', 'harnessCommit', 'candidatePackageSha256', 'candidateBinarySha256'))
+ and intent.get('params') == {'Bucket': binding['bucketPrefix'], 'Key': scope['entry']['key'],
+ 'VersionId': scope['entry']['versionId']}
+ and intent.get('retryAllowed') is False and intent.get('operation') == 'get_object'
+ and intent.get('endpoint') == binding['defaultEndpoint']
+ and intent.get('endpointUrl') == binding['endpoints'][binding['defaultEndpoint']].rstrip('/'),
+ 'I05 source chain changed binding, candidate, run or exact historical request')
+ request_sha = hashes[caller_prefix + 'transport/request.json']
+ require(all(row.get('requestSha256') == request_sha and row.get('callerMarker') == intent.get('callerMarker')
+ and row.get('requestTarget') == intent.get('requestTarget')
+ and row.get('requestInvocationStarted') is True and row.get('requestWriteCompleted') is True
+ and all(row.get(key) is False for key in ('responseObserved', 'acknowledged', 'retryAllowed', 'serverOwnershipProven'))
+ and row.get('resultUnknown') is True for row in (written, closed))
+ and closed.get('connectionAttempts') == 1 and closed.get('clientSocketClosed') is True
+ and closed.get('shutdownCompleted') is True
+ and all(closed.get(key) == written.get(key) for key in (
+ 'callStartedMonotonicNs', 'requestWrittenMonotonicNs', 'localAddress', 'peerAddress')),
+ 'I05 source chain lost its single unretried TCP request or unknown outcome')
+ holder, final = admission['attempt'], caller['finalAttempt']
+ require(all(holder.get(key) == final.get(key) and holder.get(key) is not None for key in (
+ 'request_id', 'lock_attempt_id', 'owner', 'namespace'))
+ and holder.get('observedAcquiredWithoutDrop') is True and final.get('diagnosticGuardDropped') is True,
+ 'I05 source chain lost original server request, owner or local guard drain')
+ edge_name = 'timeout.json' if branch == 'timeout' else 'queued.json'
+ edge = read(prefix + edge_name)
+ ack_prefix = prefix + ('contender/' if branch == 'timeout' else 'successor/')
+ request = read(ack_prefix + '000001-request.json')
+ ack = read(ack_prefix + '000001-ack.json')
+ require(ack.get('requestSha256') == hashes[ack_prefix + '000001-request.json']
+ and request.get('runId') == ack.get('runId') == binding['runId']
+ and request.get('params', {}).get('Bucket') == intent['params']['Bucket']
+ and request.get('params', {}).get('Key') == intent['params']['Key']
+ and request.get('operation') == 'put_object' and ack.get('operationCheck') == 'PASS'
+ and ack.get('observation', {}).get('response', {}).get('ResponseMetadata', {}).get('RequestId') == edge.get('requestId'),
+ 'I05 source chain lost the exact foreground request or original ACK')
+ times = [intent.get('preparedMonotonicNs'), written.get('callStartedMonotonicNs'),
+ written.get('requestWrittenMonotonicNs'), edge.get('observedMonotonicNs'),
+ closed.get('disconnectStartedMonotonicNs'), closed.get('clientClosedMonotonicNs'),
+ caller.get('observationCompletedMonotonicNs')]
+ timed = [intent, written, closed, edge, caller, request, ack, ack.get('observation', {})]
+ require(all(row.get('clockDomain') == CLOCK_DOMAIN for row in timed)
+ and all(type(value) is int and value > 0 for value in times) and times == sorted(times),
+ 'I05 source chain lost coordinator clock order around the actual TCP disconnect')
+ if branch == 'timeout':
+ require(edge.get('typedTimeoutBeyondBudgetObserved') is True and edge.get('writeLockAcquired') is False
+ and edge.get('readResponseBackpressureObservedAfterTimeout') is True
+ and diagnostic.get('timeoutContenderAcknowledged') is False,
+ 'I05 source chain has no typed contention beyond its registered timeout')
+ else:
+ completion = read(prefix + 'successor-complete.json')
+ require(edge.get('states') == ['acquiring']
+ and all(completion.get(key) == edge.get(key) for key in ('requestId', 'lockAttemptId'))
+ and completion.get('states') == ['acquiring', 'acquired', 'guard_dropped']
+ and completion.get('queuedBeforeDisconnectObserved') is True
+ and completion.get('originalDiagnosticDropBeforeAcquire') is True
+ and completion.get('heldAtDisconnectProven') is False
+ and completion.get('clockDomain') == CLOCK_DOMAIN
+ and type(completion.get('observedMonotonicNs')) is int
+ and times[-2] <= completion['observedMonotonicNs'] <= times[-1],
+ 'I05 source chain lacks the queued successor and ordered original guard release')
+ return {'sourceEvidenceComplete': True, 'branch': branch, 'runId': binding['runId'],
+ 'bindingSha256': digest, 'originalEvidenceSha256': hashes,
+ 'callerRequestId': holder['request_id'], 'callerLockAttemptId': holder['lock_attempt_id'],
+ 'foregroundRequestId': edge['requestId'], 'coordinatorClockDomain': CLOCK_DOMAIN,
+ 'coordinatorTimes': times, 'heldAtDisconnectProven': False, 'distributedReleaseProven': False,
+ 'nativeOwnerProofRequiredSeparately': True, 'canonicalCasePassClaimed': False}
+
+
+def run_source_plan(args, candidate, inspector, run_topology, native_proof):
+ """Run real timeout/disconnect controllers serially, retaining the first dirty scene."""
+ save(args.evidence / 'source-native-prerequisite.json', native_proof)
+ results = []
+ remaining = [f'{branch}/{topology}' for branch in LIVE_BRANCHES for topology in args.topologies]
+ for branch in LIVE_BRANCHES:
+ child = copy.copy(args)
+ child.diagnostic_branch, child.source_plan = branch, True
+ child.evidence = args.evidence / 'source-plan' / branch
+ for topology in args.topologies:
+ result = run_topology(child, candidate, inspector, topology)
+ results.append(result)
+ if not (result.get('diagnosticComplete') is True and result.get('cleanup') == 'PASS'
+ and result.get('sourceEvidenceChain', {}).get('sourceEvidenceComplete') is True):
+ break
+ remaining.remove(f'{branch}/{topology}')
+ if remaining and remaining[0].startswith(branch + '/'):
+ break
+ passed = not remaining
+ summary = {'caseId': CASE_ID, 'diagnosticBranch': 'source-plan', 'diagnostics': results,
+ 'requestedTopologies': args.topologies, 'diagnosticComplete': passed,
+ 'sourceProceduresComplete': passed and args.topologies == ['H1', 'H2'],
+ 'sourceBranches': {
+ 'object-lock-timeout': {'scope': 'fleet', 'controller': 'timeout'},
+ 'caller-disconnect': {'scope': 'fleet', 'controller': 'queued-successor'},
+ 'stale-read-repair': {'scope': 'independent-native',
+ 'groups': ['heal.race-versioned-put', 'heal.race-version-delete']},
+ 'native-lock-owner-fence': {'scope': 'independent-native', 'group': CASE_ID}},
+ 'nativeProof': native_proof, 'remainingLiveInstances': remaining,
+ 'cleanup': 'PASS' if results and all(row['cleanup'] == 'PASS' for row in results) else 'FAIL',
+ 'complete': False, 'wholeCasePassClaimed': False, 'canonicalCasePassClaimed': False,
+ 'heldAtDisconnectProven': False, 'distributedReleaseProven': False,
+ 'remaining': ['independent complete live audit of the source plan']}
+ save(args.evidence / 'source-plan-summary.json', summary)
+ save(args.evidence / 'healthy-summary.json', summary)
+ return summary
+
+
+def main():
+ import argparse
+ from types import SimpleNamespace
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--directory', type=Path, required=True)
+ parser.add_argument('--candidate', type=Path, required=True)
+ parser.add_argument('--testing-commit', required=True)
+ args = parser.parse_args()
+ proof = verify_source_native(SimpleNamespace(native_directory=args.directory,
+ testing_commit=args.testing_commit), json.loads(args.candidate.read_text()))
+ print(json.dumps(proof, indent=2))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/scripts/validation_heal/lock_successor.py b/scripts/validation_heal/lock_successor.py
new file mode 100644
index 00000000..e46ea06b
--- /dev/null
+++ b/scripts/validation_heal/lock_successor.py
@@ -0,0 +1,373 @@
+#!/usr/bin/env python3
+"""I05 queued PUT after one caller disconnect; diagnostic subset only."""
+
+import copy
+from concurrent.futures import ThreadPoolExecutor
+import hashlib
+import json
+from pathlib import Path
+import time
+from urllib.parse import quote
+import uuid
+
+try:
+ from . import lock_observer
+ from .heal_probe import CLOCK_DOMAIN, Probe, canonical, payload, require, save, validate_binding
+ from .heal_workload import Recorder, verify_seed
+ from .healthy import physical_content
+ from .lock_timeout import CALLER_RECEIVE_BUFFER_BYTES, MIN_BACKPRESSURE_BYTES, require_exact_inventory_entry
+ from .shards import files, map_target
+except ImportError:
+ import lock_observer
+ from heal_probe import CLOCK_DOMAIN, Probe, canonical, payload, require, save, validate_binding
+ from heal_workload import Recorder, verify_seed
+ from healthy import physical_content
+ from lock_timeout import CALLER_RECEIVE_BUFFER_BYTES, MIN_BACKPRESSURE_BYTES, require_exact_inventory_entry
+ from shards import files, map_target
+
+
+SUCCESSOR_BYTES = 1024 * 1024 + 197
+QUEUE_SECONDS = 10
+COMPLETE_SECONDS = 30
+PUT_TIMEOUT_MS = 60000
+
+
+class SuccessorProbe(Probe):
+ def __init__(self, original, marker):
+ super().__init__(original.binding, original.endpoint_name, credential_pair=original.credential_pair)
+ self.marker = marker
+
+ def s3_client(self):
+ client = super().s3_client()
+ client.meta.events.register('before-sign.s3.PutObject', self.mark)
+ return client
+
+ def mark(self, request, **kwargs):
+ value = request.headers.get('User-Agent', '')
+ if isinstance(value, bytes):
+ value = value.decode('ascii')
+ request.headers['User-Agent'] = value + ' ' + self.marker
+
+
+def successor_attempt(rows, marker, holder, bucket, key):
+ """The caller marker selects one server request, never supplies its ID."""
+ require(marker.startswith('auto-testing-i05-successor/')
+ and str(uuid.UUID(marker.split('/')[-1])) == marker.split('/')[-1],
+ 'I05 successor marker differs')
+ selected, request_ids, metadata_attempts = [], set(), {}
+ for row in rows:
+ if row.get('target') != lock_observer.TARGET:
+ continue
+ spans = [span for span in [row.get('span'), *row.get('spans', [])]
+ if isinstance(span, dict) and span.get('name') == 'http-request'
+ and marker in str(span.get('user_agent', '')).split()]
+ if not spans:
+ continue
+ require(len({canonical(span) for span in spans}) == 1,
+ 'I05 successor has ambiguous HTTP ancestry')
+ span = spans[0]
+ request_id = span.get('request_id')
+ require(isinstance(request_id, str) and str(uuid.UUID(request_id)) == request_id
+ and uuid.UUID(request_id).int and request_id != holder['request_id']
+ and row.get('request_id') == request_id and span.get('method') == 'PUT'
+ and span.get('uri') == '/' + quote(bucket, safe='') + '/' + quote(key, safe='/~'),
+ 'I05 successor server request, method or exact URI differs')
+ request_ids.add(request_id)
+ # Slow-acquire warnings share the target but carry no lock lifecycle.
+ # Bind this auxiliary row to the preceding acquired write observation.
+ if row.get('message') == 'object namespace lock acquisition exceeded threshold':
+ require(row.get('level') == 'WARN' and row.get('op') == 'put_object_commit'
+ and row.get('bucket') == bucket and row.get('object') == key
+ and row.get('mode') == 'write' and row.get('owner') == holder['owner']
+ and not any(name in row for name in ('event', 'component', 'subsystem', 'state',
+ 'namespace', 'lock_attempt_id', 'timeout_ms', 'hold_ms', 'requested_version_id'))
+ and all(isinstance(row.get(name), str) and row[name].isascii()
+ and row[name].isdecimal() and str(int(row[name])) == row[name]
+ for name in ('acquire_ms', 'threshold_ms'))
+ and int(row['acquire_ms']) >= int(row['threshold_ms']) > 0
+ and selected and selected[-1]['state'] == 'acquired'
+ and selected[-1]['request_id'] == request_id
+ and int(row['acquire_ms']) >= selected[-1]['acquire_ms'],
+ 'I05 successor slow-acquire warning lacks its exact acquired write')
+ continue
+ # Bucket fencing reads both persisted metadata objects within this PUT.
+ # They share its HTTP identity, but cannot establish write admission.
+ metadata_read = (row.get('mode') == 'read' and row.get('op') == 'get_object'
+ and row.get('bucket') == '.rustfs.sys'
+ and row.get('object') in {f'buckets/{bucket}/.bucket-incarnation',
+ f'buckets/{bucket}/.metadata.bin'})
+ require(row.get('event') == 'object_namespace_lock' and row.get('component') == 'ecstore'
+ and row.get('subsystem') == 'object_lock'
+ and (metadata_read or row.get('op') == 'put_object_commit'
+ and row.get('bucket') == bucket and row.get('object') == key and row.get('mode') == 'write')
+ and row.get('requested_version_id') is None and row.get('owner') == holder['owner']
+ and row.get('namespace') == holder['namespace'] and type(row.get('timeout_ms')) is int
+ and row['timeout_ms'] == (1000 if metadata_read else PUT_TIMEOUT_MS)
+ and isinstance(row.get('lock_attempt_id'), str)
+ and str(uuid.UUID(row['lock_attempt_id'])) == row['lock_attempt_id']
+ and uuid.UUID(row['lock_attempt_id']).int
+ and row['lock_attempt_id'] != holder['lock_attempt_id']
+ and type(row.get('acquire_ms')) is int and row['acquire_ms'] >= 0,
+ 'I05 successor lock attempt escaped its exact namespace or budget')
+ require(row.get('state') in {'acquiring', 'acquired', 'guard_dropped'}
+ and row.get('level') == 'TRACE'
+ and type(row.get('hold_ms')) is int and row['hold_ms'] >= 0,
+ 'I05 successor failed, cancelled or lost its diagnostic lifecycle')
+ if metadata_read:
+ metadata_attempts.setdefault(row['lock_attempt_id'], []).append(row)
+ else:
+ selected.append(row)
+ for attempt_id, events in metadata_attempts.items():
+ require(len({row['object'] for row in events}) == 1
+ and not any(row['lock_attempt_id'] == attempt_id for row in selected)
+ and [row['state'] for row in events] in (['acquiring'], ['acquiring', 'acquired'],
+ ['acquiring', 'acquired', 'guard_dropped']),
+ 'I05 successor metadata read reused an attempt or lost its lifecycle')
+ require(len(request_ids) <= 1, 'I05 successor marker was retried or reused')
+ if not selected:
+ return None
+ require(len({row['lock_attempt_id'] for row in selected}) == 1,
+ 'I05 successor made multiple target write attempts')
+ states = [row['state'] for row in selected]
+ require(states in (['acquiring'], ['acquiring', 'acquired'],
+ ['acquiring', 'acquired', 'guard_dropped']),
+ 'I05 successor write lifecycle is missing, duplicated or out of order')
+ return {'requestId': selected[0]['request_id'], 'lockAttemptId': selected[0]['lock_attempt_id'],
+ 'states': states, 'events': selected, 'canonicalCasePassClaimed': False}
+
+
+def validate_successor_ack(directory, probe, bucket, key, body, attempt):
+ request_path = directory / '000001-request.json'
+ ack_path = directory / '000001-ack.json'
+ require({path.name for path in directory.iterdir()} == {request_path.name, ack_path.name},
+ 'I05 successor lacks exactly one original request and receipt')
+ request, ack = json.loads(request_path.read_text()), json.loads(ack_path.read_text())
+ params = {'Bucket': bucket, 'Key': key, 'ContentType': 'application/octet-stream',
+ 'Metadata': {'heal-fixture': 'I05-queued-successor'}}
+ digest = {'size': len(body), 'sha256': hashlib.sha256(body).hexdigest()}
+ require(request.get('schemaVersion') == ack.get('schemaVersion') == 2
+ and request.get('runId') == ack.get('runId') == probe.binding['runId']
+ and request.get('sequence') == ack.get('sequence') == 1
+ and request.get('operation') == 'put_object' and request.get('params') == params
+ and request.get('payload') == digest and request.get('expectHttp') is None
+ and request.get('identity') is None and ack.get('identity') is None
+ and ack.get('requestFile') == request_path.name
+ and ack.get('requestSha256') == hashlib.sha256(request_path.read_bytes()).hexdigest()
+ and ack.get('operationCheck') == 'PASS'
+ and request.get('casePassClaimed') is False and ack.get('casePassClaimed') is False,
+ 'I05 successor receipt differs from its exact durable PUT')
+ observation = ack.get('observation', {})
+ response = observation.get('response') or {}
+ metadata = response.get('ResponseMetadata', {})
+ version = response.get('VersionId')
+ require(observation.get('operation') == 'put_object' and observation.get('params') == params
+ and observation.get('endpoint') == probe.endpoint_name and observation.get('payload') == digest
+ and observation.get('clientCallInvoked') is True and observation.get('acknowledged') is True
+ and observation.get('resultUnknown') is False and not observation.get('clientCleanupError')
+ and metadata.get('HTTPStatusCode') == 200 and type(metadata.get('RetryAttempts')) is int
+ and metadata['RetryAttempts'] == 0 and metadata.get('RequestId') == attempt['requestId']
+ and isinstance(version, str) and str(uuid.UUID(version)) == version and uuid.UUID(version).int
+ and response.get('ETag') == '"' + hashlib.md5(body, usedforsecurity=False).hexdigest() + '"',
+ 'I05 successor lacks one known, unretried, content-bound version ACK')
+ times = [request.get('preparedMonotonicNs'), ack.get('collectorCallStartedMonotonicNs'),
+ observation.get('callStartedMonotonicNs'), observation.get('callFinishedMonotonicNs'),
+ ack.get('collectorCallFinishedMonotonicNs'), ack.get('recordedMonotonicNs')]
+ require(request.get('clockDomain') == ack.get('clockDomain') == observation.get('clockDomain') == CLOCK_DOMAIN
+ and all(type(value) is int and value > 0 for value in times) and times == sorted(times),
+ 'I05 successor receipt lost its collector clock ordering')
+ return {'key': key, 'versionId': version, 'isLatest': True, 'deleteMarker': False,
+ 'payload': digest, 'properties': {'ETag': response['ETag'], 'ContentLength': len(body),
+ 'ContentType': params['ContentType'], 'Metadata': params['Metadata']}, 'tags': []}
+
+
+def physical_append(before, after, bucket, baseline, added):
+ """Bind all old shards and the exact new version before any public read."""
+ require(set(before) == set(after) and all(before[node]['formats'] == after[node]['formats'] for node in before),
+ 'I05 successor changed physical topology or formats')
+ old, new = files(before), files(after)
+ metadata_path = f"{bucket}/{added['key']}/xl.meta"
+ for identity, entry in old.items():
+ require(identity in new, 'I05 successor removed an original physical file')
+ if identity[2] != metadata_path:
+ require(new[identity] == entry, 'I05 successor changed an original shard or control')
+ target = map_target(after, bucket, added['key'], 'data', 1, added['versionId'])
+ old_dirs = set()
+ for entry in baseline['entries']:
+ if entry['key'] == added['key'] and not entry['deleteMarker']:
+ original = map_target(before, bucket, entry['key'], 'data', 1, entry['versionId'], latest_required=False)
+ old_dirs.add(original['dataDir'])
+ retained = map_target(after, bucket, entry['key'], 'data', 1, entry['versionId'], latest_required=False)
+ require(original['dataDir'] == retained['dataDir']
+ and {name: value for name, value in original['logical'].items() if name != 'isLatest'}
+ == {name: value for name, value in retained['logical'].items() if name != 'isLatest'}
+ and [(p['index'], p['sha256'], p['size']) for p in original['cohort']]
+ == [(p['index'], p['sha256'], p['size']) for p in retained['cohort']],
+ 'I05 successor rewrote an acknowledged historical generation')
+ require(target['dataDir'] not in old_dirs and target['logical']['size'] == added['payload']['size'],
+ 'I05 successor reused an old physical generation or wrong logical size')
+ expected = sorted((entry['versionId'], entry['deleteMarker'], False if entry['key'] == added['key'] else entry['isLatest'])
+ for entry in baseline['entries'] if entry['key'] == added['key'])
+ expected.append((added['versionId'], False, True))
+ for identity, entry in new.items():
+ if identity[2] == metadata_path:
+ versions = entry['metadata']['versions']
+ require(sorted((v['versionId'], v['deleted'], v['isLatest']) for v in versions) == sorted(expected),
+ 'I05 physical version population differs from original and successor ACKs')
+ if identity not in old:
+ prefix = f"{bucket}/{added['key']}/{target['dataDir']}/"
+ leaf = identity[2].removeprefix(prefix)
+ require(identity[2].startswith(prefix)
+ and (leaf == 'part.1' or leaf.startswith('part.1.integrity.')
+ and str(uuid.UUID(leaf.removeprefix('part.1.integrity.')))
+ == leaf.removeprefix('part.1.integrity.')),
+ 'I05 successor left an unacknowledged physical key or generation')
+ return target
+
+
+def observe_successor(probe, fleet, bucket, entry, output):
+ lock_observer.validate_scope(probe, fleet, bucket, entry)
+ require(fleet.runtime_profile == 'lock-successor', 'I05 queued successor requires its separate runtime budget')
+ output = Path(output)
+ output.mkdir(exist_ok=False)
+ bound = canonical(probe.binding)
+ marker = 'auto-testing-i05-successor/' + str(uuid.uuid4())
+ save(output / 'scope.json', {'runId': fleet.run_id, 'entry': entry, 'marker': marker,
+ 'bindingSha256': hashlib.sha256(bound.encode()).hexdigest(), 'putTimeoutMs': PUT_TIMEOUT_MS,
+ 'queueSeconds': QUEUE_SECONDS, 'completeSeconds': COMPLETE_SECONDS,
+ 'canonicalCasePassClaimed': False})
+ def binding():
+ validate_binding(probe.binding)
+ require(canonical(probe.binding) == bound, 'I05 successor binding changed')
+ physical = fleet.snapshot('I05-successor-original-physical')
+ baseline = fleet.record('I05-successor-original-S3', probe.inventory(bucket))
+ require_exact_inventory_entry(baseline, bucket, entry)
+ require(entry['payload']['size'] >= MIN_BACKPRESSURE_BYTES,
+ 'NOT_OBSERVED: I05 successor target cannot maintain response backpressure')
+ require(physical_content(physical) == physical_content(fleet.snapshot('I05-successor-baseline-after-reads')),
+ 'I05 successor baseline reads changed original physical content')
+ body = payload(f"{fleet.run_id}/I05-queued-successor", SUCCESSOR_BYTES)
+ successor = SuccessorProbe(probe, marker)
+ recorder = Recorder(successor, output / 'successor')
+ future, final, target, after_ack = None, None, None, None
+ def capture(window, sent, admitted):
+ binding()
+ rows = window.capture()
+ node, holder = admitted['node'], admitted['attempt']
+ attempts = {host: successor_attempt(events, marker, holder, bucket, entry['key'])
+ for host, events in rows.items()}
+ require(all(value is None for host, value in attempts.items() if host != node),
+ 'I05 successor request appeared on another endpoint')
+ return rows[node], lock_observer.caller_attempt(rows[node], sent, bucket, entry['key']), attempts[node]
+ with ThreadPoolExecutor(max_workers=1) as pool:
+ def queued(sent, admitted, window):
+ nonlocal future
+ binding()
+ future = pool.submit(recorder.call, 'put_object', {'Bucket': bucket, 'Key': entry['key'],
+ 'ContentType': 'application/octet-stream', 'Metadata': {'heal-fixture': 'I05-queued-successor'}}, body)
+ deadline = time.monotonic() + QUEUE_SECONDS
+ while True:
+ _, holder, attempt = capture(window, sent, admitted)
+ require(holder is not None and holder['observedAcquiredWithoutDrop'],
+ 'NOT_OBSERVED: I05 original GET ended before successor queue observation')
+ if future.done():
+ future.result()
+ raise ValueError('NOT_OBSERVED: I05 successor completed before caller disconnect')
+ require(time.monotonic() <= deadline, 'NOT_OBSERVED: I05 successor queue observation exceeded its budget')
+ if attempt is not None:
+ require(attempt['states'] == ['acquiring'],
+ 'I05 successor acquired before caller disconnect')
+ attempt.update(clockDomain=CLOCK_DOMAIN, observedMonotonicNs=time.monotonic_ns())
+ save(output / 'queued.json', attempt)
+ return attempt
+ time.sleep(.02)
+ def complete(sent, admitted, window):
+ nonlocal final, target, after_ack
+ require(sent.get('clientSocketClosed') is True and sent.get('shutdownCompleted') is True
+ and sent.get('resultUnknown') is True, 'I05 successor lost the original caller disconnect')
+ deadline = time.monotonic() + COMPLETE_SECONDS
+ while True:
+ rows, holder, attempt = capture(window, sent, admitted)
+ require(holder is not None, 'I05 successor lost its original GET request')
+ require(time.monotonic() <= deadline, 'NOT_OBSERVED: I05 successor completion exceeded its budget')
+ if future.done():
+ future.result()
+ if attempt is not None and attempt['states'] == ['acquiring', 'acquired', 'guard_dropped']:
+ require(holder['diagnosticGuardDropped'], 'I05 successor acquired without original GET guard drop')
+ drop = next(i for i, row in enumerate(rows) if row is holder['events'][-1])
+ acquire = next(i for i, row in enumerate(rows) if row is attempt['events'][1])
+ require(drop < acquire, 'I05 successor acquired before original GET diagnostic release')
+ added = validate_successor_ack(recorder.output, probe, bucket, entry['key'], body, attempt)
+ require(not any(e['versionId'] == added['versionId'] for e in baseline['entries']),
+ 'I05 successor ACK reused an original version')
+ final = copy.deepcopy(baseline)
+ for item in final['entries']:
+ if item['key'] == entry['key']:
+ item['isLatest'] = False
+ final['entries'].append(added)
+ final['entries'].sort(key=lambda item: (item['key'], item['versionId']))
+ after = fleet.snapshot('I05-successor-after-ACK-before-public-read')
+ after_ack = physical_content(after)
+ target = physical_append(physical, after, bucket, baseline, added)
+ save(output / 'successor-complete.json', {**attempt, 'added': added,
+ 'clockDomain': CLOCK_DOMAIN, 'observedMonotonicNs': time.monotonic_ns(),
+ 'queuedBeforeDisconnectObserved': True, 'originalDiagnosticDropBeforeAcquire': True,
+ 'heldAtDisconnectProven': False, 'canonicalCasePassClaimed': False})
+ return attempt
+ time.sleep(.05)
+ try:
+ caller = lock_observer.observe_disconnect(probe, fleet, bucket, entry, output / 'caller',
+ while_admitted=queued, after_disconnect=complete, receive_buffer_bytes=CALLER_RECEIVE_BUFFER_BYTES)
+ before_reads = fleet.snapshot('I05-successor-before-all-peer-reads')
+ require(after_ack == physical_content(before_reads),
+ 'I05 successor first historical verification read changed physical content')
+ inventories = {}
+ for endpoint in probe.binding['endpoints']:
+ binding()
+ peer = Probe(probe.binding, endpoint, credential_pair=probe.credential_pair)
+ observed = fleet.record('I05-successor-all-peer-S3', {'endpoint': endpoint, **peer.inventory(bucket)})
+ retained = [item for item in final['entries'] if item['versionId'] != target['versionId']]
+ actual_retained = {**observed, 'entries': [item for item in observed['entries']
+ if item['versionId'] != target['versionId']]}
+ verify_seed(retained, actual_retained)
+ wanted = next(item for item in final['entries'] if item['versionId'] == target['versionId'])
+ actual = require_exact_inventory_entry(observed, bucket, wanted)
+ require(actual.get('isLatest') is True and actual.get('tags') == []
+ and all(actual.get('properties', {}).get(name) == value
+ for name, value in wanted['properties'].items()),
+ 'I05 successor public content, metadata, tags or current version differs from its ACK')
+ inventories[endpoint] = observed
+ require(physical_content(before_reads) == physical_content(fleet.snapshot('I05-successor-after-all-peer-reads')),
+ 'I05 successor verification reads changed physical content')
+ deadline = time.monotonic() + lock_observer.DRAIN_SECONDS
+ while True:
+ binding()
+ resources = {node: fleet.record('I05-successor-final-resources', {
+ 'node': node, **fleet.remote(node, 'process-metrics')}) for node in fleet.nodes}
+ for node, resource in resources.items():
+ lock_observer.same_process(resource.get('identity'), caller['baselineResources'][node]['identity'])
+ require(type(resource.get('fileDescriptors')) is int and resource['fileDescriptors'] >= 0,
+ 'I05 successor final descriptor census is incomplete')
+ require(time.monotonic() <= deadline, 'NOT_OBSERVED: I05 successor final resource budget expired')
+ if all(resources[node]['fileDescriptors'] <= caller['baselineResources'][node]['fileDescriptors']
+ for node in fleet.nodes):
+ break
+ time.sleep(.2)
+ result = {'diagnosticComplete': True, 'callerObservation': caller, 'expectedEntries': final['entries'],
+ 'verifiedEndpoints': list(inventories), 'finalResources': resources,
+ 'heldAtDisconnectProven': False, 'distributedReleaseProven': False,
+ 'noGlobalFdLeakProven': False, 'wholeCasePassClaimed': False, 'canonicalCasePassClaimed': False}
+ save(output / 'diagnostic.json', result)
+ return result
+ except BaseException as error:
+ save(output / 'failure.json', {'diagnosticComplete': False, 'errorType': type(error).__name__,
+ 'state': 'NOT_OBSERVED' if str(error).startswith('NOT_OBSERVED:') else 'INCOMPLETE',
+ 'canonicalCasePassClaimed': False, 'originalRequestsAndResponsesRetained': True})
+ raise
+ finally:
+ # The original caller is closed by observe_disconnect even on an
+ # observation failure. Drain the single SDK call before capture/stop.
+ if future is not None:
+ try:
+ future.result()
+ except BaseException:
+ pass
diff --git a/scripts/validation_heal/lock_timeout.py b/scripts/validation_heal/lock_timeout.py
new file mode 100644
index 00000000..fea35333
--- /dev/null
+++ b/scripts/validation_heal/lock_timeout.py
@@ -0,0 +1,336 @@
+#!/usr/bin/env python3
+"""I05 real PUT lock timeout behind an observed GET; diagnostic subset only."""
+
+import argparse
+import hashlib
+import json
+from pathlib import Path
+import re
+import time
+import uuid
+
+try:
+ from . import lock_observer
+ from .heal_probe import CLOCK_DOMAIN, MAX_BODY, Probe, canonical, compare_inventories, payload, require, save, validate_binding
+ from .heal_workload import Recorder
+ from .healthy import Fleet, physical_content
+except ImportError:
+ import lock_observer
+ from heal_probe import CLOCK_DOMAIN, MAX_BODY, Probe, canonical, compare_inventories, payload, require, save, validate_binding
+ from heal_workload import Recorder
+ from healthy import Fleet, physical_content
+
+
+OBSERVATION_SECONDS = 10
+CONTENDER_BYTES = 257 * 1024 + 123
+MIN_BACKPRESSURE_BYTES = 32 * 1024**2
+CALLER_RECEIVE_BUFFER_BYTES = 64 * 1024
+
+
+def require_exact_inventory_entry(inventory, bucket, entry):
+ require(isinstance(inventory, dict), 'I05 timeout requires an S3 inventory object')
+ entries = inventory.get('entries')
+ pagination = inventory.get('pagination')
+ require(inventory.get('bucket') == bucket and isinstance(entries, list) and entries
+ and all(isinstance(row, dict) and isinstance(row.get('key'), str) and row['key']
+ and isinstance(row.get('versionId'), str) and row['versionId']
+ and type(row.get('deleteMarker')) is bool and type(row.get('isLatest')) is bool
+ for row in entries), 'I05 timeout inventory has incomplete version identities')
+ identities = [(row['key'], row['versionId']) for row in entries]
+ require(len(set(identities)) == len(entries)
+ and all(sum(row['isLatest'] for row in entries if row['key'] == key) == 1
+ for key in {row['key'] for row in entries}),
+ 'I05 timeout inventory has duplicate versions or incomplete latest generations')
+ for row in entries:
+ content = row.get('payload')
+ require(content is None if row['deleteMarker'] else
+ isinstance(content, dict) and set(content) == {'size', 'sha256'}
+ and type(content.get('size')) is int and 0 <= content['size'] <= MAX_BODY
+ and isinstance(content.get('sha256'), str) and re.fullmatch(r'[0-9a-f]{64}', content['sha256']),
+ 'I05 timeout inventory lacks complete bounded payload digests')
+ require(isinstance(pagination, dict)
+ and all(isinstance(pagination.get(name), list) and pagination[name]
+ and all(type(size) is int and 0 <= size <= 1000 for size in pagination[name])
+ for name in ('versionPageSizes', 'currentPageSizes')),
+ 'I05 timeout inventory lacks complete typed pagination')
+ require(type(inventory.get('objectsAndMarkers')) is int
+ and inventory['objectsAndMarkers'] == len(entries)
+ and type(inventory.get('bytesRead')) is int
+ and inventory['bytesRead'] == sum(row.get('payload', {}).get('size', 0)
+ for row in entries if not row.get('deleteMarker'))
+ and sum(pagination['versionPageSizes']) == len(entries)
+ and sum(pagination['currentPageSizes']) == sum(
+ bool(row.get('isLatest') and not row.get('deleteMarker')) for row in entries),
+ 'I05 timeout requires a structurally complete cross-checked S3 inventory')
+ matches = [row for row in entries if row.get('key') == entry.get('key')
+ and row.get('versionId') == entry.get('versionId')]
+ require(len(matches) == 1 and matches[0].get('deleteMarker') is False
+ and matches[0].get('payload') == entry.get('payload'),
+ 'I05 timeout target is not the exact content verified in its complete S3 inventory')
+ return matches[0]
+
+
+def validate_timeout_ack(ack, request, request_bytes, run_id, endpoint_name, bucket, key, body,
+ *, clock_domain=CLOCK_DOMAIN):
+ expected_params = {'Bucket': bucket, 'Key': key,
+ 'Metadata': {'heal-fixture': 'I05-timeout-contender'}}
+ expected_payload = {'size': len(body), 'sha256': hashlib.sha256(body).hexdigest()}
+ require(ack.get('schemaVersion') == 2 and ack.get('runId') == run_id
+ and ack.get('sequence') == 1 and ack.get('requestFile') == '000001-request.json'
+ and ack.get('requestSha256') == hashlib.sha256(request_bytes).hexdigest()
+ and ack.get('operationCheck') == 'PASS'
+ and ack.get('casePassClaimed') is False
+ and request.get('schemaVersion') == 2 and request.get('runId') == run_id
+ and request.get('sequence') == 1 and request.get('operation') == 'put_object'
+ and request.get('params') == expected_params and request.get('identity') is None
+ and request.get('expectHttp') == 503
+ and request.get('casePassClaimed') is False
+ and request.get('payload') == expected_payload
+ and ack.get('clockDomain') == request.get('clockDomain') == clock_domain,
+ 'I05 timeout ACK is not durably bound to the exact contender PUT')
+ observation = ack.get('observation')
+ require(isinstance(observation, dict) and observation.get('endpoint') == endpoint_name
+ and observation.get('operation') == 'put_object' and observation.get('params') == expected_params
+ and observation.get('payload') == expected_payload and observation.get('clockDomain') == clock_domain
+ and observation.get('clientCallInvoked') is True
+ and observation.get('acknowledged') is False and observation.get('resultUnknown') is False
+ and observation.get('clientCleanupError') is None
+ and type(observation.get('callStartedMonotonicNs')) is int
+ and type(observation.get('callFinishedMonotonicNs')) is int
+ and all(type(value) is int and value > 0 for value in (
+ request.get('preparedMonotonicNs'), ack.get('collectorCallStartedMonotonicNs'),
+ ack.get('collectorCallFinishedMonotonicNs'), ack.get('recordedMonotonicNs')))
+ and request['preparedMonotonicNs'] <= ack['collectorCallStartedMonotonicNs']
+ <= observation['callStartedMonotonicNs'] <= observation['callFinishedMonotonicNs']
+ <= ack['collectorCallFinishedMonotonicNs'] <= ack['recordedMonotonicNs'],
+ 'I05 timeout PUT has an unknown transport result or incomplete durable refusal')
+ response = observation.get('response')
+ require(isinstance(response, dict), 'I05 timeout PUT ACK lacks its structured refusal')
+ return response
+
+
+def timeout_proof(rows, response, holder, bucket, key):
+ """Reconcile one structured S3 refusal with its actual write-lock attempt."""
+ metadata = response.get('ResponseMetadata', {})
+ request_id = metadata.get('RequestId')
+ # Exact candidate error.rs attaches this header to typed namespace timeouts.
+ require(metadata.get('HTTPStatusCode') == 503 and type(metadata.get('RetryAttempts')) is int
+ and metadata['RetryAttempts'] == 0
+ and response.get('Error', {}).get('Code') == 'ServiceUnavailable'
+ and metadata.get('HTTPHeaders', {}).get('retry-after') == '1'
+ and isinstance(request_id, str) and str(uuid.UUID(request_id)) == request_id and uuid.UUID(request_id).int
+ and request_id != holder['request_id'] and response.get('VersionId') is None,
+ 'I05 contender lacks an exact structured lock-timeout refusal')
+ request_rows = [row for row in rows if row.get('target') == lock_observer.TARGET
+ and row.get('request_id') == request_id]
+ require(all(row.get('mode') in ('read', 'write') for row in request_rows),
+ 'I05 contender emitted an unknown namespace lock mode')
+ read_rows = [row for row in request_rows if row.get('mode') == 'read']
+ write_rows = [row for row in request_rows if row.get('mode') == 'write']
+ selected = [row for row in write_rows if row.get('op') == 'put_object_commit'
+ and row.get('bucket') == bucket and row.get('object') == key]
+ require(len(selected) == len(write_rows),
+ 'I05 contender made another namespace write-lock attempt')
+ if read_rows:
+ require([row.get('state') for row in read_rows] == ['acquiring', 'acquired', 'guard_dropped']
+ and len({(row.get('owner'), row.get('namespace'), row.get('lock_attempt_id'))
+ for row in read_rows}) == 1,
+ 'I05 contender bucket-incarnation read lock has an incomplete lifecycle')
+ for row in read_rows:
+ require(row.get('event') == 'object_namespace_lock' and row.get('component') == 'ecstore'
+ and row.get('subsystem') == 'object_lock' and row.get('op') == 'get_object'
+ and row.get('bucket') == '.rustfs.sys'
+ and row.get('object') == f'buckets/{bucket}/.bucket-incarnation'
+ and row.get('namespace') == holder['namespace'] and row.get('owner') == holder['owner']
+ and row.get('requested_version_id') is None
+ and type(row.get('timeout_ms')) is int and row['timeout_ms'] == 1000
+ and type(row.get('acquire_ms')) is int and row['acquire_ms'] >= 0
+ and isinstance(row.get('lock_attempt_id'), str)
+ and str(uuid.UUID(row['lock_attempt_id'])) == row['lock_attempt_id']
+ and uuid.UUID(row['lock_attempt_id']).int,
+ 'I05 contender bucket-incarnation read lock changed identity or budget')
+ if not selected:
+ return None
+ require([row.get('state') for row in selected] in (['acquiring'], ['acquiring', 'failed']),
+ 'I05 contender acquired a lock or lost its exact timeout lifecycle')
+ for row in selected:
+ require(row.get('event') == 'object_namespace_lock' and row.get('component') == 'ecstore'
+ and row.get('subsystem') == 'object_lock' and row.get('op') == 'put_object_commit'
+ and row.get('mode') == 'write' and row.get('bucket') == bucket and row.get('object') == key
+ and row.get('namespace') == holder['namespace'] and row.get('requested_version_id') is None
+ and type(row.get('timeout_ms')) is int and row['timeout_ms'] == 1000
+ and type(row.get('acquire_ms')) is int and row['acquire_ms'] >= 0
+ # new_ns_lock reuses this SetDisks owner for same-node requests;
+ # the server request and attempt UUIDs distinguish contenders.
+ and row.get('owner') == holder['owner']
+ and isinstance(row.get('lock_attempt_id'), str)
+ and str(uuid.UUID(row['lock_attempt_id'])) == row['lock_attempt_id']
+ and uuid.UUID(row['lock_attempt_id']).int and row['lock_attempt_id'] != holder['lock_attempt_id'],
+ 'I05 contender has another lock budget, object, namespace or owner')
+ http_spans = [span for span in [row.get('span'), *row.get('spans', [])]
+ if isinstance(span, dict) and span.get('name') == 'http-request']
+ if http_spans:
+ unique_http_spans = {canonical(span): span for span in http_spans}
+ require(len(unique_http_spans) == 1 and all(
+ span.get('request_id') == request_id and span.get('method') == 'PUT'
+ and span.get('uri') == f'/{bucket}/{key}' for span in unique_http_spans.values()),
+ 'I05 contender lock attempt is not bound to its exact PUT request')
+ require(len({(row['owner'], row['lock_attempt_id']) for row in selected}) == 1,
+ 'I05 contender changed its original write owner')
+ if len(selected) == 1:
+ return None
+ start, end = selected
+ require(start.get('level') == 'TRACE' and end.get('level') == 'ERROR' and end.get('failure') == 'timeout'
+ and type(end.get('acquire_ms')) is int and end['acquire_ms'] >= end['timeout_ms'],
+ 'I05 contender did not observe actual contention beyond its registered budget')
+ return {'requestId': request_id, 'lockAttemptId': end['lock_attempt_id'], 'owner': end['owner'],
+ 'namespace': end['namespace'], 'events': selected, 'typedTimeoutBeyondBudgetObserved': True,
+ 'writeLockAcquired': False, 'canonicalCasePassClaimed': False}
+
+
+def observe_timeout(probe, fleet, bucket, entry, output):
+ """Use existing owned service and fixtures; preserve every failed request."""
+ lock_observer.validate_scope(probe, fleet, bucket, entry)
+ output = Path(output)
+ output.mkdir(exist_ok=False)
+ bound = canonical(probe.binding)
+ save(output / 'scope.json', {'runId': fleet.run_id, 'entry': entry,
+ 'candidateCommit': probe.binding['candidateCommit'],
+ 'bindingSha256': hashlib.sha256(bound.encode()).hexdigest(),
+ 'minimumCallerPayloadBytes': MIN_BACKPRESSURE_BYTES,
+ 'receiveBufferBytesRequested': CALLER_RECEIVE_BUFFER_BYTES,
+ 'canonicalCasePassClaimed': False})
+ try:
+ return _observe_timeout(probe, fleet, bucket, entry, output, bound)
+ except BaseException as error:
+ save(output / 'failure.json', {'diagnosticComplete': False, 'errorType': type(error).__name__,
+ 'state': 'NOT_OBSERVED' if str(error).startswith('NOT_OBSERVED:') else 'INCOMPLETE',
+ 'canonicalCasePassClaimed': False, 'originalRequestsAndResponsesRetained': True})
+ raise
+
+
+def _observe_timeout(probe, fleet, bucket, entry, output, bound):
+ def binding():
+ validate_binding(probe.binding)
+ require(canonical(probe.binding) == bound, 'I05 timeout binding changed during observation')
+ def unchanged_physical(label):
+ binding()
+ after = fleet.snapshot(label)
+ require(physical_content(after) == physical_content(physical),
+ 'I05 rejected timeout PUT changed an original physical file or object generation')
+ return {'originalFilesUnchanged': True, 'canonicalCasePassClaimed': False}
+ physical = fleet.snapshot('I05-timeout-original-physical')
+ before = fleet.record('I05-timeout-original-S3', probe.inventory(bucket))
+ target = require_exact_inventory_entry(before, bucket, entry)
+ require(target['payload']['size'] >= MIN_BACKPRESSURE_BYTES,
+ 'NOT_OBSERVED: I05 timeout target is too small to maintain response backpressure')
+ unchanged_physical('I05-timeout-baseline-physical-after-public-read')
+ recorder = Recorder(probe, output / 'contender')
+ body = payload(f'{probe.binding["runId"]}/I05-timeout-contender', CONTENDER_BYTES)
+ def contend(sent, admitted, window):
+ binding()
+ node, holder = admitted['node'], admitted['attempt']
+ recorder.call('put_object', {'Bucket': bucket, 'Key': entry['key'],
+ 'Metadata': {'heal-fixture': 'I05-timeout-contender'}}, body, expect_http=503)
+ ack_path = recorder.output / '000001-ack.json'
+ request_path = recorder.output / '000001-request.json'
+ request_bytes = request_path.read_bytes()
+ response = validate_timeout_ack(json.loads(ack_path.read_text()),
+ json.loads(request_bytes), request_bytes, probe.binding['runId'], probe.endpoint_name,
+ bucket, entry['key'], body)
+ deadline = time.monotonic() + OBSERVATION_SECONDS
+ while True:
+ binding()
+ all_rows = window.capture()
+ rows = all_rows[node]
+ current = lock_observer.caller_attempt(rows, sent, bucket, entry['key'])
+ require(current is not None and current['observedAcquiredWithoutDrop']
+ and all(current[name] == holder[name] for name in ('request_id', 'lock_attempt_id', 'owner', 'namespace')),
+ 'NOT_OBSERVED: I05 original GET owner ended before contention was observed')
+ foreign_request = [row for host, host_rows in all_rows.items() if host != node
+ for row in host_rows if row.get('request_id') == response.get(
+ 'ResponseMetadata', {}).get('RequestId')]
+ require(not foreign_request, 'I05 contender request ID appeared on a foreign endpoint')
+ proof = timeout_proof(rows, response, holder, bucket, entry['key'])
+ require(time.monotonic() <= deadline, 'NOT_OBSERVED: I05 contender timeout observation exceeded its budget')
+ if proof is not None:
+ socket_state = fleet.record('I05-timeout-caller-backpressure-socket', {
+ 'node': node, **fleet.remote(node, 'caller-socket',
+ expectedProcess=admitted['socket']['process'],
+ clientAddress=sent['localAddress'], serverAddress=sent['peerAddress'])})
+ samples = socket_state.get('samples', [])
+ require(socket_state.get('readOnly') is True
+ and socket_state.get('process') == admitted['socket']['process']
+ and socket_state.get('clockDomain') == admitted['socket']['clockDomain']
+ and socket_state.get('networkNamespace') == admitted['socket']['networkNamespace']
+ and len(samples) == 2
+ and all(len(sample.get('ownedMatches', [])) == 1
+ and sample['ownedMatches'][0].get('state') == '01'
+ and type(sample['ownedMatches'][0].get('txQueueBytes')) is int
+ and sample['ownedMatches'][0]['txQueueBytes'] > 0
+ and sample['vanishedDescriptors'] == 0 for sample in samples),
+ 'NOT_OBSERVED: timed-out PUT did not overlap a backpressured original GET socket')
+ current = lock_observer.caller_attempt(window.capture()[node], sent, bucket, entry['key'])
+ require(current is not None and current['observedAcquiredWithoutDrop']
+ and all(current[name] == holder[name] for name in (
+ 'request_id', 'lock_attempt_id', 'owner', 'namespace')),
+ 'NOT_OBSERVED: original GET ended while sampling post-timeout backpressure')
+ require(time.monotonic() <= deadline,
+ 'NOT_OBSERVED: I05 backpressure census exceeded its observation budget')
+ proof['backpressureSocket'] = socket_state
+ proof['readResponseBackpressureObservedAfterTimeout'] = True
+ proof.update(clockDomain=CLOCK_DOMAIN, observedMonotonicNs=time.monotonic_ns())
+ save(output / 'timeout.json', proof)
+ return proof
+ time.sleep(.1)
+ caller = lock_observer.observe_disconnect(probe, fleet, bucket, entry, output / 'caller',
+ while_admitted=contend, receive_buffer_bytes=CALLER_RECEIVE_BUFFER_BYTES,
+ before_verification=lambda: unchanged_physical('I05-timeout-final-physical-before-public-read'))
+ binding()
+ public = fleet.record('I05-timeout-final-S3', probe.inventory(bucket))
+ require_exact_inventory_entry(public, bucket, entry)
+ agreement = compare_inventories(before, public)
+ unchanged_physical('I05-timeout-final-physical-after-public-read')
+ deadline = time.monotonic() + lock_observer.DRAIN_SECONDS
+ while True:
+ binding()
+ final_resources = {}
+ for node in fleet.nodes:
+ value = fleet.record('I05-timeout-final-resources', {
+ 'node': node, **fleet.remote(node, 'process-metrics')})
+ lock_observer.same_process(value.get('identity'), caller['finalResources'][node]['identity'])
+ require(type(value.get('fileDescriptors')) is int and value['fileDescriptors'] >= 0,
+ 'I05 final timeout descriptor census is incomplete')
+ final_resources[node] = value
+ require(time.monotonic() <= deadline, 'NOT_OBSERVED: I05 final timeout FD census exceeded its budget')
+ if all(value['fileDescriptors'] <= caller['baselineResources'][node]['fileDescriptors']
+ for node, value in final_resources.items()):
+ break
+ time.sleep(.2)
+ result = {'diagnosticComplete': True, 'callerObservation': caller, 'inventoryAgreement': agreement,
+ 'finalResources': final_resources, 'rejectedWriteChangedOriginalFiles': False,
+ 'timeoutContenderAcknowledged': False, 'canonicalCasePassClaimed': False, 'wholeCasePassClaimed': False,
+ 'remaining': ['same-source native exact cancellation/timeout/stale fences',
+ 'complete source DL/DV workload and H1/H2 final ACK/physical/lock/FD closure']}
+ save(output / 'diagnostic.json', result)
+ return result
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--binding', type=Path, required=True)
+ parser.add_argument('--nodes', nargs='+', required=True)
+ parser.add_argument('--bucket', required=True)
+ parser.add_argument('--entry', type=Path, required=True)
+ parser.add_argument('--output', type=Path, required=True)
+ parser.add_argument('--endpoint')
+ args = parser.parse_args()
+ probe = Probe(json.loads(args.binding.read_text()), args.endpoint)
+ args.output.mkdir(parents=True, exist_ok=False)
+ fleet = Fleet(args.nodes, args.output, probe.binding['runId'], runtime_profile='lock-timeout')
+ fleet.secret_values.update(probe.credentials())
+ observe_timeout(probe, fleet, args.bucket, json.loads(args.entry.read_text()), args.output / 'timeout')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/scripts/validation_heal/lock_timeout_runner.py b/scripts/validation_heal/lock_timeout_runner.py
new file mode 100644
index 00000000..5f6490f6
--- /dev/null
+++ b/scripts/validation_heal/lock_timeout_runner.py
@@ -0,0 +1,310 @@
+#!/usr/bin/env python3
+"""Run I05 source procedures and retain failures pending independent live audit."""
+
+import argparse
+from datetime import datetime, timedelta, timezone
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import secrets
+import signal
+import subprocess
+import sys
+import time
+
+try:
+ from . import lock_source
+ from .heal_probe import Probe, compare_inventories, require, save, validate_binding
+ from .heal_workload import Recorder, seed, verify_seed
+ from .healthy import Fleet, physical_content, validate_topology
+ from .inspector import verify as verify_inspector
+ from .lock_observer import CANDIDATE, DRAIN_SECONDS, same_process
+ from .lock_timeout import MIN_BACKPRESSURE_BYTES, observe_timeout
+ from .lock_successor import observe_successor
+ from .race_native_evidence import verify as verify_native
+ from .shards import map_target, verify_cohort
+ from .timeout_retention import seal_successor_scene
+except ImportError:
+ import lock_source
+ from heal_probe import Probe, compare_inventories, require, save, validate_binding
+ from heal_workload import Recorder, seed, verify_seed
+ from healthy import Fleet, physical_content, validate_topology
+ from inspector import verify as verify_inspector
+ from lock_observer import CANDIDATE, DRAIN_SECONDS, same_process
+ from lock_timeout import MIN_BACKPRESSURE_BYTES, observe_timeout
+ from lock_successor import observe_successor
+ from race_native_evidence import verify as verify_native
+ from shards import map_target, verify_cohort
+ from timeout_retention import seal_successor_scene
+
+
+NODES = ('vm000', 'vm001', 'vm002', 'vm003')
+CASE_ID = 'heal.lock-timeout-and-caller-abort'
+NATIVE_I05_LABELS = ('versioned-read-repair-lock-owner', 'cancelled-physical-unlink-owner',
+ 'stale-read-repair-final-rename')
+
+
+def interrupted(signum, frame):
+ raise KeyboardInterrupt(f'I05 diagnostic interrupted by signal {signum}')
+
+
+def preflight(args):
+ """Validate immutable local inputs before reserving or starting a node."""
+ candidate = json.loads(args.candidate.read_text())
+ require(candidate.get('source_sha') == CANDIDATE,
+ 'I05 timeout diagnostic requires the exact reviewed candidate')
+ require(re.fullmatch(r'[0-9a-f]{64}', candidate.get('package_sha256', '')),
+ 'I05 package digest is missing')
+ if args.binary_sha is not None:
+ require(re.fullmatch(r'[0-9a-f]{64}', args.binary_sha), 'I05 binary digest is missing')
+ require(re.fullmatch(r'[0-9a-f]{40}', args.testing_commit), 'I05 requires an immutable helper commit')
+ root = Path(__file__).resolve().parents[2]
+ head = subprocess.run(['git', '-C', str(root), 'rev-parse', 'HEAD'],
+ capture_output=True, text=True, check=True).stdout.strip()
+ require(head == args.testing_commit, 'I05 helper differs from the selected checkout')
+ changes = subprocess.run(['git', '-C', str(root), 'status', '--porcelain', '--untracked-files=normal',
+ '--', 'scripts/validation_heal'], capture_output=True, text=True, check=True)
+ require(not changes.stdout.strip(), 'I05 diagnostic source differs from its immutable helper commit')
+ for name in ('GITHUB_RUN_ID', 'GITHUB_RUN_ATTEMPT'):
+ value = os.environ.get(name, '1' if name == 'GITHUB_RUN_ATTEMPT' else '')
+ require(re.fullmatch(r'[1-9][0-9]*', value), 'I05 requires the owning workflow identity')
+ nodes = (os.environ.get('RUSTFS_NODES') or ' '.join(NODES)).split()
+ require(nodes[:4] == list(NODES), 'I05 requires the current vm000-vm003 Heal node binding')
+ inspector = verify_inspector(args.tool_directory, candidate)
+ require(args.rc_bin.is_file(), 'I05 requires the pinned client binary')
+ if getattr(args, 'diagnostic_branch', 'timeout') == 'source-plan':
+ lock_source.verify_source_native(args, candidate)
+ elif getattr(args, 'diagnostic_branch', 'timeout') == 'queued-successor':
+ verify_native_prerequisite(args, candidate)
+ return candidate, inspector
+
+
+def verify_native_prerequisite(args, candidate):
+ directory = getattr(args, 'native_directory', None)
+ require(directory is not None, 'I05 queued-successor requires immutable exact-source native evidence')
+ proof = verify_native(directory, candidate, args.testing_commit, CASE_ID)
+ require(proof.get('requiredRegressionLabels') == list(NATIVE_I05_LABELS)
+ and proof.get('requiredNativeSubsetPassed') is True
+ and proof.get('wholeCasePassClaimed') is False
+ and proof.get('fleetExecutionClaimed') is False
+ and proof.get('canonicalCasePassClaimed') is False,
+ 'I05 queued-successor exact-source native prerequisite is incomplete')
+ return proof
+
+
+def run_topology(args, candidate, inspector, topology):
+ require(topology in ('H1', 'H2'), 'I05 timeout supports only H1/H2')
+ # Repeat the source/tool guard for callers which bypass main().
+ require(candidate.get('source_sha') == inspector.get('candidateCommit') == CANDIDATE,
+ 'I05 timeout candidate and inspector differ')
+ branch = getattr(args, 'diagnostic_branch', 'timeout')
+ require(branch in {'timeout', 'queued-successor'}, 'I05 diagnostic branch is not implemented')
+ if branch == 'queued-successor':
+ verify_native_prerequisite(args, candidate)
+ directory = args.evidence / topology
+ directory.mkdir(parents=True, exist_ok=False)
+ run_id = f"gh-{os.environ['GITHUB_RUN_ID']}-{os.environ.get('GITHUB_RUN_ATTEMPT', '1')}-{topology.lower()}"
+ if getattr(args, 'source_plan', False):
+ run_id += '-' + branch
+ nodes = list(NODES[:1] if topology == 'H1' else NODES)
+ bucket = 'rv-heal-' + hashlib.sha256(run_id.encode()).hexdigest()[:16]
+ fleet = Fleet(nodes, directory, run_id, args.tool_directory,
+ runtime_profile='lock-successor' if branch == 'queued-successor' else 'lock-timeout')
+ access, secret = secrets.token_hex(16), secrets.token_hex(24)
+ fleet.secret_values.update((access, secret))
+ def error_info(error):
+ message = str(error)
+ for value in (access, secret):
+ message = message.replace(value, '[REDACTED]')
+ return {'type': type(error).__name__, 'message': message}
+ binding = {'schemaVersion': 1, 'runId': run_id, 'jumpHostOnly': True, 'exclusiveHealService': True,
+ 'leaseExpiresAt': (datetime.now(timezone.utc) + timedelta(minutes=30)).isoformat(),
+ 'bucketPrefix': bucket, 'endpoints': {f'node{i}': f'http://rustfs-node{i}:9000'
+ for i in range(1, len(nodes) + 1)}, 'defaultEndpoint': 'node1',
+ 'candidateCommit': candidate['source_sha'], 'harnessCommit': args.testing_commit,
+ 'candidatePackageSha256': candidate['package_sha256'], 'candidateBinarySha256': args.binary_sha,
+ 'rcBinarySha256': hashlib.sha256(args.rc_bin.read_bytes()).hexdigest(),
+ 'maxInventoryObjects': 32, 'maxInventoryBytes': 512 * 1024**2}
+ validate_binding(binding)
+ fleet.record('binding', binding)
+ fleet.record('inspector-build', inspector)
+ probe = Probe(binding, credential_pair=(access, secret))
+ result = {'topology': topology, 'branch': branch, 'diagnosticComplete': False, 'cleanup': 'PENDING',
+ 'wholeCasePassClaimed': False, 'canonicalCasePassClaimed': False}
+ retained_scene = None
+ try:
+ fleet.deploy(args.binary_sha, bucket, access, secret)
+ fleet.record('actual-topology', validate_topology(fleet.snapshot('deployed-topology'), topology))
+ recorder = Recorder(probe, directory / 'fixture')
+ # DV history plus one concurrent, unretried DL request/ACK ledger.
+ # Large historical GETs make real timeout/disconnect contention observable.
+ expected = seed(recorder, bucket, 'DV', 2, [MIN_BACKPRESSURE_BYTES, 1024])
+ target = next(row for row in expected if not row['deleteMarker'] and not row['isLatest']
+ and row['payload']['size'] >= MIN_BACKPRESSURE_BYTES)
+ baseline = fleet.record('original-ACK-bound-S3', probe.inventory(bucket))
+ fleet.record('original-ACK-proof', verify_seed(expected, baseline))
+ controller = observe_successor if branch == 'queued-successor' else observe_timeout
+ diagnostic = controller(probe, fleet, bucket, target, directory / branch)
+ require(diagnostic.get('diagnosticComplete') is True
+ and diagnostic.get('canonicalCasePassClaimed') is False
+ and diagnostic.get('wholeCasePassClaimed') is False,
+ 'I05 timeout did not complete its diagnostic-only contract')
+ physical = fleet.snapshot('all-peer-verification-before-public-reads')
+ for endpoint in binding['endpoints']:
+ peer = Probe(binding, endpoint, credential_pair=(access, secret))
+ observed = fleet.record('all-peer-S3', {'endpoint': endpoint, **peer.inventory(bucket)})
+ if branch == 'timeout':
+ fleet.record('all-peer-ACK-proof', {'endpoint': endpoint,
+ **verify_seed(expected, observed), **compare_inventories(baseline, observed)})
+ else:
+ # The successor controller retains exact public properties and
+ # separately verifies its new ACK before these outer reads.
+ require(endpoint in diagnostic['verifiedEndpoints'], 'I05 successor omitted a peer')
+ wanted = diagnostic['expectedEntries']
+ wanted = [{name: value for name, value in item.items() if name not in {'properties', 'tags'}}
+ for item in wanted]
+ fleet.record('all-peer-ACK-proof', {'endpoint': endpoint, **verify_seed(wanted, observed)})
+ require(physical_content(physical) == physical_content(fleet.snapshot('physical-after-all-peer-reads')),
+ 'I05 peer verification changed the diagnostic physical scene')
+ baseline_resources = diagnostic['callerObservation']['baselineResources']
+ require(set(baseline_resources) == set(nodes), 'I05 diagnostic omitted a peer resource baseline')
+ deadline = time.monotonic() + DRAIN_SECONDS
+ while True:
+ validate_binding(binding)
+ resources = {}
+ for node in nodes:
+ resources[node] = fleet.record('after-peer-verification-resources', {
+ 'node': node, **fleet.remote(node, 'process-metrics')})
+ same_process(resources[node].get('identity'), baseline_resources[node]['identity'])
+ require(type(resources[node].get('fileDescriptors')) is int
+ and resources[node]['fileDescriptors'] >= 0, 'I05 final peer FD census is incomplete')
+ require(time.monotonic() <= deadline, 'NOT_OBSERVED: I05 peer verification FD census exceeded its budget')
+ if all(resources[node]['fileDescriptors'] <= baseline_resources[node]['fileDescriptors'] for node in nodes):
+ break
+ time.sleep(.2)
+ if branch == 'queued-successor':
+ # Stream checksums only after all live ownership/FD observations.
+ # The stopped oracle must still be the ACK-bound pre-read scene.
+ fleet.stop()
+ stopped = fleet.snapshot('I05-successor-stopped-before-integrity', quiescent=True)
+ require(physical_content(physical) == physical_content(stopped),
+ 'I05 successor stopping the service changed the verified physical scene')
+ original_versions = {item['versionId'] for item in baseline['entries']}
+ added = [item for item in diagnostic['expectedEntries'] if item['versionId'] not in original_versions]
+ require(len(added) == 1 and added[0]['key'] == target['key']
+ and added[0]['isLatest'] is True and added[0]['deleteMarker'] is False,
+ 'I05 successor stopped verification lacks one exact new data version')
+ new_target = map_target(stopped, bucket, target['key'], 'data', 1, added[0]['versionId'])
+ fleet.record('I05-successor-new-shard-integrity', verify_cohort(fleet, new_target, 'I05-successor-shard'))
+ require(physical_content(stopped) == physical_content(
+ fleet.snapshot('I05-successor-stopped-after-integrity', quiescent=True)),
+ 'I05 successor streaming verification changed physical content')
+ if getattr(args, 'source_plan', False):
+ result['sourceEvidenceChain'] = fleet.record('I05-source-evidence-chain',
+ lock_source.link_fleet_evidence(directory, branch, binding, diagnostic))
+ result['diagnosticComplete'] = True
+ except (Exception, KeyboardInterrupt) as error:
+ result['failure'] = error_info(error)
+ finally:
+ if result['diagnosticComplete']:
+ try:
+ result['cleanup'] = 'PASS' if fleet.cleanup() else 'FAIL'
+ except (Exception, KeyboardInterrupt) as error:
+ result['cleanup'] = 'FAIL'
+ result['cleanupError'] = error_info(error)
+ else:
+ # A successful contender or uncertain seed must not be erased by
+ # namespace cleanup. Stop and retain the original files and ACKs.
+ result.update(cleanup='FAIL', namespacesRetained=True)
+ try:
+ if fleet.reserved:
+ fleet.stop()
+ retained_scene = fleet.snapshot('original-failure-scene', quiescent=True)
+ for node in fleet.reserved:
+ fleet.record('original-failure-logs', {'node': node, **fleet.remote(node, 'logs')})
+ except (Exception, KeyboardInterrupt) as error:
+ result['failureCaptureError'] = error_info(error)
+ fleet.record('diagnostic-result', result)
+ if branch == 'queued-successor' and retained_scene is not None and 'failureCaptureError' not in result:
+ try:
+ save(directory / 'successor-retention.json', seal_successor_scene(directory, binding, retained_scene))
+ except (Exception, KeyboardInterrupt) as error:
+ result['retentionCaptureError'] = error_info(error)
+ fleet.record('retention-capture-error', result['retentionCaptureError'])
+ return result
+
+
+def run(args):
+ require(args.topologies in (['H1'], ['H2'], ['H1', 'H2']), 'I05 topology order must be H1 then H2')
+ require(isinstance(args.binary_sha, str) and re.fullmatch(r'[0-9a-f]{64}', args.binary_sha),
+ 'I05 deployment requires the installed binary digest')
+ candidate, inspector = preflight(args)
+ branch = getattr(args, 'diagnostic_branch', 'timeout')
+ native_proof = (lock_source.verify_source_native(args, candidate) if branch == 'source-plan' else
+ verify_native_prerequisite(args, candidate) if branch == 'queued-successor' else None)
+ # run_case.sh creates this parent and opens test.log before invoking us.
+ # Reuse only those wrapper files; a prior topology or summary is a replay.
+ require(not args.evidence.is_symlink(), 'I05 evidence directory must not be a symlink')
+ args.evidence.mkdir(parents=True, exist_ok=True)
+ require(all(path.name in {'timeout-preflight.json', 'setup.log', 'test.log'}
+ and path.is_file() and not path.is_symlink() for path in args.evidence.iterdir()),
+ 'I05 requires fresh topology and summary evidence before deployment')
+ if branch == 'source-plan':
+ return lock_source.run_source_plan(args, candidate, inspector, run_topology, native_proof)
+ if native_proof is not None:
+ save(args.evidence / 'native-prerequisite.json', native_proof)
+ results = []
+ for topology in args.topologies:
+ result = run_topology(args, candidate, inspector, topology)
+ results.append(result)
+ if not result['diagnosticComplete'] or result['cleanup'] != 'PASS':
+ break
+ summary = {'diagnostics': results, 'requestedTopologies': args.topologies,
+ 'diagnosticBranch': getattr(args, 'diagnostic_branch', 'timeout'),
+ 'diagnosticComplete': len(results) == len(args.topologies)
+ and all(row['diagnosticComplete'] and row['cleanup'] == 'PASS' for row in results),
+ 'wholeCasePassClaimed': False, 'canonicalCasePassClaimed': False,
+ 'remaining': ['exact disconnect-edge ownership and complete native timeout/stale fences',
+ 'stale-generation controller and independent queued-successor live acceptance',
+ 'complete H1/H2 source DL/DV ACK, physical, lock and FD acceptance']}
+ save(args.evidence / 'timeout-diagnostic-summary.json', summary)
+ save(args.evidence / 'healthy-summary.json', {**summary, 'caseId': CASE_ID, 'complete': False,
+ 'diagnosticBranchesPassed': summary['diagnosticComplete'],
+ 'cleanup': 'PASS' if results and all(row['cleanup'] == 'PASS' for row in results) else 'FAIL'})
+ return summary
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ argv = sys.argv[1:]
+ preflight_only = argv[:1] == ['preflight']
+ if preflight_only:
+ argv = argv[1:]
+ parser.add_argument('--candidate', type=Path, required=True)
+ parser.add_argument('--case-id', choices=(CASE_ID,), default=CASE_ID)
+ parser.add_argument('--testing-commit', required=True)
+ parser.add_argument('--binary-sha', required=not preflight_only)
+ parser.add_argument('--tool-directory', type=Path, required=True)
+ parser.add_argument('--rc-bin', type=Path, required=True)
+ parser.add_argument('--evidence', type=Path, required=not preflight_only)
+ parser.add_argument('--topologies', nargs='+', choices=('H1', 'H2'), default=['H1', 'H2'])
+ parser.add_argument('--diagnostic-branch', choices=('timeout', 'queued-successor', 'source-plan'),
+ default=os.environ.get('I05_DIAGNOSTIC_BRANCH', 'source-plan'))
+ parser.add_argument('--native-directory', type=Path, default=os.environ.get('VALIDATION_HEAL_NATIVE_DIRECTORY'))
+ args = parser.parse_args(argv)
+ require(args.topologies in (['H1'], ['H2'], ['H1', 'H2']), 'I05 topology order must be H1 then H2')
+ if preflight_only:
+ preflight(args)
+ print(json.dumps({'preflightPassed': True, 'candidateCommit': CANDIDATE,
+ 'diagnosticBranch': args.diagnostic_branch,
+ 'nativePrerequisitePassed': args.diagnostic_branch in ('queued-successor', 'source-plan'),
+ 'diagnosticOnly': True, 'canonicalCasePassClaimed': False}))
+ return 0
+ signal.signal(signal.SIGTERM, interrupted)
+ return 0 if run(args)['diagnosticComplete'] else 1
+
+
+if __name__ == '__main__':
+ raise SystemExit(main())
diff --git a/scripts/validation_heal/manager_fixture.py b/scripts/validation_heal/manager_fixture.py
new file mode 100644
index 00000000..2cfcc8ea
--- /dev/null
+++ b/scripts/validation_heal/manager_fixture.py
@@ -0,0 +1,59 @@
+"""Consume hosted manager evidence as scoped canonical-case prerequisites."""
+
+import argparse
+import json
+from pathlib import Path
+import re
+
+try:
+ from . import manager_native as suite
+except ImportError:
+ import manager_native as suite
+
+
+SCOPES = {
+ "heal.checkpoint-and-mrf-corruption": (
+ "checkpoint-truncated", "checkpoint-future", "checkpoint-digest", "checkpoint-stale-manager",
+ "checkpoint-page-order", "mrf-payload-integrity", "mrf-manifest-write-failure", "mrf-payload-write-failure",
+ "mrf-migration-boundaries", "root-uncertain-publication"),
+ "heal.bucket-incarnation-recreation": (
+ "incarnation-root-replay", "incarnation-deferred-metadata", "incarnation-checkpoint"),
+ "heal.outcome-counters-and-error-truth": ("outcome-truth", "outcome-wrong-identity"),
+}
+
+
+def verify(directory, candidate, case_id):
+ suite.native.require(case_id in SCOPES, "manager native suite has no reviewed scope for this canonical case")
+ directory = Path(directory)
+ api = json.loads((directory / "tool-run.json").read_text())
+ suite.native.require(type(api.get("id")) is int and api["id"] > 0
+ and type(api.get("run_attempt")) is int and api["run_attempt"] > 0
+ and api.get("status") == "completed" and api.get("conclusion") == "success"
+ and isinstance(api.get("head_sha"), str) and re.fullmatch(r"[0-9a-f]{40}", api["head_sha"])
+ and api.get("path") in {".github/workflows/rustfs-functional-chain.yml", ".github/workflows/rustfs-validation-tools.yml"},
+ "manager artifact has no exact successful hosted tools build attempt")
+ tools, evidence = directory / "manager-tools", directory / "manager-evidence"
+ manifest = json.loads((tools / "manager-native.json").read_text())
+ proof = suite.verify_evidence(tools, evidence, candidate, api["id"], api["run_attempt"], manifest.get("testingCommit"))
+ scoped = []
+ for key in SCOPES[case_id]:
+ value = suite.REGRESSIONS[key]
+ suite.native.require(value["relatedCases"] == [case_id], "manager native mapping changed canonical scope")
+ scoped.append({"regression": key, **value})
+ return {**proof, "caseId": case_id, "runId": api["id"], "runAttempt": api["run_attempt"],
+ "driverCommit": api["head_sha"], "collectorCommit": manifest["testingCommit"],
+ "casePrerequisiteTests": scoped, "originalEvidenceDirectory": str(evidence.resolve()),
+ "individualFsyncHooksClaimed": False, "physicalPowerLossClaimed": False,
+ "emittedBarrierLogsClaimed": False, "canonicalFleetBranchesExecuted": False}
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--directory", type=Path, required=True)
+ parser.add_argument("--candidate", type=Path, required=True)
+ parser.add_argument("--case-id", choices=tuple(SCOPES), required=True)
+ args = parser.parse_args()
+ print(json.dumps(verify(args.directory, json.loads(args.candidate.read_text()), args.case_id), indent=2))
+
+
+if __name__ == "__main__": main()
diff --git a/scripts/validation_heal/manager_native.py b/scripts/validation_heal/manager_native.py
new file mode 100644
index 00000000..8d6ded80
--- /dev/null
+++ b/scripts/validation_heal/manager_native.py
@@ -0,0 +1,276 @@
+#!/usr/bin/env python3
+"""Exact candidate manager regressions for persistence and incarnation guards."""
+
+import argparse
+import json
+import os
+from pathlib import Path
+import re
+import shutil
+import subprocess
+import sys
+import time
+
+try:
+ from . import native_regressions as native
+except ImportError:
+ import native_regressions as native
+
+
+SOURCE_TREE = "8071fb0a9e5353eb7d3fa58c8a58c8622473f2bd"
+LOCK_SHA = "d8f7bc416ba67d19873d34e9389e5aa823b22d7d7552706a5a810cee26f6eda2"
+REVIEWED_CANDIDATES = {
+ native.CANDIDATE: {"sourceTree": SOURCE_TREE, "cargoLockSha256": LOCK_SHA},
+ # The manager, checkpoint, MRF and outcome sources are byte-identical to
+ # the original reviewed candidate; this source adds the validation fixes
+ # elsewhere in the RustFS tree.
+ "16cf19817e0c81e24d0d787a916f734a34517ec5": {
+ "sourceTree": "7edb1a2d02d60323cdfab4173485c33cb06b297a",
+ "cargoLockSha256": LOCK_SHA,
+ },
+ "e224eb48e5311cff022b04fd78c97a8572d5c270": {
+ "sourceTree": "098d5486f801e689ef30c00e5cedb775f0f1be0d",
+ "cargoLockSha256": LOCK_SHA,
+ },
+ "1aa9be87d18cf941e5020f19e8622c26ba4b10ac": {
+ "sourceTree": "d57bcda8b1551727ced7073ddd09f8c97c13485f",
+ "cargoLockSha256": LOCK_SHA,
+ },
+}
+SOURCE_FILES = {
+ "crates/heal/src/heal/resume/tests.rs": "cea1558f8659f01968a5592af55cd0626f1d309c9fecfa0bee26c36775229900",
+ "crates/heal/src/heal/resume/checkpoint.rs": "fe067b39ee406e16517bd665f7f49457daffa05d5d7bc298bd965e927b1381c1",
+ "crates/heal/src/heal/mrf_queue/snapshot.rs": "ecfcb5ee63edef5c5d3d6713edc127e16f489522afc5fc02dc1044ccca241d14",
+ "crates/heal/src/heal/mrf_queue/snapshot/migration.rs": "9163c87bcdee1439667f79aea2a36f6ffce14c6d7c64be0786306dd3a75478b9",
+ "crates/heal/src/heal/manager/tests/root_recovery.rs": "7c4a0cb7834390f053737a8950f24e070a01e1c74583d9db206ea1b620385594",
+ "crates/heal/src/heal/manager/root_recovery.rs": "ba6beba9d9d14985e4c5448d85727036388d3d72f28879ac5c67fc5f760048ba",
+ "crates/heal/src/heal/erasure_healer/resume_loop_tests/admin_outcome.rs": "f2251a9ab5f2df10a2299cd23bb3231615d946ce95a104002088f06235dde621",
+ "crates/heal/src/heal/storage.rs": "52ce6edff5b64b69a0d0c2a425b980984cf927fab79139e33d5ff9c7ad98efe9",
+}
+# Native prerequisites only; fleet executors retain their own candidate gates.
+REVIEWED_CANDIDATES["1c7fc818dd858247c45b39542abd8291b1e6e31b"] = {
+ "sourceTree": "9713c550ce148e1e32004f166a19e3a67eb83434",
+ "cargoLockSha256": "2589b3f5afc54f94419a3e2bfd537c11fae7ae3ee2d2a73ba31c72ee8d8a62be",
+ "sourceFiles": {**SOURCE_FILES,
+ "crates/heal/src/heal/storage.rs": "0e27cc1fbb7b732d211e744f385223bee23844c95ba86918e1765b546c197096"},
+}
+REVIEWED_CANDIDATES["7951bb629821246868ee3761b321dd07c06db45c"] = {
+ "sourceTree": "e4d863efff2792c0ab4da0036c04c3e0902aca89",
+ "cargoLockSha256": "2589b3f5afc54f94419a3e2bfd537c11fae7ae3ee2d2a73ba31c72ee8d8a62be",
+ "sourceFiles": {**REVIEWED_CANDIDATES["1c7fc818dd858247c45b39542abd8291b1e6e31b"]["sourceFiles"],
+ "crates/heal/src/heal/manager.rs": "6e41b39d74aedf3b40b6c6a8f7c5de60bf2cb7927d7d76a10745033002dbc00d",
+ "crates/heal/src/heal/manager/tests.rs": "844b84870e85fca4de1c72faf66e4f7a43561d8d6db341db43fd089223ff9a1e",
+ "crates/heal/tests/shard_identity_receipt_test.rs": "51ffe71b57efceb03ef40aa2333e235c5515149c89b64df0553dbe26b331db32"},
+}
+RESUME = "heal::resume::tests::"
+SNAPSHOT = "heal::mrf_queue::snapshot::tests::"
+ROOT = "heal::manager::tests::root_recovery::"
+OUTCOME = "heal::erasure_healer::resume_loop_tests::admin_outcome::"
+
+
+def spec(test, cases, assertions, kind="deterministic candidate guard assertions"):
+ return {"test": test, "relatedCases": cases, "sourceAssertions": assertions,
+ "fenceKind": kind, "fleetPassClaimed": False, "physicalPowerLossClaimed": False,
+ "emittedBarrierLogsClaimed": False}
+
+
+F07 = ["heal.checkpoint-and-mrf-corruption"]
+C11 = ["heal.bucket-incarnation-recreation"]
+J02 = ["heal.outcome-counters-and-error-truth"]
+REGRESSIONS = {
+ "checkpoint-truncated": spec(RESUME + "checkpoint_save_does_not_replace_a_non_empty_truncated_snapshot", F07,
+ ["save fails closed", "original truncated bytes remain", "task is blocked"]),
+ "checkpoint-future": spec(RESUME + "checkpoint_save_does_not_replace_a_future_schema_snapshot", F07,
+ ["future schema save fails closed", "unknown bytes remain", "task is blocked"]),
+ "checkpoint-digest": spec(RESUME + "checkpoint_digest_rejects_same_length_progress_tampering", F07,
+ ["same-length progress tampering fails load", "task is blocked"]),
+ "checkpoint-stale-manager": spec(RESUME + "stale_checkpoint_manager_cannot_overwrite_newer_progress", F07,
+ ["stale manager save is rejected", "reopened newer bucket/object cursor remains 4/20"]),
+ "checkpoint-page-order": spec(RESUME + "checkpoint_page_commit_keeps_ledger_until_cursor_is_durable", F07,
+ ["reload preserves completed page cursor, counters and ledger", "ledger prunes only after durable cursor"],
+ "real checkpoint publication and reload ordering; no injected fsync interruption"),
+ "mrf-payload-integrity": spec(SNAPSHOT + "whole_payload_integrity_is_required_even_with_a_valid_manifest", F07,
+ ["valid outer manifest cannot hide damaged full-record integrity"]),
+ "mrf-manifest-write-failure": spec(SNAPSHOT + "committed_snapshot_writer_manifest_failure_preserves_previous_anchor", F07,
+ ["actual successor manifest path blocks publication", "error remains visible", "reopened previous payload and manifest are unchanged"]),
+ "mrf-payload-write-failure": spec(SNAPSHOT + "committed_snapshot_writer_payload_failure_does_not_publish_manifest", F07,
+ ["actual successor payload path blocks staging", "error remains visible", "no successor manifest", "previous anchor survives reload"]),
+ "mrf-migration-boundaries": spec("heal::mrf_queue::snapshot::migration::tests::migration_commit_boundaries_and_lost_response_recover_idempotently", F07,
+ ["actual AfterPayload/BeforeManifest/AfterManifest/AfterReadback interruptions are injected",
+ "source bytes survive every interruption", "uncommitted recovery is refused", "same owner retry recovers identical records"],
+ "four actual migration publication interruption hooks; no physical power-loss or individual fsync-hook claim"),
+ "root-uncertain-publication": spec(ROOT + "root_recovery_uncertain_terminal_publication_preserves_commit_and_pending_fence", F07,
+ ["failure is injected after terminal write", "pending owner remains", "restart fences stale runnable work and restores committed outcome"],
+ "actual candidate terminal publication failure hook"),
+ "incarnation-root-replay": spec(ROOT + "bucket_incarnation_recovery_retires_old_owner_and_admits_successor", C11,
+ ["same incarnation replays original token", "new incarnation rejects old owner", "new token admits successor",
+ "reintroduced original pending bytes cannot defeat durable stale terminal"]),
+ "incarnation-deferred-metadata": spec(ROOT + "bucket_incarnation_metadata_failure_defers_replay_without_retiring_owner", C11,
+ ["metadata lookup failure preserves original pending owner instead of retiring or rebinding"]),
+ "incarnation-checkpoint": spec(OUTCOME + "admin_erasure_recreated_bucket_cannot_rebind_checkpoint", C11,
+ ["recreated incarnation returns StaleBucketIncarnation", "storage calls do not advance", "acknowledged healed counter is retained"]),
+ "outcome-truth": spec(OUTCOME + "admin_erasure_dispositions_do_not_invent_success_and_retries_count_once", J02,
+ ["verified/absent/unproven/transient cases remain distinct", "exact 5/0/2/0/3/1 counters", "eight attempt failures remain visible",
+ "exhausted missing-version retries remain deferred"]),
+ "outcome-wrong-identity": spec(OUTCOME + "admin_erasure_rejects_every_receipt_identity_mismatch", J02,
+ ["bucket/object/version/incarnation/pool/set mismatches each yield unknown and zero healed"]),
+}
+BUILD_ENV = {"CARGO_BUILD_JOBS": "2", "CARGO_INCREMENTAL": "0",
+ "CARGO_PROFILE_RELEASE_DEBUG": "0", "CARGO_PROFILE_RELEASE_STRIP": "debuginfo"}
+
+
+def collectors():
+ return {path.name: native.sha(path) for path in (Path(__file__), Path(native.__file__))}
+
+
+def verify_manifest(directory):
+ manifest = json.loads((directory / "manager-native.json").read_text())
+ candidate = manifest.get("candidateCommit")
+ contract = REVIEWED_CANDIDATES.get(candidate)
+ native.require(manifest.get("schemaVersion") == 1 and contract is not None
+ and manifest.get("candidateSourceTree") == contract["sourceTree"]
+ and manifest.get("candidateCargoLockSha256") == contract["cargoLockSha256"]
+ and manifest.get("sourceFiles") == contract.get("sourceFiles", SOURCE_FILES) and manifest.get("collectorSources") == collectors()
+ and manifest.get("regressions") == REGRESSIONS and manifest.get("sourceUnmodified") is True
+ and manifest.get("optimized") is True and manifest.get("buildEnvironment") == BUILD_ENV
+ and manifest.get("binaryName") == "heal-manager-native-tests" and manifest.get("fleetPassClaimed") is False,
+ "manager native artifact changed its reviewed source, test or collector contract")
+ binary = directory / manifest["binaryName"]
+ native.require(binary.is_file() and not binary.is_symlink() and native.sha(binary) == manifest.get("binarySha256"),
+ "manager native executable identity differs")
+ return manifest, binary
+
+
+def build(source, commit, out):
+ contract = REVIEWED_CANDIDATES.get(commit)
+ native.require(contract is not None and subprocess.check_output(
+ ["git", "-C", str(source), "rev-parse", "HEAD"], text=True).strip() == commit,
+ "manager native tests require the exact reviewed candidate")
+ native.require(subprocess.check_output(["git", "-C", str(source), "rev-parse", "HEAD^{tree}"], text=True).strip() == contract["sourceTree"]
+ and native.sha(source / "Cargo.lock") == contract["cargoLockSha256"]
+ and all(native.sha(source / path) == digest for path, digest in contract.get("sourceFiles", SOURCE_FILES).items()),
+ "manager candidate source/tree/lock differs")
+ for args in (["diff", "--quiet"], ["diff", "--cached", "--quiet"]):
+ native.require(subprocess.run(["git", "-C", str(source), *args]).returncode == 0, "candidate source is modified")
+ native.require(not subprocess.check_output(["git", "-C", str(source), "ls-files", "--others", "--exclude-standard"], text=True).strip(),
+ "candidate contains untracked source/configuration")
+ native.require(not out.exists() and shutil.disk_usage(source).free >= 12 * 1024**3, "fresh output and 12 GiB build space required")
+ out.mkdir(parents=True)
+ request = {"schemaVersion": 1, "candidateCommit": commit, "candidateSourceTree": contract["sourceTree"],
+ "candidateCargoLockSha256": contract["cargoLockSha256"], "sourceFiles": contract.get("sourceFiles", SOURCE_FILES), "collectorSources": collectors(),
+ "regressions": REGRESSIONS, "testingCommit": os.environ.get("AUTO_TESTING_SHA"),
+ "runId": os.environ.get("GITHUB_RUN_ID"), "runAttempt": os.environ.get("GITHUB_RUN_ATTEMPT"),
+ "fleetPassClaimed": False, "sourceUnmodified": True, "optimized": True, "buildEnvironment": BUILD_ENV}
+ native.save(out / "manager-build-request.json", request)
+ target = out / "target"
+ command = ["cargo", "test", "--locked", "--release", "-p", "rustfs-heal", "--lib", "--no-run", "--message-format=json"]
+ with (out / "cargo.jsonl").open("x") as stdout, (out / "cargo.stderr").open("x") as stderr:
+ result = subprocess.run(command, cwd=source, env={**os.environ, **BUILD_ENV, "CARGO_TARGET_DIR": str(target)},
+ stdout=stdout, stderr=stderr, timeout=150 * 60)
+ native.require(result.returncode == 0, "manager native compilation failed; original diagnostics retained")
+ executables = []
+ for line in (out / "cargo.jsonl").read_text().splitlines():
+ try: row = json.loads(line)
+ except json.JSONDecodeError: continue
+ if row.get("reason") == "compiler-artifact" and row.get("profile", {}).get("test") is True \
+ and row.get("target", {}).get("name") == "rustfs_heal" and row.get("executable"):
+ executables.append(Path(row["executable"]))
+ native.require(len(executables) == 1 and executables[0].resolve().is_relative_to(target.resolve()), "one owned manager test executable required")
+ binary = out / "heal-manager-native-tests"
+ shutil.copyfile(executables[0], binary)
+ binary.chmod(0o500)
+ listing = subprocess.run([str(binary), "--list"], capture_output=True, text=True, timeout=60)
+ (out / "test-inventory.log").write_text(listing.stdout + listing.stderr)
+ native.require(listing.returncode == 0, "manager native inventory failed")
+ for value in REGRESSIONS.values(): native.exact_listing(listing.stdout, value["test"])
+ for args in (["diff", "--quiet"], ["diff", "--cached", "--quiet"]):
+ native.require(subprocess.run(["git", "-C", str(source), *args]).returncode == 0, "build changed tracked candidate source")
+ native.save(out / "manager-native.json", {**request, "binaryName": binary.name, "binarySha256": native.sha(binary),
+ "binaryKind": "native-regression-not-released-service", "buildCommand": command,
+ "toolchain": subprocess.check_output(["rustc", "-vV"], cwd=source, text=True)})
+ shutil.rmtree(target)
+
+
+def run(directory, out):
+ manifest, binary = verify_manifest(directory)
+ native.require(not out.exists(), "manager native evidence must be fresh")
+ out.mkdir(parents=True)
+ listing = subprocess.run([str(binary), "--list"], capture_output=True, text=True, timeout=60)
+ (out / "runtime-inventory.log").write_text(listing.stdout + listing.stderr)
+ native.require(listing.returncode == 0, "manager runtime inventory failed")
+ results = []
+ for key, value in REGRESSIONS.items():
+ native.exact_listing(listing.stdout, value["test"])
+ verify_manifest(directory)
+ command = [str(binary), value["test"], "--exact", "--nocapture", "--test-threads=1"]
+ record = {"regression": key, **value, "candidateCommit": manifest["candidateCommit"],
+ "binarySha256": manifest["binarySha256"], "command": command}
+ started = time.monotonic_ns()
+ try:
+ child = subprocess.run(command, capture_output=True, text=True, timeout=120)
+ (out / (key + ".log")).write_text(child.stdout + child.stderr)
+ record["exitCode"] = child.returncode
+ native.test_result(child.stdout + child.stderr, value["test"], child.returncode)
+ record["verdict"] = "PASS"
+ except subprocess.TimeoutExpired as error:
+ parts = [part.decode(errors="replace") if isinstance(part, bytes) else part or "" for part in (error.stdout, error.stderr)]
+ (out / (key + ".log")).write_text("".join(parts))
+ record.update(verdict="FAIL", reason="fixed 120-second native deadline exceeded")
+ except (OSError, ValueError) as error:
+ record.update(verdict="FAIL", reason=str(error))
+ record["elapsedNanoseconds"] = time.monotonic_ns() - started
+ native.save(out / (key + ".json"), record)
+ results.append(record)
+ complete = all(row["verdict"] == "PASS" for row in results)
+ native.save(out / "manager-native-summary.json", {"results": results, "complete": complete,
+ "canonicalCasePassClaimed": False, "fleetPassClaimed": False, "physicalPowerLossClaimed": False})
+ native.require(complete, "manager native failures remain in original evidence")
+
+
+def verify_evidence(directory, evidence, candidate, run_id, run_attempt, helper_sha):
+ manifest, _ = verify_manifest(directory)
+ native.require(candidate["source_sha"] == manifest.get("candidateCommit")
+ and manifest.get("candidateCommit") in REVIEWED_CANDIDATES and manifest.get("runId") == str(run_id)
+ and manifest.get("runAttempt") == str(run_attempt) and manifest.get("testingCommit") == helper_sha
+ and re.fullmatch(r"[0-9a-f]{40}", helper_sha), "manager native evidence belongs to another build attempt")
+ summary = json.loads((evidence / "manager-native-summary.json").read_text())
+ rows = summary.get("results", [])
+ native.require(summary.get("complete") is True and summary.get("canonicalCasePassClaimed") is False
+ and summary.get("fleetPassClaimed") is False and summary.get("physicalPowerLossClaimed") is False
+ and len(rows) == len(REGRESSIONS) and {row.get("regression") for row in rows} == set(REGRESSIONS),
+ "manager native summary is incomplete, duplicated or claims fleet success")
+ listings = [(path).read_text() for path in (directory / "test-inventory.log", evidence / "runtime-inventory.log")]
+ for row in rows:
+ key = row["regression"]
+ native.require(row == json.loads((evidence / (key + ".json")).read_text()) and row.get("verdict") == "PASS"
+ and row.get("binarySha256") == manifest["binarySha256"] and row.get("candidateCommit") == manifest["candidateCommit"]
+ and all(row.get(name) == value for name, value in REGRESSIONS[key].items()), "manager result changed exact scope or identity")
+ command = row.get("command", [])
+ native.require(len(command) == 5 and Path(command[0]).name == manifest["binaryName"]
+ and command[1:] == [row["test"], "--exact", "--nocapture", "--test-threads=1"], "manager result lacks exact one-test invocation")
+ for listing in listings: native.exact_listing(listing, row["test"])
+ native.test_result((evidence / (key + ".log")).read_text(), row["test"], row.get("exitCode"))
+ return {"complete": True, "candidateCommit": manifest["candidateCommit"], "binarySha256": manifest["binarySha256"],
+ "exactTests": len(rows), "canonicalCasePassClaimed": False, "fleetPassClaimed": False}
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ commands = parser.add_subparsers(dest="command", required=True)
+ compiling = commands.add_parser("build")
+ compiling.add_argument("--source", type=Path, required=True)
+ compiling.add_argument("--commit", required=True)
+ compiling.add_argument("--out", type=Path, required=True)
+ running = commands.add_parser("run")
+ running.add_argument("--directory", type=Path, required=True)
+ running.add_argument("--out", type=Path, required=True)
+ args = parser.parse_args()
+ if args.command == "build": build(args.source.resolve(strict=True), args.commit, args.out.resolve())
+ else: run(args.directory.resolve(strict=True), args.out.resolve())
+
+
+if __name__ == "__main__":
+ try: main()
+ except Exception as error:
+ print(f"manager native validation failed: {error}", file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_heal/markers.py b/scripts/validation_heal/markers.py
new file mode 100644
index 00000000..2eac87b4
--- /dev/null
+++ b/scripts/validation_heal/markers.py
@@ -0,0 +1,240 @@
+"""D04: genuine marker ACKs, missing metadata and same-drive stale transitions."""
+
+import copy
+import hashlib
+
+try:
+ from .heal_probe import Probe, payload, require
+ from .heal_workload import verify_seed
+ from .healthy import physical_content
+ from .shards import files
+except ImportError:
+ from heal_probe import Probe, payload, require
+ from heal_workload import verify_seed
+ from healthy import physical_content
+ from shards import files
+
+
+BRANCHES = ("current-marker", "historical-marker", "missing-marker-replica", "stale-disk-marker")
+INSTANCES = (
+ {"name": "current-marker", "sourceBranch": "current-marker", "mode": "current", "role": "data"},
+ {"name": "historical-marker", "sourceBranch": "historical-marker", "mode": "historical", "role": "parity"},
+ {"name": "missing-current-data", "sourceBranch": "missing-marker-replica", "mode": "current", "role": "data"},
+ {"name": "missing-historical-parity", "sourceBranch": "missing-marker-replica", "mode": "historical", "role": "parity"},
+ {"name": "stale-create-current-marker", "sourceBranch": "stale-disk-marker", "mode": "create", "role": "data"},
+ {"name": "stale-delete-current-marker", "sourceBranch": "stale-disk-marker", "mode": "delete", "role": "parity"},
+)
+
+
+def key_for(instance):
+ return "markers/" + instance["name"] + ".bin"
+
+
+def add_marker(expected, key, response):
+ marker = response.get("VersionId")
+ require(response.get("DeleteMarker") is True and isinstance(marker, str) and marker not in {"", "null"}
+ and response.get("ResponseMetadata", {}).get("HTTPStatusCode") == 204
+ and not any(entry["versionId"] == marker for entry in expected), "marker creation lacks a distinct exact DELETE ACK")
+ result = copy.deepcopy(expected)
+ require(any(entry["key"] == key for entry in result), "marker creation has no acknowledged data key")
+ for entry in result:
+ if entry["key"] == key: entry["isLatest"] = False
+ result.append({"key": key, "versionId": marker, "deleteMarker": True, "isLatest": True})
+ return result
+
+
+def remove_marker(expected, key, response):
+ selected = [entry for entry in expected if entry["key"] == key and entry["deleteMarker"] and entry["isLatest"]]
+ require(len(selected) == 1 and response.get("VersionId") == selected[0]["versionId"]
+ and response.get("DeleteMarker") is True and response.get("ResponseMetadata", {}).get("HTTPStatusCode") == 204,
+ "current marker removal lacks its exact DELETE ACK")
+ result = [copy.deepcopy(entry) for entry in expected if entry != selected[0]]
+ data = [entry for entry in result if entry["key"] == key and not entry["deleteMarker"]]
+ require(len(data) == 3, "marker removal lost or invented a retained data version")
+ for entry in result:
+ if entry["key"] == key: entry["isLatest"] = entry is data[-1]
+ return result
+
+
+def seed_markers(recorder, bucket, size):
+ recorder.call("create_bucket", {"Bucket": bucket})
+ recorder.call("put_bucket_versioning", {"Bucket": bucket, "VersioningConfiguration": {"Status": "Enabled"}})
+ expected = []
+ for instance in INSTANCES:
+ key = key_for(instance)
+ for nonce in range(3):
+ body = payload(f"{bucket}/{key}/{nonce}", size)
+ response = recorder.call("put_object", {"Bucket": bucket, "Key": key,
+ "Metadata": {"heal-fixture": "marker-semantics", "generation": str(nonce)},
+ "Tagging": f"suite=heal&generation=v{nonce}"}, body)
+ version = response.get("VersionId")
+ require(isinstance(version, str) and version not in {"", "null"}
+ and not any(entry["versionId"] == version for entry in expected), "marker fixture lacks distinct versioned PUT ACKs")
+ for entry in expected:
+ if entry["key"] == key: entry["isLatest"] = False
+ expected.append({"key": key, "versionId": version, "deleteMarker": False, "isLatest": True,
+ "payload": {"size": size, "sha256": hashlib.sha256(body).hexdigest()}})
+ if instance["mode"] == "historical" and nonce == 1:
+ expected = add_marker(expected, key, recorder.call("delete_object", {"Bucket": bucket, "Key": key}))
+ if instance["mode"] in {"current", "delete"}:
+ expected = add_marker(expected, key, recorder.call("delete_object", {"Bucket": bucket, "Key": key}))
+ return expected
+
+
+def marker_plan(instance, snapshot, bucket, expected):
+ # Imported lazily: the shared version executor also uses this module.
+ try:
+ from .versions import choose_target, metadata_target
+ except ImportError:
+ from versions import choose_target, metadata_target
+ key = key_for(instance)
+ selected = [entry for entry in expected if entry["key"] == key]
+ data = [entry for entry in selected if not entry["deleteMarker"]]
+ require(len(data) == 3 and len({entry["versionId"] for entry in selected}) == len(selected), "marker plan lacks the three exact data versions")
+ if instance["mode"] != "create":
+ require(len([entry for entry in selected if entry["deleteMarker"]]) == 1, "marker plan lacks its acknowledged current/historical marker")
+ target = choose_target(snapshot, bucket, data[0], instance["role"])
+ proofs = [choose_target(snapshot, bucket, entry, "data") for entry in data]
+ proofs = [{**item, **next(shard for shard in item["cohort"] if
+ (shard["node"], shard["disk"]) == (target["node"], target["disk"]))} for item in proofs]
+ return {"scope": {"bucket": bucket, "key": key}, "recursive": True, "expected": selected,
+ "faults": [] if instance["mode"] in {"create", "delete"} else [metadata_target(snapshot, target)],
+ "proofs": proofs, "requiredRepairs": set(), "metadataRepairs": {key}, "parameters": instance,
+ "staleMarker": instance["mode"] if instance["mode"] in {"create", "delete"} else None}
+
+
+def stale_expected_scene(original, authorities, bucket, key, expected, target):
+ """Derive an oracle from real quorum metadata. This never writes metadata."""
+ left, right = files(original), files(authorities)
+ selected = (target["node"], target["disk"])
+ require({node: value["formats"] for node, value in original.items()} ==
+ {node: value["formats"] for node, value in authorities.items()}, "stale window changed original disk/set identities")
+ require({identity: entry for identity, entry in left.items() if identity[:2] == selected} ==
+ {identity: entry for identity, entry in right.items() if identity[:2] == selected}, "inaccessible member received a marker write")
+ wanted = {(entry["versionId"], entry["deleteMarker"], entry["isLatest"])
+ for entry in expected if entry["key"] == key}
+ path = f"{bucket}/{key}/xl.meta"
+ donors = [entry["metadata"] for identity, entry in right.items() if identity[2] == path and identity[:2] != selected]
+ require(len(donors) == len(target["cohort"]) - 1 and all(
+ {(version["versionId"], version["deleted"], version["isLatest"]) for version in donor["versions"]} == wanted
+ for donor in donors), "online marker replicas lack a full same-version ACK authority quorum")
+ for identity, entry in right.items():
+ if identity[2] != path or identity[:2] == selected: continue
+ previous = left[identity]["metadata"]
+ actual = entry["metadata"]
+ require({name: value for name, value in previous.items() if name != "versions"} ==
+ {name: value for name, value in actual.items() if name != "versions"},
+ "marker operation changed the metadata format or object identity")
+ previous_data = {version["versionId"]: {name: value for name, value in version.items() if name != "isLatest"}
+ for version in previous["versions"] if not version["deleted"]}
+ actual_data = {version["versionId"]: {name: value for name, value in version.items() if name != "isLatest"}
+ for version in actual["versions"] if not version["deleted"]}
+ require(previous_data == actual_data, "marker operation changed a retained data-version reference")
+ current_meta = left[(*selected, path)]["metadata"]
+ require({(version["versionId"], version["deleted"], version["isLatest"]) for version in current_meta["versions"]} != wanted,
+ "marker window did not create a genuinely stale replica")
+ template = donors[0]
+ old = {version["versionId"]: version for version in current_meta["versions"]}
+ versions = []
+ for authority in template["versions"]:
+ if authority["deleted"]:
+ require(authority.get("dataDir") is None and not authority.get("parts") and authority.get("inline") is None,
+ "marker authority unexpectedly has live data references")
+ versions.append(copy.deepcopy(authority))
+ else:
+ require(authority["versionId"] in old and not old[authority["versionId"]]["deleted"], "stale marker changed a data generation")
+ version = copy.deepcopy(old[authority["versionId"]])
+ version["isLatest"] = authority["isLatest"]
+ versions.append(version)
+ oracle = copy.deepcopy(authorities)
+ for entry in oracle[selected[0]]["files"]:
+ if entry["disk"] == selected[1] and entry["path"] == path:
+ entry["metadata"]["versions"] = versions
+ # Data shards and all controls are unchanged throughout the legitimate
+ # marker operation; only this key's xl.meta is allowed to change online.
+ require({identity: entry for identity, entry in left.items() if identity[2] != path} ==
+ {identity: entry for identity, entry in right.items() if identity[2] != path},
+ "marker ACK changed a data shard, auxiliary file or control")
+ return oracle
+
+
+def prepare_stale(recorder, fleet, bucket, plan, before, expected):
+ target = plan["proofs"][0]
+ node, disk, key = target["node"], target["disk"], plan["scope"]["key"]
+ fleet.drive_gates.add(node)
+ fleet.record("marker-member-isolated", fleet.remote(node, "drive-gate", disk=disk, mode="permission-denied"))
+ fleet.start()
+ refusal = fleet.record("marker-service-denial", fleet.remote(node, "drive-gate-probe"))
+ require(refusal.get("readProbe", {}).get("errno") == 13 and refusal.get("dacBypassRemoved") is True,
+ "stale marker member lacks actual same-service read/write denial")
+ params = {"Bucket": bucket, "Key": key}
+ if plan["staleMarker"] == "delete":
+ params["VersionId"] = next(entry["versionId"] for entry in plan["expected"] if entry["deleteMarker"] and entry["isLatest"])
+ response = recorder.call("delete_object", params)
+ manifest = (add_marker if plan["staleMarker"] == "create" else remove_marker)(expected, key, response)
+ baseline = fleet.record("marker-online-ack-inventory", recorder.probe.inventory(bucket))
+ fleet.record("marker-online-ack-proof", {"expected": manifest, **verify_seed(manifest, baseline)})
+ fleet.stop()
+ authority = fleet.snapshot("marker-stale-authorities", quiescent=True)
+ oracle = stale_expected_scene(before, authority, bucket, key, manifest, target)
+ fleet.record("marker-derived-physical-oracle", {"scene": oracle, "metadataWrittenByHarness": False,
+ "derivation": "acknowledged marker quorum plus original same-drive data-version references"})
+ fleet.record("marker-member-rejoined", fleet.remote(node, "drive-gate", mode="clear"))
+ fleet.drive_gates.remove(node)
+ plan["expected"] = [entry for entry in manifest if entry["key"] == key]
+ return oracle, authority, manifest, baseline
+
+
+def validate_marker_error(observed, status, version):
+ response = observed.get("response") or {}
+ metadata = response.get("ResponseMetadata", {})
+ headers = {name.lower(): value for name, value in metadata.get("HTTPHeaders", {}).items()}
+ require(observed.get("acknowledged") is False and observed.get("resultUnknown") is False and "payload" not in observed
+ and metadata.get("HTTPStatusCode") == status and headers.get("x-amz-delete-marker") == "true"
+ and headers.get("x-amz-version-id") == version, "marker refusal lost its exact HTTP/version/marker identity")
+
+
+def verify_markers(probe, fleet, bucket, key, expected):
+ selected = [entry for entry in expected if entry["key"] == key]
+ probes = []
+ for endpoint in probe.binding["endpoints"]:
+ peer = Probe(probe.binding, endpoint)
+ for entry in [entry for entry in selected if entry["deleteMarker"]]:
+ requests = [(entry["versionId"], 405)] + ([(None, 404)] if entry["isLatest"] else [])
+ for selector, status in requests:
+ for method in ("get_object", "head_object"):
+ params = {"Bucket": bucket, "Key": key}
+ if selector: params["VersionId"] = selector
+ observed = fleet.record("marker-s3-refusal", peer.s3(method, params))
+ validate_marker_error(observed, status, entry["versionId"])
+ probes.append({"endpoint": endpoint, "method": method, "selector": selector,
+ "status": status, "markerVersion": entry["versionId"]})
+ return {"allPeerMarkerIdentityHeaders": True, "probes": probes}
+
+
+def reveal_current(recorder, fleet, bucket, key, expected, baseline):
+ current = next((entry for entry in expected if entry["key"] == key and entry["deleteMarker"] and entry["isLatest"]), None)
+ if current is None: return expected, baseline, {"currentMarkerPresent": False}
+ response = recorder.call("delete_object", {"Bucket": bucket, "Key": key, "VersionId": current["versionId"]})
+ manifest = remove_marker(expected, key, response)
+ revealed = next(entry for entry in manifest if entry["key"] == key and entry["isLatest"])
+ for endpoint in recorder.probe.binding["endpoints"]:
+ peer = Probe(recorder.probe.binding, endpoint)
+ read = fleet.record("marker-revealed-data", peer.s3("get_object", {"Bucket": bucket, "Key": key}))
+ require(read.get("acknowledged") and read.get("payload") == revealed["payload"]
+ and read.get("response", {}).get("VersionId") == revealed["versionId"], "marker removal exposed another data generation")
+ deleted = fleet.record("marker-removed-version", peer.s3("get_object", {
+ "Bucket": bucket, "Key": key, "VersionId": current["versionId"]}))
+ require(deleted.get("acknowledged") is False and deleted.get("resultUnknown") is False
+ and "payload" not in deleted and deleted.get("response", {}).get("ResponseMetadata", {}).get("HTTPStatusCode") == 404,
+ "explicitly removed marker remained addressable")
+ inventory = fleet.record("marker-removal-inventory", recorder.probe.inventory(bucket))
+ verify_seed(manifest, inventory)
+ old = {(entry["key"], entry["versionId"]): entry for entry in baseline["entries"]}
+ require(len(inventory["entries"]) == len(baseline["entries"]) - 1, "marker removal created or lost extra versions")
+ for entry in inventory["entries"]:
+ previous = copy.deepcopy(old[(entry["key"], entry["versionId"])])
+ if entry["key"] == key: previous["isLatest"] = entry["isLatest"]
+ require(previous == entry, "marker removal changed a retained version or public property")
+ return manifest, inventory, {"currentMarkerRemoved": current["versionId"], "revealedVersion": revealed["versionId"],
+ "noAdditionalDataVersions": True, "allPeerCurrentReadsMatch": True}
diff --git a/scripts/validation_heal/matrix.py b/scripts/validation_heal/matrix.py
new file mode 100644
index 00000000..b65cbfcb
--- /dev/null
+++ b/scripts/validation_heal/matrix.py
@@ -0,0 +1,289 @@
+#!/usr/bin/env python3
+"""Preserve all Heal procedures and audit branch evidence on the auto-testing fleet."""
+
+from __future__ import annotations
+
+import argparse
+from datetime import datetime, timezone
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import socket
+import sys
+
+try:
+ from .execution_policy import DISPATCHABLE_CASES, ENVIRONMENT_GATED_CASES, EVIDENCE_BLOCKED_CASES, policy_for
+except ImportError:
+ from execution_policy import DISPATCHABLE_CASES, ENVIRONMENT_GATED_CASES, EVIDENCE_BLOCKED_CASES, policy_for
+
+
+ROOT = Path(__file__).resolve().parents[2]
+RECIPES = Path(__file__).with_name("recipes.json")
+CATALOG = ROOT / "validation-cases.json"
+OPERATIONS = ROOT / "docs/validation-heal-operations.md"
+CONTRACT = ROOT / "docs/validation-heal-contract.md"
+TOPOLOGIES = {"H1", "H2", "H3", "H4", "H5", "H6"}
+STAGES = {"preflight", "baseline", "fault", "trigger", "control", "physical", "s3", "recovery", "logs"}
+AUTOMATIC_CASES = DISPATCHABLE_CASES - set(ENVIRONMENT_GATED_CASES)
+
+
+def require(condition: bool, message: str) -> None:
+ if not condition:
+ raise ValueError(message)
+
+
+def digest(path: Path) -> str:
+ return hashlib.sha256(path.read_bytes()).hexdigest()
+
+
+def save(path: Path, value: dict) -> None:
+ path.parent.mkdir(parents=True, exist_ok=True)
+ with path.open("x", encoding="utf-8") as stream:
+ stream.write(json.dumps(value, ensure_ascii=False, indent=2) + "\n")
+
+
+def variant_topologies(item: dict) -> dict[str, set[str]]:
+ allowed = set(item["contract"]["probes"]["topology"].split(","))
+ require(bool(allowed) and allowed <= TOPOLOGIES, "invalid Heal topology")
+ if "variantTopologies" not in item:
+ return {variant: allowed for variant in item["variants"]}
+ mapping = item["variantTopologies"]
+ require(set(mapping) == set(item["variants"]), "variant topology mapping omits a branch")
+ require(bool(item.get("topologyApplicabilityReason")), "missing topology applicability reason")
+ for values in mapping.values():
+ require(isinstance(values, list) and bool(values) and len(values) == len(set(values))
+ and set(values) <= allowed, "invalid variant topology mapping")
+ require(set().union(*(set(values) for values in mapping.values())) == allowed,
+ "variant topology mapping omits a required topology")
+ return {variant: set(values) for variant, values in mapping.items()}
+
+
+def cases() -> list[dict]:
+ catalog = json.loads(CATALOG.read_text())
+ designed = {case["id"]: case for module in catalog["modules"] for case in module["cases"]
+ if case["id"].startswith("heal.")}
+ document = json.loads(RECIPES.read_text())
+ require(document.get("schemaVersion") == 1 and document.get("executionMode") == "operator-assisted",
+ "invalid Heal procedure contract")
+ observed = document["cases"]
+ require(len(observed) == 84 and {item["id"] for item in observed} == set(designed),
+ "Heal procedures must cover exactly the 84 canonical source cases")
+ require(len({item["code"] for item in observed}) == 84, "duplicate Heal procedure code")
+ anchors = set(re.findall(r'', OPERATIONS.read_text()))
+ for item in observed:
+ contract = designed[item["id"]]
+ require(item["code"] == contract["probes"]["designCode"], "Heal procedure identity mismatch")
+ require(bool(item["variants"]) and len(item["variants"]) == len(set(item["variants"]))
+ and all(re.fullmatch(r"[a-z0-9][a-z0-9-]*", variant) for variant in item["variants"]),
+ "invalid Heal procedure variants")
+ require(len(item["steps"]) >= 3 and set(item["requiredEvidence"]) == STAGES,
+ "incomplete Heal procedure or evidence stages")
+ require(bool(item["commands"]) and set(item["commands"]) <= anchors,
+ "Heal procedure refers to an undefined operation")
+ require(bool(item["recovery"]) and bool(item["verdict"]), "missing Heal recovery/verdict contract")
+ expected_policy = policy_for(item["id"])
+ if expected_policy is None:
+ require("executionPolicy" not in item,
+ f"unexpected execution policy for {item['id']}")
+ else:
+ require(item.get("executionPolicy") == expected_policy,
+ f"execution policy mismatch for {item['id']}")
+ require(item["executionPolicy"]["dispatchable"] == (item["id"] in DISPATCHABLE_CASES),
+ f"dispatchability mismatch for {item['id']}")
+ item["contract"] = contract
+ variant_topologies(item)
+ return sorted(observed, key=lambda item: item["code"])
+
+
+def select(selection: str) -> list[dict]:
+ inventory = cases()
+ if selection == "all":
+ return inventory
+ requested = selection.split(",")
+ require(len(requested) == len(set(requested)), "duplicate Heal case selection")
+ lookup = {key: item for item in inventory for key in (item["id"], item["code"])}
+ require(all(key in lookup for key in requested), "unknown Heal case")
+ selected = [lookup[key] for key in requested]
+ require(len({item["id"] for item in selected}) == len(selected), "same Heal case selected by code and ID")
+ return selected
+
+
+def fleet_binding() -> dict:
+ nodes = os.environ.get("RUSTFS_NODES", "vm000 vm001 vm002 vm003").split()
+ require(len(nodes) >= 4 and len(nodes) == len(set(nodes))
+ and all(re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", node) for node in nodes),
+ "Heal requires the four distinct current auto-testing SSH nodes")
+ user = os.environ.get("RUSTFS_SSH_USER", "azureuser")
+ require(bool(re.fullmatch(r"[A-Za-z_][A-Za-z0-9_-]*", user)), "invalid auto-testing SSH user")
+ return {"provider": "auto-testing", "jumpHost": socket.gethostname(), "sshUser": user,
+ "sshNodes": nodes, "servicePort": 9000, "verified": False,
+ "topologies": {
+ "H1": {"nodes": nodes[:1], "drivesPerNode": 4, "requiredSets": 1},
+ "H2": {"nodes": nodes[:4], "drivesPerNode": 4, "crossNodeSetRequired": True},
+ "H3": {"nodes": nodes[:3], "drivesPerNode": 4, "dataShards": 8, "parityShards": 4, "requiredSets": 1},
+ "H4": {"nodes": nodes[:3], "drivesPerNode": 8, "dataShards": 8, "parityShards": 4,
+ "minimumSets": 2, "requiresExistingAdditionalDrives": True},
+ "H5": {"nodes": nodes[:4], "minimumPools": 2, "poolAndSetMappingRequired": True,
+ "nodeRedundancyMustBeMeasured": True},
+ "H6": {"nodes": [], "requiresDedicatedHostBinding": True,
+ "requiresPlatformEventAndRecoveryEvidence": True}},
+ "beforeMutation": ["workflow fleet lock and unexpired environment lease", "actual hostname and unit/PID",
+ "independent mount identities and disk UUIDs", "actual pool/set/slot mapping and EC quorums",
+ "run-owned roots, fixture ACKs, fault witnesses and undo plan"]}
+
+
+def compile_plan(selected: list[dict], topologies: set[str], run_id: str) -> dict:
+ require(bool(re.fullmatch(r"(?:gh|manual)-[a-z0-9-]{5,60}", run_id)), "invalid Heal run ID")
+ require(bool(topologies) and topologies <= TOPOLOGIES, "unknown Heal topology")
+ instances, excluded, non_applicable = [], [], []
+ for item in selected:
+ allowed = set(item["contract"]["probes"]["topology"].split(","))
+ mapping = variant_topologies(item)
+ for topology in sorted(allowed):
+ if topology not in topologies:
+ excluded.append({"caseId": item["id"], "topology": topology, "state": "OUT_OF_SELECTED_SCOPE"})
+ for variant in item["variants"]:
+ if topology not in mapping[variant]:
+ non_applicable.append({"caseId": item["id"], "topology": topology, "branch": variant,
+ "state": "NOT_APPLICABLE_TO_TOPOLOGY",
+ "reason": item["topologyApplicabilityReason"]})
+ elif topology in topologies:
+ instances.append({"instanceId": f"{item['code']}.{topology}.{variant}", "caseId": item["id"],
+ "code": item["code"], "topology": topology, "branch": variant,
+ "state": "NOT_RUN", "parameterBindings": {}, "sourceAssertions": item["contract"]["assertions"],
+ "operatorSteps": item["steps"], "operations": item["commands"],
+ "requiredEvidence": item["requiredEvidence"], "caseVerdict": None,
+ "executionPolicy": item.get("executionPolicy")})
+ require(bool(instances), "selected Heal cases have no requested topology")
+ return {"schemaVersion": 1, "runId": run_id, "createdAt": datetime.now(timezone.utc).isoformat(),
+ "executionMode": "operator-assisted", "state": "PLAN_ONLY", "automaticExecution": False,
+ "livePassClaimed": False, "recipeSha256": digest(RECIPES), "catalogSha256": digest(CATALOG),
+ "operationBookSha256": digest(OPERATIONS), "publicContractSha256": digest(CONTRACT),
+ "selectedBaseCaseCount": len(selected),
+ "selectedCaseIds": [item["id"] for item in selected], "selectedTopologies": sorted(topologies),
+ "dispatchableCaseCount": len(DISPATCHABLE_CASES),
+ "automaticCaseCount": len(AUTOMATIC_CASES),
+ "environmentGatedCaseCount": len(ENVIRONMENT_GATED_CASES),
+ "evidenceBlockedCaseCount": len(EVIDENCE_BLOCKED_CASES),
+ "branchTemplateCount": len(instances), "scopeExclusions": excluded,
+ "nonApplicableCombinations": non_applicable, "environmentBinding": fleet_binding(), "instances": instances}
+
+
+def audit_evidence(plan: dict, directory: Path) -> dict:
+ require(plan.get("schemaVersion") == 1 and plan.get("state") == "PLAN_ONLY"
+ and plan.get("automaticExecution") is False, "invalid Heal operator plan")
+ require(plan.get("recipeSha256") == digest(RECIPES) and plan.get("catalogSha256") == digest(CATALOG)
+ and plan.get("operationBookSha256") == digest(OPERATIONS)
+ and plan.get("publicContractSha256") == digest(CONTRACT), "Heal plan contract changed")
+ expected = compile_plan(select(",".join(plan["selectedCaseIds"])), set(plan["selectedTopologies"]), plan["runId"])
+ require(plan["instances"] == expected["instances"] and plan["scopeExclusions"] == expected["scopeExclusions"],
+ "Heal plan omitted or changed required branches")
+ root = directory.resolve(strict=True)
+ problems, recorded = [], []
+ for instance in plan["instances"]:
+ identity = instance["instanceId"]
+ index = root / identity / "evidence.json"
+ if not index.is_file() or index.is_symlink():
+ problems.append({"instanceId": identity, "reason": "missing evidence index"})
+ continue
+ require(index.resolve().is_relative_to(root), "Heal evidence index escaped archive")
+ value = json.loads(index.read_text())
+ require(value.get("runId") == plan["runId"] and value.get("instanceId") == identity,
+ "Heal evidence identity mismatch")
+ entries = value.get("evidence", {})
+ missing = STAGES - set(entries)
+ if missing:
+ problems.append({"instanceId": identity, "reason": "missing evidence stages", "stages": sorted(missing)})
+ for stage, references in entries.items():
+ require(stage in STAGES and isinstance(references, list) and bool(references), "invalid Heal evidence stage")
+ for reference in references:
+ relative = Path(reference["path"])
+ require(not relative.is_absolute() and ".." not in relative.parts, "unsafe Heal evidence path")
+ file = index.parent / relative
+ require(file.resolve().is_relative_to(index.parent.resolve()) and not file.is_symlink() and file.is_file(),
+ "missing or escaped Heal evidence file")
+ require(digest(file) == reference["sha256"], "Heal evidence digest mismatch")
+ recorded.append(identity)
+ return {"state": "EVIDENCE_INCOMPLETE" if problems else "EVIDENCE_COMPLETE_REVIEW_REQUIRED",
+ "expectedBranches": len(plan["instances"]), "recordedBranches": len(recorded), "problems": problems,
+ "livePassClaimed": False, "canonicalVerdict": None,
+ "note": "Evidence completeness does not prove physical repair or satisfy the source assertions."}
+
+
+def render(selected: list[dict]) -> str:
+ lines = ["# Heal validation procedures", "", "These source procedures require live execution and branch evidence on the current auto-testing Heal fleet.",
+ "Use [the operations reference](validation-heal-operations.md) before injecting a fault. A procedure card or complete archive is not a PASS.", ""]
+ for item in selected:
+ lines += [f'', "", f'## {item["code"]}: {item["id"]}', "",
+ f'Topologies: {item["contract"]["probes"]["topology"]}. Branches: {", ".join(item["variants"])}.', ""]
+ policy = item.get("executionPolicy")
+ if policy:
+ lines += [f"Execution policy: `{policy['kind']}`; disabled by default: `{str(policy['disabledByDefault']).lower()}`.",
+ "", f"Blocked reason (`{policy['reasonCode']}`): {policy['reason']}", ""]
+ lines += [f"{index}. {step}" for index, step in enumerate(item["steps"], 1)]
+ lines += ["", "Operations: " + " → ".join(f"[{command}](validation-heal-operations.md#{command})" for command in item["commands"]),
+ "", "Source assertions:", ""]
+ lines += [f"- {assertion}" for assertion in item["contract"]["assertions"]]
+ lines += ["", item["recovery"], "", item["verdict"], ""]
+ return "\n".join(lines).rstrip()
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description=__doc__)
+ sub = parser.add_subparsers(dest="command", required=True)
+ sub.add_parser("check")
+ sub.add_parser("list")
+ show = sub.add_parser("show")
+ show.add_argument("selection")
+ rendering = sub.add_parser("render")
+ rendering.add_argument("--selection", default="all")
+ rendering.add_argument("--out", type=Path)
+ plan = sub.add_parser("plan")
+ plan.add_argument("--selection", default="all")
+ plan.add_argument("--topologies", default="H1,H2,H3,H4,H5,H6")
+ plan.add_argument("--run-id", required=True)
+ plan.add_argument("--out", type=Path, required=True)
+ audit = sub.add_parser("audit")
+ audit.add_argument("--plan", type=Path, required=True)
+ audit.add_argument("--evidence-dir", type=Path, required=True)
+ audit.add_argument("--out", type=Path, required=True)
+ args = parser.parse_args()
+ if args.command in {"check", "list"}:
+ inventory = cases()
+ full = compile_plan(inventory, TOPOLOGIES, "manual-heal-contract-check")
+ result = {"baseCases": len(inventory), "operatorBranches": sum(len(item["variants"]) for item in inventory),
+ "topologyBranchTemplates": len(full["instances"]), "automaticCaseCount": len(AUTOMATIC_CASES),
+ "environmentGatedCaseCount": len(ENVIRONMENT_GATED_CASES),
+ "evidenceBlockedCaseCount": len(EVIDENCE_BLOCKED_CASES),
+ "dispatchableCaseCount": len(DISPATCHABLE_CASES), "livePassClaimed": False}
+ if args.command == "list":
+ result["cases"] = [{"code": item["code"], "id": item["id"], "variants": item["variants"],
+ "automaticExecutor": item["id"] in AUTOMATIC_CASES,
+ "dispatchable": item["id"] in DISPATCHABLE_CASES,
+ "executionPolicy": item.get("executionPolicy")} for item in inventory]
+ print(json.dumps(result, ensure_ascii=False))
+ elif args.command == "show":
+ print(json.dumps(select(args.selection), ensure_ascii=False, indent=2))
+ elif args.command == "render":
+ text = render(select(args.selection))
+ if args.out:
+ args.out.write_text(text + "\n")
+ else:
+ print(text)
+ elif args.command == "plan":
+ save(args.out, compile_plan(select(args.selection), set(args.topologies.split(",")), args.run_id))
+ else:
+ result = audit_evidence(json.loads(args.plan.read_text()), args.evidence_dir)
+ save(args.out, result)
+ return 0 if not result["problems"] else 1
+ return 0
+
+
+if __name__ == "__main__":
+ try:
+ sys.exit(main())
+ except (OSError, KeyError, ValueError) as exc:
+ print(f"Heal validation contract error: {exc}", file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_heal/mpu_race_backups.py b/scripts/validation_heal/mpu_race_backups.py
new file mode 100644
index 00000000..ecd1d2b4
--- /dev/null
+++ b/scripts/validation_heal/mpu_race_backups.py
@@ -0,0 +1,186 @@
+"""Stopped G02/G05 backups, decoded from the same held file that was hashed."""
+import hashlib
+import json
+import os
+import re
+import stat
+import uuid
+
+KEY = 'objects-0256.bin'
+LIMIT = 1024**2
+
+
+def require(value, message):
+ if not value:
+ raise ValueError(message)
+
+
+def stable(info):
+ return (info.st_mode,info.st_uid,info.st_gid,info.st_dev,info.st_ino,info.st_nlink,
+ info.st_size,info.st_mtime_ns,info.st_ctime_ns)
+
+
+def run(request,node):
+ require(set(request)=={'runId','action','bucket','expectedBinarySha256','expectedInspectorSha256'}
+ and request['action'] in {'mpu-race-backups','version-race-backups'}
+ and re.fullmatch(r'rv-heal-[0-9a-f]{16}',request['bucket']),
+ 'G05 backup census requires the exact original owned key')
+ key = KEY if request['action']=='mpu-race-backups' else 'objects/0256.bin'
+ generations = 3 if request['action']=='mpu-race-backups' else 6
+ state,roots,owner=node['owned'](request['runId'])
+ require((owner.get('runtimeProfile')=='manual' or
+ owner.get('runtimeProfile')=='frozen-race-legacy' and request['action']=='mpu-race-backups')
+ and owner.get('bucket')==request['bucket']
+ and owner.get('binarySha256')==request['expectedBinarySha256']
+ and owner.get('inspectorSha256')==request['expectedInspectorSha256']
+ and all(re.fullmatch(r'[0-9a-f]{64}',request[k]) for k in ('expectedBinarySha256','expectedInspectorSha256'))
+ and len(roots)==len(node['MOUNTS'])==4,'G05 backup owner/binary/inspector differs')
+ node['stopped'](owner)
+ tool=node['checked'](state/'heal-meta-inspect',state)
+ require(node['digest'](tool)==owner['inspectorSha256'] and node['digest'](state/'rustfs')==owner['binarySha256'],
+ 'G05 original candidate or inspector changed')
+ fault=node['fault'];rows=[]
+ for disk,(root,mount) in enumerate(zip(roots,node['MOUNTS'],strict=True)):
+ require(root==mount/'heal-validation'/request['runId'],'G05 backup root escaped the original mount')
+ with fault.directory(mount) as mount_fd, fault.parent(mount_fd,['heal-validation',request['runId'],'data',request['bucket'],*key.split('/')]) as key_fd:
+ before_key=os.fstat(key_fd);names=sorted(os.listdir(key_fd))
+ require(len(names)<=generations+1 and 'xl.meta' in names,'G02/G05 key population differs from its bounded version fixture')
+ for name in names:
+ if name=='xl.meta':continue
+ require(str(uuid.UUID(name))==name and uuid.UUID(name).int!=0,'G05 backup parent is not an original generation UUID')
+ with fault.parent(key_fd,[name]) as part_fd:
+ before_parent=os.fstat(part_fd)
+ try:fd=os.open('xl.meta.bkp',os.O_RDONLY|os.O_NOFOLLOW|os.O_NONBLOCK,dir_fd=part_fd)
+ except FileNotFoundError:
+ require(stable(os.fstat(part_fd))==stable(before_parent),
+ 'G05 backup population changed during absent-file census')
+ continue
+ try:
+ before=os.fstat(fd)
+ require(stat.S_ISREG(before.st_mode) and before.st_nlink==1 and before.st_size<=LIMIT
+ and fault.mount_id(fd)==fault.mount_id(part_fd),
+ 'G05 backup is oversized, linked, special or on a different mount')
+ raw=os.read(fd,LIMIT+1)
+ require(len(raw)==before.st_size and stable(before)==stable(os.fstat(fd)),
+ 'G05 backup changed while hashing')
+ os.lseek(fd,0,os.SEEK_SET)
+ # Linux parent-fd access keeps the decoder on the checked
+ # inode even if an attacker replaces its pathname.
+ observed=node['command'](str(tool),f'/proc/{os.getpid()}/fd/{fd}',request['bucket'],key,check=False)
+ require(observed.returncode==0 and len(observed.stdout)<=LIMIT,
+ 'G05 backup metadata decoder failed or exceeded its bound')
+ decoded=json.loads(observed.stdout)
+ require(isinstance(decoded,dict) and decoded.get('bucket')==request['bucket']
+ and decoded.get('key')==key and decoded.get('physicalBitrotVerified') is False,
+ 'G05 backup decoder identity or proof role differs')
+ require(stable(os.fstat(fd))==stable(before)
+ and stable(os.stat('xl.meta.bkp',dir_fd=part_fd,follow_symlinks=False))==stable(before),
+ 'G05 backup inode changed during descriptor-bound decode')
+ rows.append({'disk':disk,'path':f"{request['bucket']}/{key}/{name}/xl.meta.bkp",
+ 'size':len(raw),'sha256':hashlib.sha256(raw).hexdigest(),'metadata':decoded})
+ finally:os.close(fd)
+ require(stable(os.fstat(part_fd))==stable(before_parent),'G05 backup generation changed during census')
+ require(stable(os.fstat(key_fd))==stable(before_key) and sorted(os.listdir(key_fd))==names,
+ 'G05 committed key population changed during backup census')
+ node['stopped'](owner)
+ require(node['owned'](request['runId'])==(state,roots,owner)
+ and node['digest'](tool)==owner['inspectorSha256'] and node['digest'](state/'rustfs')==owner['binarySha256'],
+ 'G05 original ownership or tools changed during backup census')
+ return {'hostname':owner['hostname'],'rows':rows,'inspectorSha256':owner['inspectorSha256'],
+ 'binarySha256':owner['binarySha256'],'quiescent':True,'readOnly':True,
+ 'sameHeldFileHashedAndDecoded':True,'rawContentsExported':False,'liveAuthorityClaimed':False,
+ 'physicalRepairProved':False,'canonicalCasePassClaimed':False}
+
+
+def version_map(metadata):
+ versions=metadata.get('versions')
+ require(isinstance(versions,list) and 1<=len(versions)<=6,'G02/G05 backup needs bounded acknowledged data versions')
+ result={}
+ for version in versions:
+ vid=version.get('versionId')
+ require(isinstance(vid,str) and str(uuid.UUID(vid))==vid and uuid.UUID(vid).int!=0
+ and vid not in result and version.get('deleted') is False and type(version.get('isLatest')) is bool,
+ 'G05 backup version identity, kind or latest flag differs')
+ result[vid]=version
+ require(sum(v['isLatest'] for v in result.values())==1,'G05 backup needs exactly one latest data version')
+ return result
+
+
+def verify(before,after,observations,nodes,bucket,old_version,new_version,binary_sha,inspector_sha):
+ return verify_versions(before,after,observations,nodes,bucket,KEY,[old_version],[new_version],binary_sha,inspector_sha)
+
+
+def verify_versions(before,after,observations,nodes,bucket,key,old_versions,new_versions,binary_sha,inspector_sha):
+ """Adapt frozen additive_backups to H1/H2; backups never supply live authority."""
+ require(re.fullmatch(r'rv-heal-[0-9a-f]{16}',bucket)
+ and all(isinstance(v,str) and re.fullmatch(r'[0-9a-f]{64}',v) for v in (binary_sha,inspector_sha))
+ and len(nodes) in (1,4) and len(set(nodes))==len(nodes)
+ and set(before)==set(after)==set(observations)==set(nodes)
+ and (key==KEY and len(old_versions)==len(new_versions)==1
+ or key=='objects/0256.bin' and len(old_versions)==len(new_versions)==3)
+ and len(set(old_versions+new_versions))==len(old_versions)+len(new_versions),
+ 'G05 backup proof omitted or mixed an original host/version')
+ proofs=[];prefix=f'{bucket}/{key}/'
+ for node in nodes:
+ evidence=observations[node]
+ require(evidence.get('quiescent') is True and evidence.get('readOnly') is True
+ and evidence.get('sameHeldFileHashedAndDecoded') is True
+ and evidence.get('binarySha256')==before[node].get('binarySha256')==after[node].get('binarySha256')==binary_sha
+ and evidence.get('inspectorSha256')==inspector_sha
+ and evidence.get('hostname')==before[node].get('hostname')==after[node].get('hostname')
+ and before[node].get('quiescent') is True and after[node].get('quiescent') is True
+ and evidence.get('liveAuthorityClaimed') is False and evidence.get('rawContentsExported') is False,
+ 'G05 backup proof lost its candidate/inspector/stopped binding')
+ def index(scene):
+ rows=[r for r in scene[node]['files'] if r['path'].startswith(prefix)]
+ values={(r['disk'],r['path']):r for r in rows}
+ require(len(values)==len(rows),'G05 snapshot contains duplicate physical backup identities')
+ return values
+ previous,current=index(before),index(after)
+ primary={}
+ for scene in (previous,current):
+ meta=[r for (disk,path),r in scene.items() if path==prefix+'xl.meta']
+ require(len(meta)==4 and all(type(r['disk']) is int for r in meta)
+ and {r['disk'] for r in meta}==set(range(4)), 'G05 backup scope lacks every original primary member')
+ require(all(r.get('metadata',{}).get('bucket')==bucket and r['metadata'].get('key')==key
+ and r['metadata'].get('physicalBitrotVerified') is False for r in meta),
+ 'G05 primary decoder scope or proof role differs')
+ for disk in range(4):
+ old=version_map(previous[disk,prefix+'xl.meta']['metadata'])
+ new=version_map(current[disk,prefix+'xl.meta']['metadata'])
+ require(set(old)==set(old_versions) and set(new)==set(old_versions+new_versions)
+ and old[old_versions[-1]]['isLatest'] and new[new_versions[-1]]['isLatest'],
+ 'G05 primary versions differ from original and Complete ACK order')
+ primary[disk]=(old,new)
+ candidates={k:v for k,v in current.items() if k[1].endswith('/xl.meta.bkp')}
+ rows=evidence['rows'];require(isinstance(rows,list) and len(rows)<=4*len(old_versions),'G05 backup decode rows exceed original generations')
+ decoded={(r['disk'],r['path']):r for r in rows}
+ require(len(decoded)==len(rows) and set(decoded)==set(candidates),'G05 backup decode population differs from snapshot')
+ for (disk,path),row in candidates.items():
+ require(type(disk) is int and disk in range(4),'G05 backup has a foreign local drive')
+ final=current[disk,prefix+'xl.meta']
+ old,new=primary[disk]
+ require(path in {prefix+old[vid]['dataDir']+'/xl.meta.bkp' for vid in old_versions},
+ 'G05 backup directory is not bound to the original committed generation')
+ witness=decoded[disk,path]
+ require(all(witness[k]==row[k] for k in ('disk','path','size','sha256')),
+ 'G05 backup decoder does not describe its exact snapshotted bytes')
+ value=witness['metadata'];versions=version_map(value)
+ require({k:v for k,v in value.items() if k!='versions'}==
+ {k:v for k,v in final['metadata'].items() if k!='versions'},
+ 'G05 backup decoder schema/object identity differs')
+ count=len(versions)-len(old_versions)
+ require(0<=count<=len(new_versions) and set(versions)==set(old_versions+new_versions[:count]),
+ 'G05 backup is not a contiguous prefix of acknowledged Complete versions')
+ latest=old_versions[-1] if count==0 else new_versions[count-1]
+ for vid,version in versions.items():
+ reference=old[vid] if vid in old else new[vid]
+ require(version['isLatest'] is (vid==latest)
+ and {k:v for k,v in version.items() if k!='isLatest'}==
+ {k:v for k,v in reference.items() if k!='isLatest'},
+ 'G05 backup version descriptor changed outside the acknowledged prefix')
+ proofs.append({'node':node,'disk':disk,'path':path,'sha256':row['sha256'],'size':row['size'],
+ 'acknowledgedCompletePrefix':count,'decoderFieldsVerified':True,
+ 'unexposedMetadataVerified':False,'liveAuthorityClaimed':False})
+ return {'proofs':proofs,'allSnapshottedBackupsVerified':True,'liveAuthorityClaimed':False,
+ 'physicalRepairProved':False,'canonicalCasePassClaimed':False}
diff --git a/scripts/validation_heal/mpu_race_committed.py b/scripts/validation_heal/mpu_race_committed.py
new file mode 100644
index 00000000..368b25bb
--- /dev/null
+++ b/scripts/validation_heal/mpu_race_committed.py
@@ -0,0 +1,155 @@
+"""Frozen G05 committed multipart geometry and bytes; caller owns staging/S3 proof."""
+import copy
+import hashlib
+import re
+import uuid
+
+try:
+ from . import mpu_race_fixture as model
+ from .heal_probe import require
+ from .shards import files, map_target
+ from .mpu_race_backups import verify as verify_backups
+except ImportError:
+ import mpu_race_fixture as model
+ from heal_probe import require
+ from shards import files, map_target
+ from mpu_race_backups import verify as verify_backups
+
+
+def entries(manifest):
+ require(manifest.get('key') == model.KEY and re.fullmatch(r'rv-heal-[0-9a-f]{16}', manifest.get('bucket', ''))
+ and manifest.get('poisoned') is None and set(manifest.get('items', {})) == set(model.ROLES),
+ 'G05 committed proof requires the original known same-key model')
+ completed = [manifest['items'][role] for role in model.ROLES if manifest['items'][role]['state'] == 'complete']
+ require([item['role'] for item in completed] in (['committed'], ['committed', 'a']),
+ 'G05 committed population differs from the original and A Complete ACKs')
+ result, seen = [], set()
+ for index, item in enumerate(completed):
+ version = item['versionId']
+ require(isinstance(version, str) and str(uuid.UUID(version)) == version and uuid.UUID(version).int != 0
+ and version not in seen and len(item['parts']) == 3, 'G05 Complete lacks one distinct real version')
+ seen.add(version); digest = hashlib.sha256()
+ for number, part in enumerate(item['parts'], 1):
+ body = model.payload(manifest['bucket'], item['role'], number)
+ require(type(part.get('number')) is int and part['number'] == number
+ and type(part.get('size')) is int and part['size'] == len(body)
+ and part.get('etag') == model.etag(body)
+ and part.get('sha256') == hashlib.sha256(body).hexdigest(),
+ 'G05 committed part differs from its exact original role payload/ACK')
+ digest.update(body)
+ require(item['etag'] == model.multipart_etag(item['parts'])
+ and type(item['payload'].get('size')) is int
+ and item['payload'] == {'size': sum(model.PART_SIZES), 'sha256': digest.hexdigest()},
+ 'G05 Complete payload or ordered multipart ETag differs')
+ result.append({'key': model.KEY, 'versionId': version, 'isLatest': index == len(completed) - 1,
+ 'deleteMarker': False, 'payload': copy.deepcopy(item['payload']),
+ 'multipartParts': [{'number': part['number'], 'size': part['size']} for part in item['parts']]})
+ return result
+
+
+def mapped(snapshot, bucket, expected, *, backup_directories=()):
+ """Bind all three parts on every original coding index; reject unknown leaves."""
+ require(len(snapshot) in (1, 4), 'G05 committed topology needs one or four original hosts')
+ prefix = f'{bucket}/{model.KEY}/'
+ scope = {node: {**value, 'files': [row for row in value['files'] if row['path'].startswith(prefix)]}
+ for node, value in snapshot.items()}
+ require(all(type(row.get('disk')) is int and row['disk'] in range(4)
+ and type(row.get('size')) is int and row['size'] >= 0
+ and isinstance(row.get('sha256'), str) and re.fullmatch(r'[0-9a-f]{64}', row['sha256'])
+ for value in scope.values() for row in value['files']),
+ 'G05 committed file identity or digest/size is invalid')
+ indexed = files(scope); cohorts = {}; used = set(); auxiliary = {}
+ expected_versions = sorted((row['versionId'], row['isLatest'], False) for row in expected)
+ for node, value in scope.items():
+ require(len(value['formats']) == 4 and all(type(row.get('disk')) is int for row in value['formats'])
+ and {row['disk'] for row in value['formats']} == set(range(4)), 'G05 committed scope lacks four distinct original drives')
+ for drive in value['formats']:
+ identity = (node, drive['disk'], prefix + 'xl.meta')
+ metadata = indexed.get(identity, {}).get('metadata', {})
+ versions = metadata.get('versions', [])
+ require(metadata.get('bucket') == bucket and metadata.get('key') == model.KEY
+ and all(type(v.get('isLatest')) is bool and v.get('deleted') is False for v in versions)
+ and sorted((v['versionId'], v['isLatest'], v['deleted']) for v in versions) == expected_versions,
+ 'G05 primary version/latest population differs from committed ACKs')
+ used.add(identity)
+ directories = set()
+ for entry in expected:
+ for number in (1, 2, 3):
+ target = map_target(scope, bucket, model.KEY, 'data', number, entry['versionId'], latest_required=False)
+ logical = target['logical']
+ require(type(logical['size']) is int and logical['size'] == entry['payload']['size'] and uuid.UUID(target['dataDir']).int != 0
+ and all(type(p['number']) is int and type(p['size']) is int and type(p['actualSize']) is int for p in logical['parts'])
+ and [{'number': p['number'], 'size': p['size']} for p in logical['parts']] == entry['multipartParts']
+ and type(logical['erasure']['block_size']) is int and logical['erasure']['block_size'] > 0
+ and all(type(index) is int for index in logical['erasure']['distribution'])
+ and all(type(part['index']) is int for part in target['cohort'])
+ and logical['erasure']['data_blocks'] == (2 if len(snapshot) == 1 else 12)
+ and logical['erasure']['parity_blocks'] == (2 if len(snapshot) == 1 else 4),
+ 'G05 committed total/part sizes or original EC geometry differs')
+ cohorts[entry['versionId'], number] = target
+ for part in target['cohort']:
+ identity = (part['node'], part['disk'], part['relativePath']); used.add(identity)
+ sidecar = (part['node'], part['disk'], part['relativePath'] + '.meta')
+ if sidecar in indexed:
+ require(indexed[sidecar]['size'] > 0, 'G05 upload auxiliary sidecar is empty')
+ used.add(sidecar); auxiliary[sidecar] = indexed[sidecar]
+ generation = {cohorts[entry['versionId'], n]['dataDir'] for n in (1, 2, 3)}
+ require(len(generation) == 1 and not directories.intersection(generation),
+ 'G05 parts or distinct versions share the wrong committed generation')
+ directories.update(generation)
+ for identity, row in indexed.items():
+ if identity in used:
+ continue
+ suffix = row['path'][len(prefix):]
+ require(any(suffix == directory + '/xl.meta.bkp' for directory in backup_directories),
+ 'G05 committed key contains an unknown or unacknowledged leaf')
+ return cohorts, auxiliary
+
+
+def transition(before, after, original, final, backup_observations, binary_sha, inspector_sha):
+ """Port physical_final; staging continuity and full S3 still close new payloads."""
+ require(set(before) == set(after) and len(before) in (1, 4)
+ and re.fullmatch(r'[0-9a-f]{64}', binary_sha)
+ and original['bucket'] == final['bucket'] and original['key'] == final['key'] == model.KEY,
+ 'G05 committed transition changed original hosts, candidate or bucket/key')
+ for node in before:
+ require(before[node]['formats'] == after[node]['formats']
+ and before[node].get('quiescent') is True and after[node].get('quiescent') is True
+ and type(before[node].get('pid')) is int and type(after[node].get('pid')) is int
+ and before[node].get('pid') == after[node].get('pid') == 0
+ and before[node].get('binarySha256') == after[node].get('binarySha256') == binary_sha
+ and before[node].get('hostname') == after[node].get('hostname'),
+ 'G05 committed proof lost stopped process, original formats or candidate')
+ previous, current = entries(original), entries(final)
+ require(len(previous) == 1 and len(current) == 2 and previous[0]['versionId'] == current[0]['versionId']
+ and original['items']['committed'] == final['items']['committed']
+ and original['items']['a']['state'] == original['items']['b']['state'] == 'pending'
+ and len(original['items']['a']['parts']) in (2, 3) and len(original['items']['b']['parts']) == 3
+ and original['items']['a']['parts'] == final['items']['a']['parts'][:len(original['items']['a']['parts'])]
+ and original['items']['a']['metadata'] == final['items']['a']['metadata']
+ and {k: v for k, v in original['items']['b'].items() if k != 'state'}
+ == {k: v for k, v in final['items']['b'].items() if k != 'state'}
+ and final['items']['a']['state'] == 'complete' and final['items']['b']['state'] == 'aborted'
+ and all(original['items'][role]['uploadId'] == final['items'][role]['uploadId'] for role in model.ROLES),
+ 'G05 Complete replaced the old version or original sibling upload identities')
+ old, old_aux = mapped(before, original['bucket'], previous)
+ old_version = previous[0]['versionId']; new_version = current[1]['versionId']
+ old_dir = old[old_version, 1]['dataDir']
+ new, new_aux = mapped(after, final['bucket'], current, backup_directories=[old_dir])
+ for number in (1, 2, 3):
+ left, right = old[old_version, number], new[old_version, number]
+ require({k: v for k, v in left['logical'].items() if k != 'isLatest'}
+ == {k: v for k, v in right['logical'].items() if k != 'isLatest'}
+ and {p['index']: (p['sha256'], p['size']) for p in left['cohort']}
+ == {p['index']: (p['sha256'], p['size']) for p in right['cohort']},
+ 'G05 Complete changed an original committed multipart descriptor or shard')
+ require(new[new_version, 1]['dataDir'] != old_dir, 'G05 A Complete reused the old generation')
+ for identity, row in old_aux.items():
+ require(identity not in new_aux or new_aux[identity] == row, 'G05 retained upload sidecar bytes changed')
+ backups = verify_backups(before, after, backup_observations, list(before), original['bucket'],
+ old_version, new_version, binary_sha, inspector_sha)
+ return new, {'oldVersionRetained': True, 'newVersionExactlyOnce': True,
+ 'allPartsCodingIndicesVerified': True, 'backupProof': backups,
+ 'reclaimedOldUploadSidecars': [list(identity) for identity in old_aux if identity not in new_aux],
+ 'uploadSidecarsAreCommittedAuthority': False, 'stagingContinuityStillRequired': True,
+ 'fullPayloadS3StillRequired': True, 'physicalRepairProved': False, 'canonicalCasePassClaimed': False}
diff --git a/scripts/validation_heal/mpu_race_fixture.py b/scripts/validation_heal/mpu_race_fixture.py
new file mode 100644
index 00000000..1d36761c
--- /dev/null
+++ b/scripts/validation_heal/mpu_race_fixture.py
@@ -0,0 +1,200 @@
+#!/usr/bin/env python3
+"""G05 ACK-bound same-key multipart fixture; no topology or Heal PASS claim.
+
+One previously committed MPU plus two independent uploads for the same key.
+Caller owns the fresh versioned bucket, endpoint, physical witnesses and race.
+"""
+import copy
+import hashlib
+import json
+import re
+import threading
+import uuid
+
+try:
+ from .heal_probe import require, save
+except ImportError:
+ from heal_probe import require, save
+
+# Match the frozen common DM recipe while retaining its same-key A/B model.
+PART_SIZES=(5*1024**2+17,5*1024**2+93,1024**2+47)
+ROLES=('committed','a','b')
+KEY='objects-0256.bin'
+
+
+def payload(bucket,role,number):
+ require(role in ROLES and type(number) is int and 1<=number<=3,'registered multipart payload required')
+ return hashlib.shake_256(f'G05:{bucket}:{role}:{number}'.encode()).digest(PART_SIZES[number-1])
+
+
+def etag(body):return '"'+hashlib.md5(body,usedforsecurity=False).hexdigest()+'"'
+
+
+def multipart_etag(parts):
+ require([p['number'] for p in parts]==[1,2,3],'exact ordered three-part ACK manifest required')
+ require(all(re.fullmatch(r'"[0-9a-f]{32}"',p['etag']) for p in parts),'plain fixture part MD5 required')
+ raw=b''.join(bytes.fromhex(p['etag'][1:-1]) for p in parts)
+ return '"'+hashlib.md5(raw,usedforsecurity=False).hexdigest()+'-3"'
+
+
+def pages(recorder,operation,params):
+ markers={'list_parts':('PartNumberMarker','NextPartNumberMarker'),
+ 'list_multipart_uploads':('KeyMarker','NextKeyMarker','UploadIdMarker','NextUploadIdMarker'),
+ 'list_object_versions':('KeyMarker','NextKeyMarker','VersionIdMarker','NextVersionIdMarker')}
+ require(operation in markers,'only bounded owned multipart/version listings allowed')
+ request=copy.deepcopy(params);seen=set();out=[]
+ for _ in range(32):
+ value=recorder.call(operation,request);out.append(value)
+ require(type(value.get('IsTruncated')) is bool,'listing omitted typed truncation flag')
+ if not value['IsTruncated']:return out
+ fields=markers[operation];next_values={fields[i]:value.get(fields[i+1]) for i in range(0,len(fields),2)}
+ if operation=='list_parts':
+ require(type(next_values['PartNumberMarker']) is int and next_values['PartNumberMarker']>request.get('PartNumberMarker',0),
+ 'part pagination did not advance')
+ else:require(all(isinstance(v,str) and v for v in next_values.values()),'listing omitted continuation identity')
+ identity=json.dumps(next_values,sort_keys=True)
+ require(identity not in seen,'listing repeated a continuation cursor');seen.add(identity)
+ request.update(next_values)
+ raise ValueError('owned listing exceeds 32-page budget')
+
+
+class MpuRaceFixture:
+ def __init__(self,recorder,bucket,key=KEY):
+ require(key==KEY and isinstance(bucket,str) and re.fullmatch('[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]',bucket),
+ 'fixed G05 key and owned fixture bucket required')
+ self.recorder=recorder;self.bucket=bucket;self.key=key
+ self.items={};self.events=[];self.poisoned=None;self.lock=threading.RLock();self.seed_started=False
+
+ def manifest(self):
+ with self.lock:return copy.deepcopy(dict(bucket=self.bucket,key=self.key,items=self.items,events=self.events,
+ poisoned=self.poisoned,fixture='DM',topologyProved=False,physicalRepairProved=False,overlapProved=False,
+ casePassClaimed=False,matrix84PassClaimed=False))
+
+ def _persist(self):
+ (getattr(self.recorder,'writer',None) or save)(self.recorder.output/f'mpu-manifest-{len(self.events):04d}.json',self.manifest())
+
+ def _mutation(self,role,operation,params,apply,body=None):
+ require(self.poisoned is None,'multipart fixture halted; do not retry or mutate sibling upload')
+ response=None
+ try:
+ response=self.recorder.call(operation,params,body,identity={'mpuRole':role},
+ expect_http=204 if operation=='abort_multipart_upload' else 200)
+ apply(response)
+ except BaseException as exc:
+ self.poisoned=f'{type(exc).__name__}: {exc}'
+ self.events.append(dict(role=role,operation=operation,params=copy.deepcopy(params),
+ state='UNRESOLVED',responseReceived=response is not None,error=self.poisoned))
+ self._persist();raise
+ self.events.append(dict(role=role,operation=operation,params=copy.deepcopy(params),state='ACK',response=copy.deepcopy(response)))
+ try:self._persist()
+ except BaseException as exc:
+ self.poisoned=f'manifest persistence failed: {exc}';raise
+ return response
+
+ def create(self,role):
+ with self.lock:
+ require(len(self.items)<3 and role==ROLES[len(self.items)],'unknown/repeated/out-of-order fixture upload role')
+ if role!='committed':require(self.items['committed']['state']=='complete','original committed object must exist first')
+ metadata={'case':'G05','role':role,'nonce':hashlib.sha256(f'{self.bucket}:{role}'.encode()).hexdigest()[:24]}
+ def apply(response):
+ upload=response.get('UploadId')
+ require(isinstance(upload,str) and 1<=len(upload)<=256 and
+ upload not in {v['uploadId'] for v in self.items.values()},'missing/reused UploadId')
+ self.items[role]=dict(role=role,uploadId=upload,state='pending',parts=[],metadata=metadata)
+ return self._mutation(role,'create_multipart_upload',{'Bucket':self.bucket,'Key':self.key,
+ 'Metadata':metadata,'ContentType':'application/octet-stream'},apply)
+
+ def upload(self,role,number):
+ with self.lock:
+ item=self.items[role]
+ require(item['state']=='pending' and type(number) is int and number==len(item['parts'])+1 and number<=3,
+ 'only next known pending part can be uploaded; no retry/overwrite')
+ body=payload(self.bucket,role,number)
+ def apply(response):
+ require(response.get('ETag')==etag(body),'UploadPart ACK differs from exact submitted bytes')
+ item['parts'].append(dict(number=number,size=len(body),etag=response['ETag'],sha256=hashlib.sha256(body).hexdigest()))
+ return self._mutation(role,'upload_part',{'Bucket':self.bucket,'Key':self.key,'UploadId':item['uploadId'],
+ 'PartNumber':number},apply,body)
+
+ def complete(self,role):
+ with self.lock:
+ require(role in ('committed','a'),'B is the independently aborted upload')
+ item=self.items[role];require(item['state']=='pending','upload is already terminal')
+ if role=='committed':require(set(self.items)=={'committed'},'original MPU must complete before pending pair')
+ expected=multipart_etag(item['parts'])
+ def apply(response):
+ vid=response.get('VersionId')
+ require(isinstance(vid,str) and str(uuid.UUID(vid))==vid and uuid.UUID(vid).int!=0 and
+ vid not in {v.get('versionId') for v in self.items.values()},'missing/reused committed VersionId')
+ require(response.get('ETag')==expected,'Complete ACK multipart ETag differs from ordered ACK parts')
+ body=b''.join(payload(self.bucket,role,n) for n in (1,2,3))
+ item.update(state='complete',versionId=vid,etag=expected,payload={'size':len(body),'sha256':hashlib.sha256(body).hexdigest()})
+ return self._mutation(role,'complete_multipart_upload',{'Bucket':self.bucket,'Key':self.key,
+ 'UploadId':item['uploadId'],'MultipartUpload':{'Parts':[{'PartNumber':p['number'],'ETag':p['etag']} for p in item['parts']]}},apply)
+
+ def abort_b(self):
+ with self.lock:
+ item=self.items['b'];require(item['state']=='pending','B is already terminal')
+ return self._mutation('b','abort_multipart_upload',{'Bucket':self.bucket,'Key':self.key,'UploadId':item['uploadId']},
+ lambda response:item.update(state='aborted'))
+
+ def seed(self):
+ with self.lock:
+ require(not self.seed_started and not self.items,'fresh fixture required');self.seed_started=True
+ self.create('committed')
+ for number in (1,2,3):self.upload('committed',number)
+ self.complete('committed')
+ self.create('a')
+ for number in (1,2):self.upload('a',number)
+ self.create('b')
+ for number in (1,2,3):self.upload('b',number)
+ return self.manifest()
+
+ def verify_s3(self,recorder):
+ """Verify one endpoint; parent repeats for required topology endpoints.
+
+ This deliberately requires current/old object identity while A/B are
+ pending, unlike merely checking HEAD404 on separate unused keys.
+ """
+ with self.lock:
+ require(self.poisoned is None and set(self.items)==set(ROLES),'complete known fixture model required')
+ items=copy.deepcopy(self.items)
+ uploads=[v for page in pages(recorder,'list_multipart_uploads',{'Bucket':self.bucket,'Prefix':self.key,'MaxUploads':1})
+ for v in page.get('Uploads',[])]
+ require(all(v.get('Key')==self.key for v in uploads) and len({v['UploadId'] for v in uploads})==len(uploads) and
+ {v['UploadId'] for v in uploads}=={v['uploadId'] for v in items.values() if v['state']=='pending'},
+ 'pending UploadId population differs; sibling upload affected')
+ for item in items.values():
+ params={'Bucket':self.bucket,'Key':self.key,'UploadId':item['uploadId'],'MaxParts':1}
+ if item['state']=='pending':
+ parts=[p for page in pages(recorder,'list_parts',params) for p in page.get('Parts',[])]
+ require([{'number':p['PartNumber'],'etag':p['ETag'],'size':p['Size']} for p in parts]==
+ [{k:p[k] for k in ('number','etag','size')} for p in item['parts']], 'pending part order/ETag/length differs')
+ else:
+ response=recorder.call('list_parts',params,expect_http=404)
+ require(response.get('Error',{}).get('Code')=='NoSuchUpload','terminal UploadId failed for wrong reason')
+ committed=[v for v in items.values() if v['state']=='complete']
+ latest=next(v for v in reversed(list(items.values())) if v['state']=='complete')
+ listed=pages(recorder,'list_object_versions',{'Bucket':self.bucket,'Prefix':self.key,'MaxKeys':1})
+ versions=[v for page in listed for v in page.get('Versions',[])]
+ require(not any(page.get('DeleteMarkers') for page in listed) and len(versions)==len(committed) and
+ all(v.get('Key')==self.key for v in versions) and
+ {v['VersionId'] for v in versions}=={v['versionId'] for v in committed},
+ 'Complete duplicated/lost a version or pending upload became visible')
+ for row in versions:
+ item=next(v for v in committed if v['versionId']==row['VersionId'])
+ require(row.get('IsLatest') is (item['role']==latest['role']) and row.get('ETag')==item['etag'] and
+ row.get('Size')==item['payload']['size'],'committed version order/ETag/size differs')
+ for item,selector in [(v,{'VersionId':v['versionId']}) for v in committed]+[(latest,{})]:
+ params={'Bucket':self.bucket,'Key':self.key,**selector}
+ expected={'VersionId':item['versionId'],'ETag':item['etag'],'Metadata':item['metadata'],
+ 'ContentLength':item['payload']['size'],'ContentType':'application/octet-stream'}
+ head=recorder.call('head_object',params)
+ require(all(head.get(k)==v for k,v in expected.items()),'HEAD changed committed/current object')
+ recorder.call('get_object',params)
+ observed=json.loads((recorder.output/f'{recorder.sequence:06d}-ack.json').read_text())['observation']
+ require(observed.get('params')==params and observed.get('payload')==item['payload'] and
+ all(observed['response'].get(k)==v for k,v in expected.items()),'GET committed/current bytes or properties differ')
+ with self.lock:require(self.poisoned is None and self.items==items,'multipart model changed during verification')
+ return dict(committedVersions=len(committed),pendingUploads=len(uploads),sameKeySiblingIsolationVerified=True,
+ physicalRepairProved=False,overlapProved=False,casePassClaimed=False)
diff --git a/scripts/validation_heal/mpu_race_physical.py b/scripts/validation_heal/mpu_race_physical.py
new file mode 100644
index 00000000..83d6a8fd
--- /dev/null
+++ b/scripts/validation_heal/mpu_race_physical.py
@@ -0,0 +1,230 @@
+"""ACK-selected G05 staging census and continuity; not a Heal/EC proof."""
+import base64
+import hashlib
+import os
+import re
+import stat
+import uuid
+
+KEY = 'objects-0256.bin'
+ROLES = ('committed', 'a', 'b')
+MAX_ENTRIES = 128
+MAX_BYTES = 256 * 1024**2
+
+
+def require(value, message):
+ if not value:
+ raise ValueError(message)
+
+
+def selected(bucket, items):
+ """Retain the frozen H1 upload-ID decoder, bound to the auto-test owner."""
+ require(re.fullmatch(r'rv-heal-[0-9a-f]{16}', bucket) and isinstance(items, list) and len(items) == 3,
+ 'G05 requires one owned bucket and all three ACK-selected uploads')
+ out, roles, uploads = [], set(), set()
+ for item in items:
+ require(set(item) == {'role', 'uploadId', 'bucket', 'key'} and item['role'] in ROLES
+ and item['role'] not in roles and item['bucket'] == bucket and item['key'] == KEY,
+ 'foreign or duplicate G05 upload selection')
+ encoded = item['uploadId']
+ require(isinstance(encoded, str) and re.fullmatch(r'[A-Za-z0-9_-]{1,256}', encoded)
+ and encoded not in uploads, 'invalid or reused G05 upload identity')
+ raw = base64.b64decode(encoded + '=' * ((-len(encoded)) % 4), altchars=b'-_', validate=True)
+ require(base64.urlsafe_b64encode(raw).decode().rstrip('=') == encoded, 'noncanonical upload encoding')
+ deployment, suffix = raw.decode().split('.', 1)
+ require(str(uuid.UUID(deployment)) == deployment and uuid.UUID(deployment).int != 0,
+ 'invalid upload deployment UUID')
+ match = re.fullmatch(r'([0-9a-f-]{36})x([0-9]{1,30})', suffix)
+ require(match is not None and str(uuid.UUID(match[1])) == match[1] and uuid.UUID(match[1]).int != 0,
+ 'invalid upload suffix')
+ key_hash = hashlib.sha256((bucket + '/' + KEY).encode()).hexdigest()
+ out.append({**item, 'relative': '.rustfs.sys/multipart/' + key_hash + '/' + suffix})
+ roles.add(item['role']); uploads.add(encoded)
+ require(roles == set(ROLES), 'G05 upload selection omitted an original role')
+ return out
+
+
+def stable(info):
+ return (info.st_mode, info.st_uid, info.st_gid, info.st_dev, info.st_ino, info.st_nlink,
+ info.st_size, info.st_mtime_ns, info.st_ctime_ns)
+
+
+def run(request, node):
+ require(set(request) == {'runId', 'action', 'bucket', 'items', 'expectedBinarySha256', 'quiescent'}
+ and request['action'] == 'mpu-race-scene' and type(request['quiescent']) is bool,
+ 'G05 census accepts an exact owned read-only upload scope')
+ state, roots, owner = node['owned'](request['runId'])
+ require(owner.get('runtimeProfile') in {'manual', 'frozen-race-legacy'} and owner.get('bucket') == request['bucket']
+ and owner.get('binarySha256') == request['expectedBinarySha256']
+ and re.fullmatch(r'[0-9a-f]{64}', request['expectedBinarySha256']) and len(roots) == 4,
+ 'G05 census owner/runtime/binary differs')
+ items = selected(request['bucket'], request['items'])
+ process = None
+ if request['quiescent']:
+ node['stopped'](owner)
+ else:
+ process = node['process'](request)
+ require(process['pid'] > 1 and process['binarySha256'] == owner['binarySha256'],
+ 'live G05 staging census lacks its original owned process')
+ require(node['digest'](node['checked'](state / 'rustfs', state)) == owner['binarySha256'],
+ 'G05 census candidate changed')
+ rows, prefixes, total, count = [], [], 0, 0
+ for disk, root in enumerate(roots):
+ data = node['checked'](root / 'data', root)
+ device = data.lstat().st_dev
+ parent = node['checked']((data / items[0]['relative']).parent, data)
+ parent_before = parent.lstat() if parent.exists() else None
+ children = []
+ if parent_before:
+ for child in parent.iterdir():
+ require(len(children) < len(items), 'G05 key prefix exceeds its three original upload identities')
+ children.append(child.name)
+ children.sort()
+ require(set(children) <= {item['relative'].rsplit('/', 1)[-1] for item in items},
+ 'G05 key prefix contains an unacknowledged upload directory')
+ prefixes.append({'disk': disk, 'path': parent.relative_to(data).as_posix(),
+ 'present': parent_before is not None, 'children': children})
+ for item in items:
+ folder = node['checked'](data / item['relative'], data)
+ files, directories = {}, []
+ def visit(path, depth=0):
+ nonlocal total, count
+ path = node['checked'](path, data)
+ before = path.lstat()
+ count += 1
+ require(count <= MAX_ENTRIES, 'G05 staging entry budget exceeded')
+ require(depth <= 2 and before.st_dev == device and (stat.S_ISDIR(before.st_mode)
+ or stat.S_ISREG(before.st_mode) and before.st_nlink == 1),
+ 'G05 staging contains links, mounts, special files or excessive depth')
+ if stat.S_ISDIR(before.st_mode):
+ if path != folder: directories.append(path.relative_to(folder).as_posix())
+ children = []
+ for child in path.iterdir():
+ require(len(children) < MAX_ENTRIES, 'G05 staging directory population exceeds its entry budget')
+ children.append(child)
+ for child in sorted(children): visit(child, depth + 1)
+ else:
+ require(before.st_size <= 64 * 1024**2
+ and total + before.st_size <= MAX_BYTES, 'G05 staging file/byte budget exceeded')
+ fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
+ try:
+ require(stable(os.fstat(fd)) == stable(before), 'G05 file changed before hashing')
+ digest, size = hashlib.sha256(), 0
+ while chunk := os.read(fd, 1024**2):
+ digest.update(chunk); size += len(chunk)
+ require(size <= before.st_size, 'G05 staging file grew while hashing')
+ require(size == before.st_size and stable(os.fstat(fd)) == stable(before),
+ 'G05 staging changed while hashing')
+ finally:
+ os.close(fd)
+ files[path.relative_to(folder).as_posix()] = {'size': size, 'sha256': digest.hexdigest()}
+ total += size
+ require(stable(path.lstat()) == stable(before), 'G05 staging population changed during census')
+ exists = folder.exists()
+ if exists: visit(folder)
+ rows.append({'disk': disk, 'role': item['role'], 'uploadId': item['uploadId'],
+ 'relative': item['relative'], 'exists': exists, 'files': files, 'directories': directories})
+ parent_after = parent.lstat() if parent.exists() else None
+ require((parent_before is None and parent_after is None) or
+ (parent_before is not None and parent_after is not None and stable(parent_before) == stable(parent_after)),
+ 'G05 key prefix changed during staging census')
+ require(node['owned'](request['runId']) == (state, roots, owner), 'G05 owner or roots changed during census')
+ require(node['digest'](node['checked'](state / 'rustfs', state)) == owner['binarySha256'],
+ 'G05 candidate changed during staging census')
+ if request['quiescent']:
+ node['stopped'](owner)
+ else:
+ after = node['process'](request)
+ require(all(after.get(k) == process.get(k) for k in node['PROCESS_FIELDS']),
+ 'G05 owned process changed during live staging census')
+ return {'rows': rows, 'keyPrefixes': prefixes, 'bytes': total, 'binarySha256': owner['binarySha256'], 'quiescent': request['quiescent'],
+ 'process': process, 'readOnly': True, 'rawContentsExported': False,
+ 'physicalRepairProved': False, 'codingLayoutProved': False, 'overlapProved': False}
+
+
+def verify(manifest, evidence, nodes, binary_sha, *, previous=None, committed=None):
+ """Generalize the frozen four-disk continuity oracle to each H1/H2 host."""
+ require(len(nodes) in (1, 4) and len(set(nodes)) == len(nodes) and set(evidence) == set(nodes),
+ 'G05 staging proof omitted or duplicated an original host')
+ items = [{'role': role, 'uploadId': item['uploadId'], 'bucket': manifest['bucket'], 'key': manifest['key']}
+ for role, item in manifest['items'].items()]
+ requests = {item['role']: item for item in selected(manifest['bucket'], items)}
+ old = {} if previous is None else {(node, row['role'], row['disk']): row
+ for node, value in previous.items() for row in value['rows']}
+ if previous is not None:
+ require(set(previous) == set(nodes) and len(old) == 12 * len(nodes)
+ and all(value.get('readOnly') is True and value.get('binarySha256') == binary_sha
+ and len(value['rows']) == 12 for value in previous.values()),
+ 'G05 previous staging proof has incomplete ownership/candidate coverage')
+ live = {} if committed is None else {(node, row['disk'], row['path']): row
+ for node, value in committed.items() for row in value['files']}
+ data_dirs, completed, witnesses = {role: set() for role in ROLES}, 0, 0
+ for node, value in evidence.items():
+ rows = value['rows']; expected = {(role, disk) for role in ROLES for disk in range(4)}
+ require(value.get('readOnly') is True and value.get('binarySha256') == binary_sha
+ and type(value.get('quiescent')) is bool and len(rows) == len(expected)
+ and all(type(row['disk']) is int and row['role'] in ROLES for row in rows)
+ and {(row['role'], row['disk']) for row in rows} == expected,
+ 'G05 staging proof omitted or duplicated a drive/role')
+ prefixes = value.get('keyPrefixes', [])
+ require(len(prefixes) == 4 and all(type(p['disk']) is int for p in prefixes)
+ and {p['disk'] for p in prefixes} == set(range(4)),
+ 'G05 staging proof omitted or duplicated a complete key prefix')
+ expected_children = sorted(item['relative'].rsplit('/', 1)[-1] for role, item in requests.items()
+ if manifest['items'][role]['state'] == 'pending')
+ for prefix in prefixes:
+ require(prefix['path'] == requests['a']['relative'].rsplit('/', 1)[0]
+ and type(prefix['present']) is bool and prefix['children'] == expected_children
+ and (prefix['present'] or not expected_children),
+ 'G05 complete key-prefix population differs from acknowledged pending uploads')
+ for row in rows:
+ item, identity = manifest['items'][row['role']], requests[row['role']]
+ require(row['relative'] == identity['relative'] and row['uploadId'] == identity['uploadId'],
+ 'G05 staging identity differs from its original ACK')
+ require(all(type(v['size']) is int and v['size'] >= 0 and re.fullmatch(r'[0-9a-f]{64}', v['sha256'])
+ for v in row['files'].values()), 'G05 staging digest/size is invalid')
+ prior = old.get((node, row['role'], row['disk']))
+ if prior:
+ require(prior['uploadId'] == identity['uploadId'] and prior['relative'] == identity['relative'],
+ 'G05 previous staging identity belongs to another upload')
+ if item['state'] == 'pending':
+ require(row['exists'] is True and row['files'].get('xl.meta', {}).get('size', 0) > 0,
+ 'G05 pending upload lost its metadata')
+ parts, dirs = {}, set()
+ for path, part in row['files'].items():
+ match = re.fullmatch(r'([0-9a-f-]{36})/part\.([1-3])', path)
+ if match:
+ directory, number = match[1], int(match[2])
+ require(str(uuid.UUID(directory)) == directory and uuid.UUID(directory).int and part['size'] > 32
+ and number not in parts, 'G05 pending part has an invalid/duplicate generation')
+ parts[number] = path; dirs.add(directory)
+ require(set(parts) == {p['number'] for p in item['parts']} and len(dirs) == 1,
+ 'G05 pending physical parts differ from acknowledged parts')
+ require(set(row['files']) == {'xl.meta', *parts.values(), *(path + '.meta' for path in parts.values())}
+ and all(row['files'][path + '.meta']['size'] > 0 for path in parts.values())
+ and set(row['directories']) == dirs and len(row['directories']) == 1,
+ 'G05 pending upload has an unknown or missing staging leaf/directory')
+ data_dirs[row['role']].update(dirs)
+ if prior and prior['exists']:
+ require(all(row['files'].get(path) == part for path, part in prior['files'].items() if path != 'xl.meta'),
+ 'G05 continuation or sibling operation changed an acknowledged staging part')
+ if set(prior['files']) == set(row['files']):
+ require(prior['files'] == row['files'] and prior['directories'] == row['directories'],
+ 'G05 unchanged sibling metadata or population changed')
+ else:
+ require(item['state'] in ('complete', 'aborted') and row['exists'] is False
+ and not row['files'] and not row['directories'], 'G05 terminal upload staging remains')
+ if item['state'] == 'complete' and prior and prior['exists']:
+ paths = [p for p in prior['files'] if re.fullmatch(r'[0-9a-f-]{36}/part\.[1-3]', p)]
+ require(len(paths) == 3 and {p.rsplit('.', 1)[-1] for p in paths} == {'1', '2', '3'},
+ 'G05 Complete lacks three prior physical pending parts')
+ for path in paths:
+ current = live.get((node, row['disk'], manifest['bucket'] + '/' + KEY + '/' + path), {})
+ require({k: current.get(k) for k in ('size', 'sha256')} == prior['files'][path],
+ 'G05 completed part differs from original pending shard bytes')
+ completed += 1
+ witnesses += 1
+ require(all(len(dirs) <= 1 for dirs in data_dirs.values()), 'G05 pending dataDir differs across original hosts/drives')
+ return {'pendingAndTerminalWitnesses': witnesses, 'completedMembersCompared': completed,
+ 'codingLayoutProved': False, 'physicalRepairProved': False, 'overlapProved': False,
+ 'canonicalCasePassClaimed': False}
diff --git a/scripts/validation_heal/mpu_race_runner.py b/scripts/validation_heal/mpu_race_runner.py
new file mode 100644
index 00000000..1ff8c47a
--- /dev/null
+++ b/scripts/validation_heal/mpu_race_runner.py
@@ -0,0 +1,384 @@
+"""Frozen G05 same-key multipart execution; runtime admission is owned by race.py."""
+from concurrent.futures import ThreadPoolExecutor
+from datetime import datetime, timedelta, timezone
+import hashlib
+import json
+import os
+import secrets
+import time
+from urllib.parse import quote
+import uuid
+
+try:
+ from . import race, mpu_race_fixture as model, mpu_race_schedule as schedule
+ from . import mpu_race_physical as staging, mpu_race_committed as business
+ from .heal_probe import Probe, canonical, compare_inventories, observation_phase, payload, require, save
+ from .heal_workload import Recorder, verify_seed
+ from .healthy import Fleet, physical_content, validate_topology
+ from .inspector import verify as verify_inspector
+ from .cursor import validate_missing_batch, verify_part_manifest
+ from .shards import files, validate_repaired_physical
+ from .heal_fault import bounded_fault_id
+ from .status_keepalive import KeepAliveStatusProbe
+ from .checkpoint import records as journal_records
+ from .persistence import exact_records
+except ImportError:
+ import race, mpu_race_fixture as model, mpu_race_schedule as schedule
+ import mpu_race_physical as staging, mpu_race_committed as business
+ from heal_probe import Probe, canonical, compare_inventories, observation_phase, payload, require, save
+ from heal_workload import Recorder, verify_seed
+ from healthy import Fleet, physical_content, validate_topology
+ from inspector import verify as verify_inspector
+ from cursor import validate_missing_batch, verify_part_manifest
+ from shards import files, validate_repaired_physical
+ from heal_fault import bounded_fault_id
+ from status_keepalive import KeepAliveStatusProbe
+ from checkpoint import records as journal_records
+ from persistence import exact_records
+
+
+CASE = 'heal.race-multipart-complete-abort'
+
+
+def observe_status(probe, fleet, path, token, reference):
+ with probe.status_observation() as measured:
+ require(measured is not None, 'G05 warm/race status requires its original keepalive session')
+ request_value = {'path': path, 'query': [('clientToken', token)], 'method': 'POST', 'body': '',
+ 'statusObservation': {'sessionId': measured['sessionId'], 'sequence': measured['sequence']}}
+ with observation_phase(measured, 'requestPersistence'):
+ fleet.record('G05-status-request', request_value)
+ request_file = f'{fleet.sequence:05d}-G05-status-request.json'
+ with observation_phase(measured, 'requestEvidenceHash'):
+ request_sha = hashlib.sha256((fleet.evidence / request_file).read_bytes()).hexdigest()
+ value = probe.admin(path, [('clientToken', token)], b'', observation=measured)
+ value['requestSha256'] = request_sha
+ with observation_phase(measured, 'responsePersistence'):
+ fleet.record('G05-status-response', value)
+ response_file = f'{fleet.sequence:05d}-G05-status-response.json'
+ measured.update(requestFile=request_file, responseFile=response_file, responsePersisted=True)
+ race.settings(value, reference)
+ return value
+
+
+def ack_records(recorder, expected=None):
+ requests = sorted(recorder.output.glob('*-request.json'))
+ acks = sorted(recorder.output.glob('*-ack.json'))
+ count = recorder.sequence if expected is None else expected
+ require(len(requests) == len(acks) == recorder.sequence == count,
+ 'G05 SDK journal has an orphan, missing request/ACK or wrong population')
+ result = []
+ for sequence, (request_path, ack_path) in enumerate(zip(requests, acks, strict=True), 1):
+ request, ack = json.loads(request_path.read_text()), json.loads(ack_path.read_text())
+ observation = ack.get('observation') or {}
+ require(request_path.name == f'{sequence:06d}-request.json' and ack_path.name == f'{sequence:06d}-ack.json'
+ and request.get('runId') == ack.get('runId') == recorder.probe.binding['runId']
+ and request.get('sequence') == ack.get('sequence') == sequence
+ and ack.get('requestFile') == request_path.name
+ and ack.get('requestSha256') == hashlib.sha256(request_path.read_bytes()).hexdigest()
+ and ack.get('operationCheck') == 'PASS'
+ and observation.get('operation') == request.get('operation')
+ and observation.get('params') == request.get('params') and observation.get('resultUnknown') is False,
+ 'G05 original SDK request/ACK pairing, identity or result differs')
+ result.append(ack)
+ return result
+
+
+def seed(recorder, bucket, branch):
+ require(branch in schedule.SELECTED, 'unregistered G05 branch')
+ recorder.call('create_bucket', {'Bucket': bucket}, expect_http=200)
+ recorder.call('put_bucket_versioning', {'Bucket': bucket, 'VersioningConfiguration': {'Status': 'Enabled'}}, expect_http=200)
+ controls = []
+ for index in range(256):
+ key = f'objects-{index:04d}.bin'; body = payload(bucket + '/' + key, race.SIZE)
+ response = recorder.call('put_object', {'Bucket': bucket, 'Key': key}, body, expect_http=200)
+ version = response.get('VersionId')
+ require(isinstance(version, str) and str(uuid.UUID(version)) == version and uuid.UUID(version).int != 0
+ and response.get('ETag') == model.etag(body), 'G05 control lacks its real version and exact payload ETag')
+ controls.append({'key': key, 'versionId': version, 'isLatest': True, 'deleteMarker': False,
+ 'payload': {'size': len(body), 'sha256': hashlib.sha256(body).hexdigest()}})
+ fixture = model.MpuRaceFixture(recorder, bucket)
+ prepared = schedule.prepare(fixture, branch)
+ ack_records(recorder, 270 + int(branch != 'upload-continue'))
+ return controls, fixture, prepared
+
+
+def identities(manifest):
+ return [{'role': role, 'uploadId': item['uploadId'], 'bucket': manifest['bucket'], 'key': manifest['key']}
+ for role, item in manifest['items'].items()]
+
+
+def capture_staging(fleet, manifest, binary_sha, *, quiescent):
+ return {node: fleet.remote(node, 'mpu-race-scene', bucket=manifest['bucket'], items=identities(manifest),
+ expectedBinarySha256=binary_sha, quiescent=quiescent) for node in fleet.nodes}
+
+
+def observe_io(fleet, branch):
+ for node in fleet.nodes:
+ process = fleet.record('G05-io-process', {'node': node, **fleet.remote(node, 'process')})
+ value = fleet.record('G05-kernel-no-cap-sample', {'node': node, **fleet.remote(node, 'race-io-observe',
+ caseId=CASE, branch=branch, expectedProcess={k: v for k, v in process.items() if k != 'node'})})
+ require(value.get('inspectionOnly') is True and value.get('mutated') is False
+ and value.get('actualKernelNoLimitVerifiedAtSamples') is True
+ and value.get('continuousNoLimitProved') is False, 'G05 kernel IO observation is incomplete')
+
+
+def foreground(fixture, prepared, probe, fleet, path, token, reference, binary_sha):
+ """Only the selected first SDK call needs a contained original-token sample."""
+ samples = []
+ def checkpoint(label, manifest):
+ physical = capture_staging(fleet, manifest, binary_sha, quiescent=False)
+ value = {'phase': label, 'manifest': manifest, 'physical': physical}
+ # Worker uses independent names; Fleet.record's sequence stays on the controller.
+ save(fleet.evidence / 'mpu-checkpoints' / (label + '.json'), value)
+ return value
+ with ThreadPoolExecutor(max_workers=1) as executor:
+ worker = executor.submit(schedule.execute, fixture, prepared['branch'], prepared, checkpoint)
+ deadline = time.monotonic() + 90
+ try:
+ while not worker.done() and time.monotonic() < deadline and len(samples) < 1024:
+ sample = observe_status(probe, fleet, path, token, reference); samples.append(sample)
+ with observation_phase(probe.observation_for(sample), 'pollPause'):
+ time.sleep(.005)
+ if not worker.done() and time.monotonic() >= deadline:
+ fixture.recorder.mutations.poison('G05 original observation deadline exceeded; drain only')
+ except BaseException:
+ fixture.recorder.mutations.poison('G05 status observation failed; drain only')
+ raise
+ result = worker.result(timeout=90)
+ records = ack_records(fixture.recorder, 3 if prepared['branch'] == 'upload-continue' else 2)
+ selected = records[0]
+ require(selected['sequence'] == result['selectedAckSequence']
+ and selected['observation']['operation'] == prepared['selectedOperation']
+ and fixture.poisoned is None, 'G05 selected first mutation differs from original schedule')
+ matches = [sample['statusObservation'] for sample in samples
+ if race.sample_overlap(selected, sample, path, token, reference)]
+ proof = {'selectedOperation': prepared['selectedOperation'], 'selectedAckSequence': selected['sequence'],
+ 'matchingStatusSamples': matches, 'sampleCount': len(samples), 'sampleBudget': 1024,
+ 'pollPauseSeconds': .005, 'allCompanionACKsVerified': True, 'publicCallOverlapObserved': bool(matches),
+ 'nativeFenceProved': False, 'serverCommitOverlapProved': False, 'canonicalCasePassClaimed': False}
+ fleet.record('G05-selected-public-overlap', proof)
+ return result, bool(matches)
+
+
+def wait_terminal(probe, fleet, path, token, reference):
+ deadline = time.monotonic() + 180
+ while True:
+ value = observe_terminal(probe, fleet, path, token, reference)
+ if value['response']['summary'] != 'running': return value['response']
+ require(time.monotonic() < deadline, 'G05 original root completion exceeded180seconds')
+ time.sleep(.1)
+
+
+def observe_terminal(probe, fleet, path, token, reference):
+ value = fleet.record('G05-original-terminal-status', probe.admin(path, [('clientToken', token)], b''))
+ require(value.get('httpStatus') == 200 and isinstance(value.get('response'), dict)
+ and value['response'].get('summary') in {'running', 'finished', 'stopped'}
+ and canonical(value['response'].get('settings')) == canonical(reference['effective']),
+ 'G05 original terminal status identity/settings unavailable')
+ # A stopped root remains a failure in terminal_proof. Preserve it so the
+ # frozen source's independent final diagnostics still run.
+ return value
+
+
+def simple_entries(controls, manifest):
+ return controls + [{key: value for key, value in row.items() if key != 'multipartParts'}
+ for row in business.entries(manifest)]
+
+
+def physical_population(snapshot, bucket, controls):
+ keys = {row['key'] for row in controls} | {model.KEY}
+ require(all(row['path'].startswith(bucket + '/') and row['path'].split('/', 2)[1] in keys
+ for value in snapshot.values() for row in value['files']),
+ 'G05 physical population contains an unacknowledged object')
+
+
+def final_checks(probe, fleet, binding, controls, fixture, original, executed, before, before_staging,
+ mapped_controls, baseline, terminal, path, token, reference, binary_sha, inspector_sha):
+ """Keep independent frozen-source diagnostics after assertion failures.
+
+ Evidence I/O errors remain fatal. No failed assertion can authorize cleanup,
+ blind restoration or a later success claim.
+ """
+ violations = []
+ def check(label, operation):
+ try:
+ proof = operation()
+ except ValueError as error:
+ proof = {'state': 'FAIL', 'error': str(error), 'passClaimed': False}
+ violations.append({'stage': label, 'error': str(error)})
+ fleet.record(label, proof)
+ return proof
+
+ def repeat():
+ for _ in range(2):
+ repeated = observe_terminal(probe, fleet, path, token, reference)['response']
+ require(repeated['summary'] == terminal['summary']
+ and canonical(repeated.get('outcome')) == canonical(terminal.get('outcome')),
+ 'G05 original terminal responsibility changed on repetition')
+ return {'terminalStable': True}
+ check('G05-terminal-repeat', repeat)
+ fleet.stop(); after = fleet.snapshot('product-physical-before-GET-or-rescue', quiescent=True)
+ check('G05-final-physical-population', lambda: physical_population(after, fixture.bucket, controls))
+ def verify_controls():
+ for target in mapped_controls.values():
+ validate_repaired_physical(race.object_scene(before, fixture.bucket, target['key']),
+ race.object_scene(after, fixture.bucket, target['key']), target)
+ return {'physicalControlsReconstructed': 256}
+ check('G05-control-reconstruction', verify_controls)
+ backups = fleet.record('G05-original-backup-decoding', {node: fleet.remote(node, 'mpu-race-backups', bucket=fixture.bucket,
+ expectedBinarySha256=binary_sha, expectedInspectorSha256=inspector_sha) for node in fleet.nodes})
+ final = executed['manifest']
+ def verify_committed():
+ _, proof = business.transition(before, after, original, final, backups, binary_sha, inspector_sha)
+ return proof
+ physical = check('G05-committed-transition', verify_committed)
+ previous = before_staging
+ for step in executed['steps']:
+ checkpoint = step['checkpoint']
+ check('G05-staging-' + step['phase'], lambda: staging.verify(checkpoint['manifest'], checkpoint['physical'],
+ fleet.nodes, binary_sha, previous=previous, committed=after))
+ previous = checkpoint['physical']
+ def full_streams():
+ final_controls = race.map_fixture(after, fixture.bucket, controls)
+ # Independently map even when another committed-transition assertion failed.
+ old_dir = business.mapped(before, fixture.bucket, business.entries(original))[0][
+ original['items']['committed']['versionId'], 1]['dataDir']
+ final_mpu = business.mapped(after, fixture.bucket, business.entries(final), backup_directories=[old_dir])[0]
+ return verify_part_manifest(fleet, list(final_controls.values()) + list(final_mpu.values()), 'final-streaming', count=262)
+ check('G05-final-full-streams', full_streams)
+ fleet.start(); inventory = None
+ for endpoint in binding['endpoints']:
+ peer = Probe(binding, endpoint)
+ reader = Recorder(peer, fleet.evidence / ('final-read-ACKs-' + endpoint))
+ check('G05-all-peer-multipart-S3-' + endpoint, lambda: fixture.verify_s3(reader))
+ current = fleet.record('all-peer-final-S3', {'endpoint': endpoint, **peer.inventory(fixture.bucket)})
+ check('G05-final-payload-' + endpoint, lambda: verify_seed(simple_entries(controls, final), current))
+ check('G05-control-properties-' + endpoint, lambda: compare_inventories(
+ {'bucket': fixture.bucket, 'entries': [row for row in baseline['entries'] if row['key'] != model.KEY]},
+ {'bucket': fixture.bucket, 'entries': [row for row in current['entries'] if row['key'] != model.KEY]}))
+ if inventory is not None:
+ check('G05-peer-inventory-' + endpoint, lambda: compare_inventories(inventory, current))
+ inventory = current
+ check('G05-final-read-ledger-' + endpoint, lambda: ack_records(reader))
+ fleet.stop()
+ def unchanged():
+ require(physical_content(after) == physical_content(fleet.snapshot('physical-after-S3', quiescent=True)),
+ 'G05 final full GET changed the already-proven physical result')
+ return {'postGetPhysicalUnchanged': True}
+ check('G05-post-GET-physical', unchanged)
+ terminal_proof = check('G05-original-terminal-proof', lambda: race.terminal_proof(terminal, fixture.bucket, baseline, inventory))
+ def retired():
+ roots = exact_records(journal_records(fleet, token, 'G05-original-root-retirement', quiescent=True, family='root'), token, 'root')
+ require(not roots,
+ 'G05 original runnable root responsibility was not retired')
+ return {'originalRootRetired': True}
+ check('G05-root-retirement-proof', retired)
+ fleet.record('G05-contract-violations', violations)
+ require(not violations, 'G05 final contracts failed; independent diagnostics retained')
+ return physical, terminal_proof
+
+
+def run_branch(args, topology, nodes, branch):
+ require(args.case_id == CASE and branch in schedule.SELECTED and len(nodes) == (1 if topology == 'H1' else 4),
+ 'G05 runner requires its original case/branch and H1/H2 hosts')
+ candidate = json.loads(args.candidate.read_text())
+ require(candidate['source_sha'] == race.COPY_CANDIDATE, 'G05 same-key runner requires its reviewed native candidate')
+ reference = race.COPY_REFERENCE
+ directory = args.evidence / topology / branch; directory.mkdir(parents=True, exist_ok=False)
+ run_id = f"gh-{os.environ['GITHUB_RUN_ID']}-{os.environ.get('GITHUB_RUN_ATTEMPT', '1')}-{topology.lower()}"
+ fleet = Fleet(nodes, directory, run_id, args.tool_directory, runtime_profile='frozen-race-legacy')
+ inspector = fleet.record('inspector-build', verify_inspector(args.tool_directory, candidate))
+ bucket = 'rv-heal-' + hashlib.sha256(run_id.encode()).hexdigest()[:16]
+ access, secret = secrets.token_hex(16), secrets.token_hex(24)
+ os.environ.update(HEAL_ACCESS_KEY=access, HEAL_SECRET_KEY=secret)
+ binding = {'schemaVersion': 1, 'runId': run_id, 'jumpHostOnly': True, 'exclusiveHealService': True,
+ 'leaseExpiresAt': (datetime.now(timezone.utc) + timedelta(minutes=45)).isoformat(), 'bucketPrefix': bucket,
+ 'endpoints': {f'node{i}': f'http://rustfs-node{i}:9000' for i in range(1, len(nodes) + 1)},
+ 'defaultEndpoint': 'node1', 'candidateCommit': candidate['source_sha'], 'harnessCommit': args.testing_commit,
+ 'candidatePackageSha256': candidate['package_sha256'], 'candidateBinarySha256': args.binary_sha,
+ 'rcBinarySha256': hashlib.sha256(args.rc_bin.read_bytes()).hexdigest(),
+ 'maxInventoryObjects': 512, 'maxInventoryBytes': 512 * 1024**2}
+ fleet.record('binding', binding)
+ fleet.record('source-contract', {'caseId': CASE, 'branch': branch, 'objects': 257, 'fixture': 'DM', 'fault': 'F1',
+ 'sameKey': model.KEY, 'requested': reference['requested'], 'effective': reference['effective'],
+ 'processIoCapRequested': False, 'nativeFenceProved': False, 'canonicalCasePassClaimed': False})
+ probe = KeepAliveStatusProbe(binding)
+ result = {'caseId': CASE, 'topology': topology, 'branch': branch, 'fixture': 'DM', 'fault': 'F1',
+ 'verdict': 'RUNNING', 'cleanup': 'PENDING', 'canonicalCasePassClaimed': False}
+ seed_recorder, target_authority = None, False
+ try:
+ fleet.deploy(args.binary_sha, bucket, access, secret)
+ fleet.record('topology-proof', validate_topology(fleet.snapshot('deployed-topology'), topology))
+ seed_recorder = Recorder(probe, directory / 'seed-ACKs')
+ controls, fixture, prepared = seed(seed_recorder, bucket, branch)
+ original = prepared['manifest']
+ reader = Recorder(probe, directory / 'mpu-read-ACKs')
+ fleet.record('G05-original-multipart-S3', fixture.verify_s3(reader))
+ fleet.record('G05-baseline-read-ledger', ack_records(reader))
+ baseline = fleet.record('complete-baseline-S3', probe.inventory(bucket))
+ verify_seed(simple_entries(controls, original), baseline)
+ before_staging = fleet.record('G05-original-staging', capture_staging(fleet, original, args.binary_sha, quiescent=False))
+ staging.verify(original, before_staging, nodes, args.binary_sha)
+ fleet.stop(); before = fleet.snapshot('complete-baseline-physical', quiescent=True)
+ physical_population(before, bucket, controls)
+ mapped_controls = race.map_fixture(before, bucket, controls)
+ mapped_mpu, _ = business.mapped(before, bucket, business.entries(original))
+ baseline_parts = list(mapped_controls.values()) + list(mapped_mpu.values())
+ fleet.record('G05-baseline-full-streams', verify_part_manifest(fleet, baseline_parts, 'baseline-streaming', count=259))
+ committed = mapped_mpu[original['items']['committed']['versionId'], 1]
+ selected = next(part for part in committed['cohort'] if part['node'] == nodes[0] and part['disk'] == 3)
+ targets = list(mapped_controls.values()) + [{**committed, **selected}]
+ require(len(targets) == 257 and all(target['node'] == nodes[0] and target['disk'] == 3 for target in targets),
+ 'G05 faults must cover all 257 original objects on the fourth drive of the first host')
+ receipts = []
+ for index, target in enumerate(targets):
+ params = {'disk': target['disk'], 'faultId': bounded_fault_id('G05', branch, index)}
+ fleet.faults.append((target['node'], params))
+ receipts.append(fleet.record('exact-missing-part', {'node': target['node'], **fleet.remote(target['node'],
+ 'apply-fault', **params, relativePath=target['relativePath'], expectedSha256=target['sha256'], kind='missing')}))
+ damaged = fleet.snapshot('complete-fault-physical', quiescent=True)
+ fleet.record('fault-batch-proof', validate_missing_batch(before, damaged, targets, receipts))
+ fleet.start(); observe_io(fleet, branch)
+ require(physical_content(damaged) == physical_content(fleet.snapshot('attribution-before-START')),
+ 'G05 missing shards changed before original manual admission')
+ path = '/rustfs/admin/v3/heal/' + quote(bucket, safe='')
+ start = fleet.record('original-Admin-start', probe.admin(path, [], json.dumps(reference['requested']).encode()))
+ token = start.get('response', {}).get('clientToken') if isinstance(start.get('response'), dict) else None
+ require(start.get('httpStatus') == 200 and isinstance(token, str) and str(uuid.UUID(token)) == token,
+ 'G05 admission lacks one original canonical token')
+ with probe.status_session(token, path) as transport:
+ try:
+ observe_status(probe, fleet, path, token, reference)
+ witness = fleet.snapshot('G05-target-window')
+ missing = (selected['node'], selected['disk'], selected['relativePath']) not in files(witness)
+ fixture.recorder = Recorder(probe, directory / 'foreground-ACKs')
+ executed, overlapped = foreground(fixture, prepared, probe, fleet, path, token, reference, args.binary_sha)
+ finally:
+ fleet.record('G05-status-transport', transport)
+ observe_io(fleet, branch)
+ terminal = wait_terminal(probe, fleet, path, token, reference)
+ physical, result['terminal'] = final_checks(probe, fleet, binding, controls, fixture, original, executed, before,
+ before_staging, mapped_controls, baseline, terminal, path, token, reference, args.binary_sha, inspector['binarySha256'])
+ target_authority = True; fleet.faults.clear()
+ fleet.record('last-ACK-physical-proof', physical)
+ if not (missing and overlapped): raise race.WindowNotObserved('G05 selected missing part and SDK running-token overlap not both observed')
+ result.update(verdict='PASS', publicCallOverlapObserved=True, nativeFenceProved=False, physical=physical)
+ except (Exception, KeyboardInterrupt) as error:
+ result.update(verdict='NOT_OBSERVED' if isinstance(error, race.WindowNotObserved) else 'FAIL',
+ failure={'type': type(error).__name__, 'message': str(error)})
+ finally:
+ issued = seed_recorder is not None and seed_recorder.sequence > 0
+ if issued and not target_authority:
+ try:
+ fleet.stop(); fleet.snapshot('retained-failure-scene', quiescent=True)
+ fleet.record('raw-mpu-failure-scene', {node: fleet.remote(node, 'mpu-scene', bucket=bucket,
+ keys=[model.KEY], expectedBinarySha256=args.binary_sha) for node in nodes})
+ except Exception as error:
+ fleet.record('failure-scene-error', {'type': type(error).__name__, 'message': str(error)})
+ fleet.record('last-ACK-rescue-required', {'state': 'DIRTY', 'namespacesRetained': True,
+ 'lastACKPhysicalAuthorityVerified': False, 'originalGenerationBlindRestorationProhibited': True})
+ result['cleanup'] = 'FAIL'
+ else:
+ result['cleanup'] = 'PASS' if fleet.cleanup() else 'FAIL'
+ fleet.record('branch-result', result)
+ return result
diff --git a/scripts/validation_heal/mpu_race_schedule.py b/scripts/validation_heal/mpu_race_schedule.py
new file mode 100644
index 00000000..f2533fb8
--- /dev/null
+++ b/scripts/validation_heal/mpu_race_schedule.py
@@ -0,0 +1,61 @@
+"""Frozen G05 preparation and ordered SDK/checkpoint schedule, without I/O cap."""
+try:
+ from .heal_probe import canonical, require
+except ImportError:
+ from heal_probe import canonical, require
+
+SELECTED = {'upload-continue':'upload_part', 'complete':'complete_multipart_upload', 'abort':'abort_multipart_upload'}
+
+
+def prepare(fixture, branch):
+ with fixture.lock:
+ require(branch in SELECTED and not fixture.seed_started and not fixture.items and fixture.poisoned is None,
+ 'G05 branch preparation requires an original fresh model')
+ fixture.seed()
+ if branch != 'upload-continue':
+ fixture.upload('a', 3)
+ return {'branch':branch, 'selectedOperation':SELECTED[branch], 'manifest':fixture.manifest()}
+
+
+def execute(fixture, branch, prepared, checkpoint):
+ """Worker callback writes named physical checkpoints outside Fleet.sequence.
+
+ Only the selected first SDK call needs the public running-token witness.
+ Every companion still needs its original ACK and immediate physical check.
+ """
+ with fixture.lock:
+ require(branch in SELECTED and callable(checkpoint) and isinstance(prepared,dict)
+ and set(prepared)=={'branch','selectedOperation','manifest'} and prepared['branch']==branch
+ and prepared['selectedOperation']==SELECTED[branch] and fixture.poisoned is None
+ and canonical(prepared['manifest'])==canonical(fixture.manifest()),
+ 'G05 foreground differs from its original prepared branch/model')
+ expected_a = 2 if branch=='upload-continue' else 3
+ require(set(fixture.items)=={'committed','a','b'} and fixture.items['committed']['state']=='complete'
+ and fixture.items['a']['state']==fixture.items['b']['state']=='pending'
+ and len(fixture.items['committed']['parts'])==len(fixture.items['b']['parts'])==3
+ and len(fixture.items['a']['parts'])==expected_a,
+ 'G05 requires the exact original committed/A/B population before Heal')
+ steps = ([('continued',lambda:fixture.upload('a',3)),('completed',lambda:fixture.complete('a')),
+ ('aborted',fixture.abort_b)] if branch=='upload-continue' else
+ [('completed',lambda:fixture.complete('a')),('aborted',fixture.abort_b)] if branch=='complete' else
+ [('aborted',fixture.abort_b),('completed',lambda:fixture.complete('a'))])
+ records=[]
+ try:
+ for label,call in steps:
+ response=call()
+ sequence=fixture.recorder.sequence
+ manifest=fixture.manifest()
+ witness=checkpoint(label,manifest)
+ require(canonical(manifest)==canonical(fixture.manifest()),'G05 physical checkpoint mutated its ACK model')
+ records.append({'phase':label,'ackSequence':sequence,'response':response,'checkpoint':witness})
+ except BaseException:
+ # Every received ACK already has the original durable Recorder/model
+ # files. A missing physical witness cannot authorize another mutation.
+ if fixture.poisoned is None:
+ fixture.poisoned='G05 physical checkpoint failed; retain original ACK state and halt companion mutations'
+ raise
+ require(fixture.items['a']['state']=='complete' and fixture.items['b']['state']=='aborted',
+ 'G05 foreground did not retire both original uploads')
+ return {'branch':branch,'selectedOperation':SELECTED[branch],'selectedAckSequence':records[0]['ackSequence'],
+ 'steps':records,'manifest':fixture.manifest(),'ioCapRequested':False,'physicalRepairProved':False,
+ 'publicOverlapProved':False,'canonicalCasePassClaimed':False}
diff --git a/scripts/validation_heal/mpu_scene.py b/scripts/validation_heal/mpu_scene.py
new file mode 100644
index 00000000..b594cce1
--- /dev/null
+++ b/scripts/validation_heal/mpu_scene.py
@@ -0,0 +1,116 @@
+"""Read-only stopped I03/G05 multipart census, scoped by original bucket/key SHA-256."""
+
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import stat
+
+
+NAMES = ('abort', 'complete', 'create', 'upload')
+G05_KEY = 'objects-0256.bin'
+MAX_FILES = 4096
+MAX_BYTES = 256 * 1024**2
+
+
+def require(value, message):
+ if not value:
+ raise ValueError(message)
+
+
+def keys_for(endpoints):
+ require(len(endpoints) == len(set(endpoints)) == 2 and 'node1' in endpoints
+ and set(endpoints) <= {'node1', 'node3', 'node4'}, 'I03 MPU scope requires its two actual partition sides')
+ return sorted(f'dl/{endpoint}/{name}.bin' for endpoint in endpoints for name in NAMES)
+
+
+def validate_scope(owner, keys):
+ if owner.get('runtimeProfile') in {'manual', 'frozen-race-legacy'}:
+ require(re.fullmatch(r'rv-heal-[0-9a-f]{16}', owner.get('bucket', ''))
+ and keys == [G05_KEY], 'G05 MPU retention requires its one frozen same-key prefix')
+ return 'the original G05 bucket/key SHA-256 prefix, including unknown upload IDs; not a bitrot/EC proof'
+ require(owner.get('runtimeProfile') == 'lock-quorum' and isinstance(keys, list) and len(keys) == 8
+ and all(isinstance(key, str) and re.fullmatch(r'dl/(node1|node3|node4)/(abort|complete|create|upload)\.bin', key)
+ for key in keys), 'MPU scene keys escaped the exact DL matrix')
+ endpoints = sorted({key.split('/')[1] for key in keys})
+ require(keys == keys_for(endpoints), 'MPU scene omitted or duplicated an original request key')
+ return 'all files under the eight original DL bucket/key SHA-256 prefixes; not a bitrot/EC proof'
+
+
+def stable(info):
+ return (info.st_mode, info.st_uid, info.st_gid, info.st_dev, info.st_ino, info.st_nlink,
+ info.st_size, info.st_mtime_ns, info.st_ctime_ns)
+
+
+def run(request, node):
+ require(set(request) == {'runId', 'action', 'bucket', 'keys', 'expectedBinarySha256'}
+ and request['action'] == 'mpu-scene', 'MPU scene accepts only an exact owned read-only scope')
+ state, roots, owner = node['owned'](request['runId'])
+ require(owner.get('runtimeProfile') in {'lock-quorum', 'manual', 'frozen-race-legacy'} and owner.get('bucket') == request['bucket']
+ and owner.get('binarySha256') == request['expectedBinarySha256']
+ and re.fullmatch(r'[0-9a-f]{64}', request['expectedBinarySha256']) is not None and len(roots) == 4,
+ 'MPU scene candidate/runtime/primary bucket differs from the owned H2 fixture')
+ node['stopped'](owner)
+ require(node['digest'](node['checked'](state / 'rustfs', state)) == owner['binarySha256'],
+ 'MPU scene candidate binary changed')
+ keys = request['keys']
+ scope = validate_scope(owner, keys)
+ files, directories, scopes, total = [], [], [], 0
+ for disk, root in enumerate(roots):
+ data = node['checked'](root / 'data', root)
+ device = data.lstat().st_dev
+ for key in keys:
+ key_hash = hashlib.sha256(f"{request['bucket']}/{key}".encode()).hexdigest()
+ folder = node['checked'](data / '.rustfs.sys' / 'multipart' / key_hash, data)
+ scopes.append({'disk': disk, 'key': key, 'keySha256': key_hash, 'present': folder.exists()})
+ if not folder.exists():
+ continue
+ def visit(path, depth=0):
+ nonlocal total
+ path = node['checked'](path, data)
+ before = path.lstat()
+ require(depth <= 8 and before.st_dev == device and (stat.S_ISDIR(before.st_mode)
+ or stat.S_ISREG(before.st_mode) and before.st_nlink == 1),
+ 'MPU scene contains a link, mount, special file or excessive depth')
+ relative = path.relative_to(data).as_posix()
+ if stat.S_ISDIR(before.st_mode):
+ require(len(directories) < MAX_FILES, 'MPU scene directory budget exceeded')
+ directories.append({'disk': disk, 'path': relative, 'key': key})
+ children = []
+ for child in path.iterdir():
+ require(len(children) < (3 if depth == 0 and owner['runtimeProfile'] in {'manual', 'frozen-race-legacy'} else MAX_FILES),
+ 'MPU scene directory population exceeded its bound')
+ children.append(child)
+ for child in sorted(children):
+ visit(child, depth + 1)
+ else:
+ require(len(files) < MAX_FILES and before.st_size <= 64 * 1024**2
+ and total + before.st_size <= MAX_BYTES, 'MPU scene file/byte budget exceeded')
+ fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
+ try:
+ require(stable(os.fstat(fd)) == stable(before), 'MPU file changed before hashing')
+ checksum, count = hashlib.sha256(), 0
+ while chunk := os.read(fd, 1024**2):
+ checksum.update(chunk)
+ count += len(chunk)
+ require(count <= before.st_size, 'MPU file grew during hashing')
+ require(count == before.st_size and stable(os.fstat(fd)) == stable(before),
+ 'MPU file changed during hashing')
+ finally:
+ os.close(fd)
+ files.append({'disk': disk, 'path': relative, 'key': key,
+ 'size': count, 'sha256': checksum.hexdigest()})
+ total += count
+ require(stable(path.lstat()) == stable(before), 'MPU scene changed during its stopped census')
+ visit(folder)
+ again = node['owned'](request['runId'])
+ node['stopped'](again[2])
+ require(again == (state, roots, owner), 'MPU scene owner or original roots changed during census')
+ value = {'bucket': request['bucket'], 'keys': keys, 'scopes': scopes,
+ 'directories': directories, 'files': files, 'bytesHashed': total}
+ return {**value, 'runId': request['runId'], 'binarySha256': owner['binarySha256'],
+ 'quiescent': True, 'originalPhysicalDrives': 4,
+ 'sceneSha256': hashlib.sha256(json.dumps(value, sort_keys=True, separators=(',', ':')).encode()).hexdigest(),
+ 'readOnly': True, 'rawContentsExported': False, 'payloadIntegrityClaimed': False,
+ 'scope': scope}
diff --git a/scripts/validation_heal/mrf_records.py b/scripts/validation_heal/mrf_records.py
new file mode 100644
index 00000000..9eb8b6cb
--- /dev/null
+++ b/scripts/validation_heal/mrf_records.py
@@ -0,0 +1,238 @@
+"""Read only the candidate's exact committed MRF files and decode whole records.
+
+This is a source-bound observer. It never searches system metadata, repairs a
+checkpoint, accepts operator risk, or treats a missing journal as repair proof.
+"""
+
+import base64
+import hashlib
+import json
+import os
+from pathlib import Path
+import stat
+import struct
+import uuid
+import zlib
+
+
+REFERENCE = {"candidateCommit": "072b57b37559fbe626aeda8abf61617fdcbdd926", "sourceFiles": {
+ "crates/heal/src/heal/mrf_queue.rs": "44fc2616024392ceef1e28816cb998d7e385618792545b26af3a3d7337ef9ca3",
+ "crates/heal/src/heal/mrf_queue/snapshot.rs": "ecfcb5ee63edef5c5d3d6713edc127e16f489522afc5fc02dc1044ccca241d14",
+ "crates/heal/src/heal/mrf_queue/partial_write.rs": "20415e0e985daea8fc62c5e152ed438e75008c33a2a592275e1dd9d1d4fee771",
+ "crates/common/src/mrf_channel.rs": "075bb84bcbbe7d76674f730c64c8b69fe37d0790ec75da46365ba2e0cab34e0f",
+ "crates/heal/src/heal/manager/scheduler.rs": "79ce1f9c278c664231b6a1eca7c521f8e98742fe6d17d8f852368da4e77cc33b",
+ "crates/heal/src/heal/task/heal_object.rs": "17bb72783b6fd58afdc0b81c24558c1f5aff867fa3fb6ee27bd7f264076d8f6e",
+ "crates/obs/src/telemetry/filter.rs": "f46372640433f231f89e3c3f092a7d9292f1e870b386685b3595238cfc9847d6"},
+ "journal": "complete CRC32/ISO-HDLC records; format 1, layouts 1/2/3",
+ "commit": "RFMRFC01 version 1; owner, sequence, payload length and two SHA-256 digests",
+ "lifecycle": "RFMFLC01 version 1; checksummed JSON, same committed owner and sequence",
+ "retirement": "exact kind/bucket/object/version/pool/set/incarnation and successful verified disposition"}
+LIMIT = 4 * 1024**2
+FILES = {f".rustfs.sys/.heal-mrf-{kind}.{slot}.bin": LIMIT * (4 if kind == "lifecycle" else 1)
+ for slot in (0, 1) for kind in ("snapshot", "commit", "lifecycle")}
+
+
+def require(condition, message):
+ if not condition:
+ raise ValueError(message)
+
+
+def canonical_uuid(value):
+ require(isinstance(value, str) and str(uuid.UUID(value)) == value and uuid.UUID(value).int != 0,
+ "MRF identity is not a canonical nonzero UUID")
+ return value
+
+
+def verify_reference(candidate):
+ require(candidate.get("source_sha") in {REFERENCE["candidateCommit"], "1aa9be87d18cf941e5020f19e8622c26ba4b10ac"}, "unsupported candidate MRF record/proof contract")
+ return REFERENCE
+
+
+def read_records(state, roots, owner, checked):
+ """Exact allowlist, no directory enumeration, no write descriptor."""
+ require(owner.get("runtimeProfile") in {"durable-mrf", "mrf-crash", "flapping", "queue-pressure", "incarnation-mrf"}, "MRF records require the dedicated run-owned runtime")
+ require(hashlib.sha256((state / "rustfs").read_bytes()).hexdigest() == owner["binarySha256"], "MRF candidate changed")
+
+ def collect():
+ rows = []
+ for disk, root in enumerate(roots):
+ data = root / "data"
+ for relative, limit in FILES.items():
+ path = checked(data / relative, data)
+ row = {"disk": disk, "relativePath": relative, "present": False}
+ try:
+ fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW)
+ except FileNotFoundError:
+ rows.append(row)
+ continue
+ with os.fdopen(fd, "rb") as stream:
+ before = os.fstat(stream.fileno())
+ require(stat.S_ISREG(before.st_mode) and before.st_nlink == 1 and before.st_size <= limit,
+ "MRF record exceeds its exact regular-file limit")
+ raw = stream.read(limit + 1)
+ after = os.fstat(stream.fileno())
+ try:
+ current = path.lstat()
+ except FileNotFoundError:
+ return None
+ fields = ("st_dev", "st_ino", "st_nlink", "st_size", "st_mtime_ns", "st_ctime_ns")
+ if len(raw) != before.st_size or any(getattr(before, field) != getattr(other, field)
+ for other in (after, current) for field in fields):
+ return None
+ row.update(present=True, size=len(raw), sha256=hashlib.sha256(raw).hexdigest(),
+ rawBase64=base64.b64encode(raw).decode())
+ rows.append(row)
+ return rows
+
+ for _ in range(3):
+ first, second = collect(), collect()
+ if first is not None and first == second:
+ return {"readOnly": True, "stable": True, "binarySha256": owner["binarySha256"], "records": second}
+ return {"readOnly": True, "stable": False, "binarySha256": owner["binarySha256"], "records": []}
+
+
+def decode_journal(raw):
+ require(isinstance(raw, bytes) and len(raw) <= LIMIT, "MRF journal exceeds the decoder bound")
+ records, cursor = [], 0
+ while cursor < len(raw):
+ start = cursor
+ require(len(records) < 2048 and len(raw) - cursor >= 25, "MRF journal is torn or has too many records")
+ format_tag, layout, kind, attempts, stamp, has_version = struct.unpack_from("= owner["sequence"],
+ "MRF retirement changed owner or rolled back the committed sequence")
+ require(not any(identity(intent) == wanted or intent.get("lifecycle", {}).get("responsibility_id")
+ == owner["responsibilityId"] for intent in value["intents"]),
+ "NOT_OBSERVED: exact MRF ACK responsibility has not retired")
+ else:
+ require(value.get("allAllowlistedFilesAbsent") is True, "MRF retirement has an unknown record state")
+ require(not any(identity(intent) == wanted for value in checkpoints.values() for intent in value["intents"]),
+ "MRF ACK responsibility migrated to another owner instead of retiring")
+ return {"allExactACKResponsibilitiesRetired": True, "queueDepthUsedAsProof": False,
+ "operatorAcceptanceUsed": False, "responsibilityCount": len(obligations)}
+
+
+def source_version(value, wanted):
+ # heal_object.rs logs Option<&str> with Rust debug syntax. Preserve the
+ # exact version; only the reviewed null representations denote no version.
+ if wanted == "null": return value in {None, "", "None", "null", "00000000-0000-0000-0000-000000000000"}
+ return isinstance(value, str) and value in {wanted, f'Some("{wanted}")'}
+
+
+def source_tasks(events, obligation, owners, after):
+ tasks = []
+ for node in {owner["node"] for owner in owners}:
+ rows = events.get(node, [])
+ for start in rows:
+ stamp, token = event_time(start), start.get("task_id")
+ if not (start.get("target") == "rustfs::heal::task" and start.get("message") == "Heal object started"
+ and start.get("stage") == "start" and start.get("bucket") == obligation["bucket"]
+ and start.get("object") == obligation["key"] and "version_id" in start
+ and source_version(start["version_id"], obligation["versionId"])
+ and isinstance(token, str) and token and stamp is not None and stamp >= after):
+ continue
+ admission = next((event for event in rows if event.get("target") == "rustfs::heal::manager"
+ and event.get("message") == "Heal queue request accepted" and event.get("request_id") == token
+ and event.get("source") == "mrf" and event.get("result") == "accepted"
+ and event_time(event) is not None and after <= event_time(event) <= stamp), None)
+ completed = next((event for event in rows if event.get("target") == "rustfs::heal::manager"
+ and event.get("state") == "task_completed" and event.get("task_id") == token
+ and event_time(event) is not None and event_time(event) >= stamp), None)
+ if admission is not None and completed is not None:
+ tasks.append({"node": node, "taskId": token, "admission": admission, "started": start, "completed": completed})
+ return tasks
+
+
+def verified_receipt(status, obligation, owners):
+ outcome = status.get("outcome") or {}
+ require(status.get("summary") == "finished" and status.get("truncated", False) is False
+ and outcome.get("execution", {}).get("state") == "completed" and outcome.get("coverage") == "complete"
+ and outcome.get("objectsTruncated") is False, "MRF task cannot discharge incomplete or failed responsibility")
+ objects = outcome.get("objects")
+ require(isinstance(objects, list) and len(objects) == 1, "MRF task did not preserve the exact ACK version scope")
+ receipt = objects[0]
+ item = receipt.get("identity", {})
+ incarnation = {owner["bucketIncarnationId"] for owner in owners}
+ require(item.get("kind") == "object" and item.get("bucket") == obligation["bucket"]
+ and item.get("object") == obligation["key"] and public_version_id(item.get("versionId")) == obligation["versionId"]
+ and type(item.get("poolIndex")) is int and item["poolIndex"] == obligation["poolIndex"]
+ and type(item.get("setIndex")) is int and item["setIndex"] == obligation["setIndex"]
+ and item.get("bucketIncarnationId") in incarnation, "MRF proof belongs to another ACK, set or bucket generation")
+ state = receipt.get("disposition", {}).get("state")
+ allowed = {"authoritatively_absent"} if obligation["authoritativelyAbsentRequired"] else {"repaired", "verified_healthy"}
+ require(state in allowed, "MRF retirement has no storage-verified disposition for this exact responsibility")
+ counters = outcome.get("counters", {})
+ expected = {"processed": 1, "healed": int(state == "repaired"), "unchanged": int(state != "repaired"),
+ "skipped": 0, "failed": 0, "unknown": 0, "attemptFailures": 0}
+ require(all(type(counters.get(name)) is int and counters[name] == count for name, count in expected.items())
+ and counters.get("overflowed") is False, "MRF proof counters hide unknown, failed or overflowed responsibility")
+ return {"identity": item, "disposition": state, "completeSuccessfulTask": True}
+
+
+def read_checkpoints(fleet, nodes, label, *, quiescent=False):
+ observed = {}
+ for node in nodes:
+ raw = fleet.record(label + "-raw", {"node": node, **fleet.remote(node, "mrf-records", quiescent=quiescent)})
+ observed[node] = checkpoint(raw)
+ return fleet.record(label + "-decoded", observed)
+
+
+def deleted_parts_absent(snapshot, targets):
+ indexed = files(snapshot)
+ for target in targets:
+ for shard in target["cohort"]:
+ require((shard["node"], shard["disk"], shard["relativePath"]) not in indexed,
+ "MRF retired an explicit deletion with its original data shard still present")
+
+
+def wait_replay(fleet, window, binding, nodes, bucket, expected, obligations, bindings, rejoin_time,
+ before, authorities, deleted_targets, *, observe=None, deadline_limit=None):
+ deadline, proofs = time.monotonic() + REPLAY_SECONDS, {}
+ if deadline_limit is not None: deadline = min(deadline, deadline_limit)
+ while True:
+ if observe is not None: observe()
+ events = window.capture()
+ for obligation in obligations:
+ key = identity(obligation)
+ if key in proofs: continue
+ for task in source_tasks(events, obligation, bindings[key], rejoin_time):
+ endpoint = "node" + str(nodes.index(task["node"]) + 1)
+ path = "/rustfs/admin/v3/heal/" + quote(bucket, safe="") + "/" + quote(obligation["key"], safe="/")
+ observed = fleet.record("MRF-exact-task-status", {"source": task,
+ **Probe(binding, endpoint).admin(path, [("clientToken", task["taskId"])], b"")})
+ require(observed.get("httpStatus") == 200, "MRF source task status is unavailable")
+ try:
+ proof = verified_receipt(observed.get("response", {}), obligation, bindings[key])
+ except ValueError as exc:
+ fleet.record("MRF-undischarged-task-result", {"task": task, "error": str(exc)})
+ continue
+ proofs[key] = {"source": task, "proof": proof}
+ break
+ scene = fleet.snapshot("MRF-replay-physical-progress")
+ try:
+ require({node: value["formats"] for node, value in scene.items()} == {node: value["formats"] for node, value in before.items()},
+ "MRF rejoin changed an original disk UUID or slot")
+ replica_census(scene, bucket, expected, authorities)
+ deleted_parts_absent(scene, deleted_targets)
+ require(set(proofs) == set(bindings), "NOT_OBSERVED: MRF replay lacks an exact verified source task for every ACK")
+ retired = retirement(read_checkpoints(fleet, nodes, "MRF-retirement"), obligations, bindings)
+ return proofs, retired
+ except ValueError as exc:
+ fleet.record("MRF-replay-incomplete", {"error": str(exc), "verifiedACKs": len(proofs)})
+ require(time.monotonic() < deadline, "MRF automatic replay deadline exceeded: " + str(exc))
+ time.sleep(2)
+
+
+def run_branch(args, branch, nodes, *, after_rejoin=None, put_count=3, fixture="DV"):
+ directory = args.evidence / branch
+ directory.mkdir()
+ run_id = f"gh-{os.environ['GITHUB_RUN_ID']}-{os.environ.get('GITHUB_RUN_ATTEMPT', '1')}-h2"
+ fleet = Fleet(nodes, directory, run_id, args.tool_directory,
+ runtime_profile="durable-mrf" if after_rejoin is None else "mrf-crash")
+ candidate = json.loads(args.candidate.read_text())
+ tool = fleet.record("inspector-build", verify_inspector(args.tool_directory, candidate))
+ contract = verify_quorum_contract(tool, candidate)
+ fleet.record("candidate-MRF-record-and-proof-contract", verify_reference(candidate))
+ bucket = "rv-heal-" + hashlib.sha256(run_id.encode()).hexdigest()[:16]
+ access, secret = secrets.token_hex(16), secrets.token_hex(24)
+ os.environ.update(HEAL_ACCESS_KEY=access, HEAL_SECRET_KEY=secret)
+ binding = {"schemaVersion": 1, "runId": run_id, "jumpHostOnly": True, "exclusiveHealService": True,
+ "leaseExpiresAt": (datetime.now(timezone.utc) + timedelta(minutes=30)).isoformat(), "bucketPrefix": bucket,
+ "endpoints": {f"node{index}": f"http://rustfs-node{index}:9000" for index in range(1, 5)},
+ "defaultEndpoint": "node1", "candidateCommit": candidate["source_sha"], "harnessCommit": args.testing_commit,
+ "candidatePackageSha256": candidate["package_sha256"], "candidateBinarySha256": args.binary_sha,
+ "rcBinarySha256": hashlib.sha256(args.rc_bin.read_bytes()).hexdigest(),
+ "maxInventoryObjects": 128, "maxInventoryBytes": 128 * 1024**2}
+ fleet.record("binding", binding)
+ probe = Probe(binding)
+ result = {"topology": "H2", "branch": branch, "verdict": "RUNNING", "cleanup": "PENDING"}
+ try:
+ fleet.deploy(args.binary_sha, bucket, access, secret)
+ recorder = Recorder(probe, directory / "fixture")
+ expected = seed(recorder, bucket, fixture, 4, [SIZE])
+ baseline = fleet.record("s3-baseline", probe.inventory(bucket, unversioned_current_tags=fixture == "DB"))
+ fleet.record("fixture-manifest", {"expected": expected, **verify_seed(expected, baseline)})
+ before = fleet.snapshot("physical-baseline")
+ fleet.record("topology-proof", validate_topology(before, "H2"))
+ initial_records = read_checkpoints(fleet, nodes, "MRF-baseline")
+ require(all(not value["intents"] for value in initial_records.values()), "fresh MRF fixture has preexisting responsibilities")
+ target = map_target(before, bucket, "objects/0001.bin", "data", 1,
+ next(entry["versionId"] for entry in expected if entry["key"] == "objects/0001.bin" and entry["isLatest"]))
+ offline_node, online_nodes = nodes[-1], nodes[:-1]
+ fleet.record("offline-quorum-plan", quorum_plan(target, offline_node, contract))
+ stopped = fleet.record("offline-original-node", fleet.remote(offline_node, "stop"))
+ require(stopped.get("quiescent") is True and stopped.get("service", {}).get("MainPID") == "0", "MRF target did not actually stop")
+ try:
+ with request.build_opener(request.ProxyHandler({})).open(binding["endpoints"]["node4"] + "/health/ready", timeout=3):
+ raise ValueError("MRF offline endpoint still accepts requests")
+ except URLError as exc:
+ require(isinstance(exc.reason, ConnectionRefusedError), "MRF target outage has no exact connection refusal")
+ fleet.record("offline-refusal", {"node": offline_node, "connectionRefused": True})
+ frozen = fleet.record("original-offline-scene", fleet.remote(offline_node, "snapshot", quiescent=True))
+ earliest_ms = int(time.time() * 1000) - 5000
+ expected, obligations, deleted = acknowledge(recorder, bucket, branch, expected, put_count=put_count, fixture=fixture)
+ fleet.record("offline-ACK-responsibility-manifest", {"expected": expected, "obligations": obligations, "deleted": deleted})
+ acknowledged = fleet.record("offline-S3-ACK-proof", probe.inventory(bucket, unversioned_current_tags=fixture == "DB"))
+ fleet.record("offline-S3-full-manifest", verify_seed(expected, acknowledged))
+ deadline = time.monotonic() + PERSIST_SECONDS
+ while True:
+ try:
+ checkpoints = read_checkpoints(fleet, online_nodes, "offline-committed-MRF")
+ bindings = pending_bindings(checkpoints, obligations, earliest_ms)
+ break
+ except ValueError as exc:
+ if not str(exc).startswith("NOT_OBSERVED:"): raise
+ require(time.monotonic() < deadline, "MRF persistence deadline exceeded: " + str(exc))
+ time.sleep(1)
+ fleet.record("durable-ACK-binding", [{"identity": list(key), "owners": value} for key, value in bindings.items()])
+ still_offline = fleet.record("offline-after-ACKs", fleet.remote(offline_node, "snapshot", quiescent=True))
+ require(physical_content({offline_node: frozen}) == physical_content({offline_node: still_offline}), "MRF ACKs reached the stopped original node")
+ authorities = fleet.record("offline-physical-authority", {node: fleet.remote(node, "snapshot", quiescent=node == offline_node) for node in nodes})
+ deleted_targets = [map_target(before, bucket, entry["key"], "data", 1, entry["versionId"], latest_required=False) for entry in deleted]
+ window = JournalWindow(fleet, "MRF-rejoin", event_targets={"rustfs::heal::manager", "rustfs::heal::task"})
+ rejoin_time = datetime.now(timezone.utc)
+ fleet.record("rejoin-original-node", fleet.remote(offline_node, "start"))
+ fleet.wait_ready()
+ fleet.record("registered-automatic-replay-budget", {"persistenceSeconds": PERSIST_SECONDS,
+ "replaySeconds": REPLAY_SECONDS, "adminMutations": 0, "targetReadsBeforePhysicalProof": 0,
+ "source": "MRF", "originalNodeAndDiskIdentitiesRequired": True})
+ interruption = None if after_rejoin is None else after_rejoin(probe, fleet, window, binding, bucket,
+ expected, obligations, bindings, rejoin_time, before, authorities, offline_node)
+ proofs, retired = wait_replay(fleet, window, binding, nodes, bucket, expected, obligations, bindings,
+ rejoin_time, before, authorities, deleted_targets)
+ if interruption is not None:
+ interrupted_identity = tuple(interruption["ackIdentity"])
+ require(interrupted_identity in proofs and
+ proofs[interrupted_identity]["source"]["taskId"] != interruption["volatileOriginalTaskId"],
+ "MRF automatic rescan was mistaken for completion of the lost volatile task")
+ interruption["newAutomaticTaskId"] = proofs[interrupted_identity]["source"]["taskId"]
+ result["coordinatorInterruption"] = interruption
+ fleet.record("MRF-verified-responsibility-retirement", {"retirement": retired,
+ "proofs": [{"identity": list(key), **value} for key, value in proofs.items()]})
+ retirement(read_checkpoints(fleet, nodes, "MRF-repeat-retirement"), obligations, bindings)
+ fleet.stop()
+ repaired = fleet.snapshot("MRF-quiescent-repaired", quiescent=True)
+ targets = replica_census(repaired, bucket, expected, authorities)
+ deleted_parts_absent(repaired, deleted_targets)
+ streaming = [verify_cohort(fleet, target, "MRF-streaming-proof") for target in targets]
+ retirement(read_checkpoints(fleet, nodes, "MRF-quiescent-retirement", quiescent=True), obligations, bindings)
+ fleet.start()
+ for endpoint in binding["endpoints"]:
+ peer = Probe(binding, endpoint)
+ inventory = fleet.record("MRF-all-peer-S3", {"endpoint": endpoint,
+ **peer.inventory(bucket, unversioned_current_tags=fixture == "DB")})
+ fleet.record("MRF-all-peer-manifest", {**verify_seed(expected, inventory), **compare_inventories(acknowledged, inventory)})
+ for entry in deleted:
+ validate_deleted_read(fleet.record("MRF-deleted-version-refusal", peer.s3("get_object",
+ {"Bucket": bucket, "Key": entry["key"], "VersionId": entry["versionId"]})), entry["versionId"])
+ fleet.stop()
+ require(physical_content(repaired) == physical_content(fleet.snapshot("MRF-after-verification", quiescent=True)),
+ "S3 verification changed the physical MRF result")
+ result.update(verdict="PASS", durableACKs=len(obligations), sourceTasks=len(proofs), retirement=retired,
+ streamingVersions=len(streaming), allPeerS3ManifestEqual=True, adminMutationCount=0, backupRestorationUsed=False)
+ except (Exception, KeyboardInterrupt) as exc:
+ result.update(verdict="FAIL", failure={"type": type(exc).__name__, "message": str(exc)})
+ if fleet.reserved:
+ try:
+ fleet.stop()
+ fleet.snapshot("MRF-failure-physical", quiescent=True)
+ read_checkpoints(fleet, nodes, "MRF-failure-records", quiescent=True)
+ except Exception as evidence_error:
+ result["failureEvidenceError"] = str(evidence_error)
+ finally:
+ result["cleanup"] = "PASS" if fleet.cleanup() else "FAIL"
+ fleet.record("branch-result", result)
+ return result
+
+
+def main():
+ def interrupted(signum, frame):
+ raise KeyboardInterrupt(f"Heal workflow interrupted by signal {signum}")
+ signal.signal(signal.SIGTERM, interrupted)
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--candidate", type=Path, required=True)
+ parser.add_argument("--case-id", choices=(CASE_ID,), required=True)
+ parser.add_argument("--testing-commit", required=True)
+ parser.add_argument("--binary-sha", required=True)
+ parser.add_argument("--rc-bin", type=Path, required=True)
+ parser.add_argument("--evidence", type=Path, required=True)
+ parser.add_argument("--tool-directory", type=Path, required=True)
+ args = parser.parse_args()
+ nodes = os.environ.get("RUSTFS_NODES", "vm000 vm001 vm002 vm003").split()
+ require(len(nodes) >= 4 and len(set(nodes)) == len(nodes) and all(re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", node) for node in nodes),
+ "four current Heal nodes required")
+ require(bool(re.fullmatch(r"[0-9a-f]{64}", args.binary_sha)), "invalid candidate binary digest")
+ require(bool(re.fullmatch(r"[1-9][0-9]*", os.environ.get("GITHUB_RUN_ID", ""))), "workflow fleet identity required")
+ require(select(CASE_ID)[0]["variants"] == list(BRANCHES), "MRF executor omitted a source branch")
+ results = []
+ try:
+ for branch in BRANCHES:
+ results.append(run_branch(args, branch, nodes[:4]))
+ if results[-1]["cleanup"] != "PASS": break
+ finally:
+ complete = len(results) == len(BRANCHES) and all(item["verdict"] == "PASS" and item["cleanup"] == "PASS" for item in results)
+ save(args.evidence / "healthy-summary.json", {"caseId": CASE_ID, "results": results,
+ "cleanup": "PASS" if results and all(item["cleanup"] == "PASS" for item in results) else "FAIL", "complete": complete})
+ require(complete, "E05 requires all three H2 durable MRF branches and complete cleanup")
+
+
+if __name__ == "__main__":
+ try:
+ main()
+ except Exception as exc:
+ print(f"Heal durable MRF validation failed: {exc}", file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_heal/multi_pool.py b/scripts/validation_heal/multi_pool.py
new file mode 100644
index 00000000..bf207500
--- /dev/null
+++ b/scripts/validation_heal/multi_pool.py
@@ -0,0 +1,526 @@
+#!/usr/bin/env python3
+"""H06: real two-pool/two-set selection with unselected faults preserved until proof."""
+
+import argparse
+from datetime import datetime, timedelta, timezone
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import secrets
+import signal
+import sys
+import time
+
+try:
+ from .heal_probe import Probe, compare_inventories, payload, require, save
+ from .heal_workload import Recorder, RecordedCallError, verify_seed
+ from .healthy import BASE_OPTIONS, Fleet, physical_content
+ from .inspector import verify as verify_inspector
+ from .matrix import select
+ from .multi_pool_layout import validate as validate_layout, volumes
+ from .shards import files, map_target, run_task, validate_fault, validate_repaired_physical
+ from .scopes import physical_selection, verify_parts, REFERENCE as SELECTOR_REFERENCE
+ from .stop import idle_fleet
+ from .versions import public_version_id
+ from .write_quorum import endpoint_refused, expected_reads
+ from ..validation_pool.pool_status import validate_pool_inventory
+except ImportError:
+ from heal_probe import Probe, compare_inventories, payload, require, save
+ from heal_workload import Recorder, RecordedCallError, verify_seed
+ from healthy import BASE_OPTIONS, Fleet, physical_content
+ from inspector import verify as verify_inspector
+ from matrix import select
+ from multi_pool_layout import validate as validate_layout, volumes
+ from shards import files, map_target, run_task, validate_fault, validate_repaired_physical
+ from scopes import physical_selection, verify_parts, REFERENCE as SELECTOR_REFERENCE
+ from stop import idle_fleet
+ from versions import public_version_id
+ from write_quorum import endpoint_refused, expected_reads
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'validation_pool'))
+ from pool_status import validate_pool_inventory
+
+
+CASE_ID = 'heal.multi-pool-target-isolation'
+BRANCHES = ('selected-pool', 'selected-set', 'unselected-control-fault')
+SIZE = 1024**2 + 73
+REFERENCE = {'candidateCommit': SELECTOR_REFERENCE['candidateCommit'], 'selectors': SELECTOR_REFERENCE['sourceFiles'],
+ 'layoutSourceFiles': ['crates/ecstore/src/layout/disks_layout.rs', 'crates/utils/src/string.rs'],
+ 'topology': 'H5: two endpoint-expansion arguments, eight original independent mounts per pool, two four-peer sets per pool',
+ 'storageClass': 'explicit EC:2; every actual object must decode as 2+2',
+ 'boundedFixture': {'DBKeys': 48, 'DVKeys': 24, 'versionsPerDVKey': 3, 'bytesPerVersion': SIZE},
+ 'placement': 'observe actual full-set XL2/version authority; no assumed pool hash or internal PUT selector',
+ 'scope': 'pool selector must match admitted pool; an unrestricted set may be null in the raw receipt, with independent exact physical binding',
+ 'control': 'unselected defects stay missing before any target GET or rescue; repair them in independent tasks only after proof'}
+REFERENCE['quotaConfig'] = {'sourceFile': 'rustfs/src/admin/handlers/quota.rs',
+ 'sourceSha256': '6a2b062d3d1c4cd81d934c570d21cb44dcd2c1883f6a85d2265e01b466924f5c',
+ 'route': '/rustfs/admin/v3/get-bucket-quota', 'query': 'one exact owned bucket',
+ 'assertion': 'unchanged no-quota config on the bound management endpoint; no authoritative usage claim'}
+
+
+def fixture_call(recorder, ledger, operation, params, body=None):
+ row = {'operation': operation, 'params': params, 'resolved': False}
+ if body is not None:
+ row['payload'] = {'size': len(body), 'sha256': hashlib.sha256(body).hexdigest()}
+ ledger.append(row) # Register before the durable recorder or the network can fail.
+ try:
+ response = recorder.call(operation, params, body)
+ except RecordedCallError as exc:
+ row['observation'] = exc.observation
+ raise
+ row['observation'] = {'acknowledged': True, 'resultUnknown': False, 'response': response}
+ if operation == 'put_object':
+ version_id = response.get('VersionId', 'null')
+ require((params['Bucket'].endswith('-dv') and version_id not in {None, 'null'})
+ or (not params['Bucket'].endswith('-dv') and version_id == 'null'),
+ 'H06 versioning mode differs from its exact PUT ACK')
+ row['versionId'] = version_id
+ row['resolved'] = True
+ return response
+
+
+def resolve_fixture_inventory(rows, inventory):
+ """Resolve an interrupted seed against exact prior ACKs, never retry it."""
+ entries = inventory['entries']
+ require(all(not entry['deleteMarker'] for entry in entries), 'seed resolution contains unexpected markers')
+ consumed, resolutions = set(), []
+ for row in rows:
+ require(row['operation'] == 'put_object', 'uncertain bucket mutation needs independent resolution; retain namespace')
+ params = row['params']
+ matches = [entry for entry in entries if entry['key'] == params['Key'] and entry['payload'] == row['payload']
+ and entry['properties'].get('ContentType') == params['ContentType']
+ and entry['properties'].get('Metadata') == params['Metadata']
+ and entry['tags'] == [{'Key': 'fixture', 'Value': 'H06'}, {'Key': 'suite', 'Value': 'heal'}]]
+ require(len(matches) <= 1, 'seed request has multiple or ambiguous committed versions')
+ observed = row.get('observation', {'acknowledged': False, 'resultUnknown': True})
+ if matches:
+ entry = matches[0]
+ version = entry['versionId']
+ require((params['Bucket'].endswith('-dv') and version not in {None, 'null'})
+ or (not params['Bucket'].endswith('-dv') and version == 'null'), 'resolved seed versioning drift')
+ if observed.get('acknowledged'):
+ require(version == observed['response'].get('VersionId', 'null'), 'resolved seed replaced its PUT ACK version')
+ else:
+ require(observed.get('resultUnknown') is True, 'refused fixture PUT committed')
+ identity = entry['key'], version
+ require(identity not in consumed, 'seed resolution reused one version for different requests')
+ consumed.add(identity)
+ resolutions.append({'key': params['Key'], 'state': 'COMMITTED', 'versionId': version})
+ else:
+ require(not observed.get('acknowledged') and not row['resolved'], 'a fixture PUT ACK cannot be redeemed')
+ resolutions.append({'key': params['Key'], 'state': 'ABSENT'})
+ require(consumed == {(entry['key'], entry['versionId']) for entry in entries},
+ 'seed resolution omitted foreign, altered or unexplained versions')
+ latest = {row['key']: row['versionId'] for row in resolutions if row['state'] == 'COMMITTED'}
+ require(all(entry['isLatest'] is (latest[entry['key']] == entry['versionId']) for entry in entries),
+ 'resolved fixture latest-version identity drifted')
+ return resolutions
+
+
+def resolve_pending_fixture(fleet, binding, ledger):
+ pending = [row for row in ledger if not row['resolved']]
+ require(all(row['operation'] == 'put_object' for row in pending),
+ 'uncertain bucket mutation needs independent resolution; retain namespace')
+ for bucket in sorted({row['params']['Bucket'] for row in pending}):
+ rows = [row for row in ledger if row['operation'] == 'put_object' and row['params']['Bucket'] == bucket]
+ inventories = [fleet.record('failed-seed-all-peer-resolution', Probe(binding, endpoint).inventory(bucket,
+ unversioned_current_tags=not bucket.endswith('-dv'))) for endpoint in binding['endpoints']]
+ proofs = [resolve_fixture_inventory(rows, inventory) for inventory in inventories]
+ require(all(proof == proofs[0] for proof in proofs), 'seed resolution disagrees across original peers')
+ for inventory in inventories[1:]:
+ compare_inventories(inventories[0], inventory)
+ for row in pending:
+ if row['params']['Bucket'] == bucket:
+ row['resolved'] = True
+ row['resolution'] = proofs[0]
+ return {'allOriginalRequestsResolved': True, 'automaticRetries': 0}
+
+
+def seed_fixtures(probe, directory, buckets, ledger):
+ recorder = Recorder(probe, directory / 'fixture')
+ expected = {}
+ for bucket, count, versions in ((buckets[0], 48, 1), (buckets[1], 24, 3)):
+ fixture_call(recorder, ledger, 'create_bucket', {'Bucket': bucket})
+ if versions == 3:
+ fixture_call(recorder, ledger, 'put_bucket_versioning', {'Bucket': bucket, 'VersioningConfiguration': {'Status': 'Enabled'}})
+ expected[bucket] = []
+ for index in range(count):
+ key = f'objects/{index:04d}.bin'
+ for version in range(versions):
+ body = payload(f'{bucket}/{key}/{version}', SIZE)
+ response = fixture_call(recorder, ledger, 'put_object', {'Bucket': bucket, 'Key': key,
+ 'ContentType': 'application/octet-stream', 'Metadata': {'heal-fixture': 'H06', 'nonce': f'{index}-{version}'},
+ 'Tagging': 'suite=heal&fixture=H06'}, body)
+ version_id = response.get('VersionId', 'null')
+ require((versions == 1 and version_id == 'null') or (versions == 3 and version_id not in {None, 'null'}),
+ 'H06 versioning mode differs from its exact PUT ACK')
+ expected[bucket].append({'key': key, 'versionId': version_id, 'deleteMarker': False,
+ 'isLatest': version == versions - 1, 'payload': {'size': SIZE, 'sha256': hashlib.sha256(body).hexdigest()}})
+ return expected
+
+
+def witness_peer_loss(fleet, binding, expected, mapped):
+ peer = fleet.nodes[-1]
+ before = fleet.record('H5-peer-process-before', fleet.remote(peer, 'process'))
+ require(before['pid'] > 1 and before['binarySha256'] == binding['candidateBinarySha256'], 'H5 peer lacks candidate process identity')
+ stopped = fleet.record('H5-one-owned-peer-stop', fleet.remote(peer, 'stop'))
+ require(stopped['quiescent'] is True and stopped['service']['MainPID'] == '0', 'H5 peer stop was not witnessed')
+ fleet.record('H5-stopped-peer-endpoint-refusal', endpoint_refused(binding, len(fleet.nodes) - 1))
+ for identity, target in mapped.items():
+ survivors = [member for member in target['cohort'] if member['node'] != peer]
+ require(len(survivors) == 3 and len({member['node'] for member in survivors}) == 3,
+ 'H5 peer loss did not leave exactly three independent slots')
+ bucket, key, version = identity
+ manifest = next(entry for entry in expected[bucket] if entry['key'] == key and entry['versionId'] == version)
+ for endpoint in list(binding['endpoints'])[:-1]:
+ read = fleet.record('H5-survivor-exact-version-read', Probe(binding, endpoint).s3('get_object',
+ {'Bucket': bucket, 'Key': key, 'VersionId': version}))
+ require(read.get('acknowledged') is True and read.get('resultUnknown') is False
+ and read.get('payload') == manifest['payload']
+ and read['response'].get('VersionId', 'null') == version, 'H5 surviving peer lost acknowledged version/bytes')
+ fleet.record('H5-original-peer-return', fleet.remote(peer, 'start'))
+ fleet.wait_ready()
+ after = fleet.record('H5-peer-process-after', fleet.remote(peer, 'process'))
+ require(after['pid'] > 1 and after['bootId'] == before['bootId'] and after['unit'] == before['unit']
+ and after['binarySha256'] == before['binarySha256']
+ and (after['pid'], after['startTicks']) != (before['pid'], before['startTicks']), 'H5 peer return changed candidate/unit or did not restart')
+ return {'oneOwnedPeerStopped': True, 'exactVersions': len(mapped), 'survivingReadEndpoints': 3,
+ 'remainingSlotsPerActualSet': 3, 'fullNodePowerLossClaimed': False}
+
+
+def map_versions(snapshot, expected):
+ layout = validate_layout(snapshot)
+ indexed, mapped = files(snapshot), {}
+ for bucket, entries in expected.items():
+ for entry in entries:
+ scopes = set()
+ for node, value in snapshot.items():
+ for fmt in value['formats']:
+ row = indexed.get((node, fmt['disk'], f"{bucket}/{entry['key']}/xl.meta"), {})
+ versions = [version for version in row.get('metadata', {}).get('versions', [])
+ if public_version_id(version.get('versionId')) == entry['versionId']]
+ require(len(versions) <= 1, 'H06 metadata duplicates an acknowledged version')
+ if not versions:
+ continue
+ groups = fmt['value']['xl']['sets']
+ set_ids = [index for index, group in enumerate(groups) if fmt['value']['xl']['this'] in group]
+ require(len(set_ids) == 1, 'H06 physical slot belongs to ambiguous sets')
+ scopes.add((fmt['disk'] // 2, set_ids[0]))
+ require(len(scopes) == 1, 'acknowledged version lacks a unique actual pool/set authority')
+ pool_index, set_index = next(iter(scopes))
+ target = map_target(snapshot, bucket, entry['key'], 'data', 1, entry['versionId'],
+ latest_required=False, pool_index=pool_index, set_index=set_index)
+ require(target['logical']['erasure']['data_blocks'] == target['logical']['erasure']['parity_blocks'] == 2
+ and len(target['cohort']) == len({row['node'] for row in target['cohort']}) == 4,
+ 'H06 object geometry is not actual cross-node EC 2+2')
+ mapped[(bucket, entry['key'], entry['versionId'])] = target
+ for bucket in expected:
+ require({(target['poolIndex'], target['setIndex']) for identity, target in mapped.items() if identity[0] == bucket}
+ == {(pool, group) for pool in range(2) for group in range(2)},
+ 'NOT_OBSERVED: bounded DB/DV writes did not populate every actual pool/set')
+ by_key = {}
+ for (bucket, key, _), target in mapped.items():
+ by_key.setdefault((bucket, key), set()).add((target['poolIndex'], target['setIndex']))
+ require(all(len(scopes) == 1 for scopes in by_key.values()), 'fixture key versions span different pools/sets; require a new reviewed scope fixture')
+ return layout, mapped
+
+
+def selection(mapped, branch):
+ require(branch in BRANCHES, 'unregistered H06 branch')
+ pool, group = (0, 0) if branch == 'unselected-control-fault' else (1, None if branch == 'selected-pool' else 1)
+ selected = {identity for identity, target in mapped.items() if target['poolIndex'] == pool
+ and (group is None or target['setIndex'] == group)}
+ require(selected and set(mapped) - selected, 'H06 selection has no target or unselected controls')
+ scopes = [(pool, index) for index in range(2)] if group is None else [(pool, group)]
+ scopes += [(1 - pool, 0)]
+ if group is not None:
+ scopes += [(pool, 1 - group)]
+ targets, keys = [], set()
+ for wanted_pool, wanted_group in scopes:
+ for versioned in (False, True):
+ candidates = sorted((identity, target) for identity, target in mapped.items()
+ if (target['poolIndex'], target['setIndex']) == (wanted_pool, wanted_group)
+ and identity[:2] not in keys and identity[0].endswith('-dv') is versioned
+ and (not versioned or target['logical']['isLatest'] is False))
+ require(candidates, 'H06 scope lacks a distinct DB or historical DV target/control key')
+ identity, target = candidates[0]
+ targets.append(target)
+ keys.add(identity[:2])
+ return {'recursive': True, 'pool': pool, 'set': group}, selected, targets
+
+
+def responsibility(current, mapped, selected, repaired, options):
+ outcome = current.get('outcome') or {}
+ require(current.get('summary') == 'finished' and current.get('truncated', False) is False
+ and outcome.get('execution', {}).get('state') == 'completed' and outcome.get('coverage') == 'complete'
+ and outcome.get('objectsTruncated') is False and current.get('settings') == options,
+ 'H06 task lost exact selectors, complete execution or diagnostics')
+ objects = outcome.get('objects', [])
+ require(len(objects) == len(selected) and repaired and repaired <= selected, 'H06 lacks exact selected version responsibility')
+ seen, incarnations = set(), {}
+ for row in objects:
+ item = row.get('identity', {})
+ identity = item.get('bucket'), item.get('object'), public_version_id(item.get('versionId'))
+ require(identity in selected and identity not in seen and item.get('kind') == 'object',
+ 'H06 outcome includes foreign, duplicate or unselected responsibility')
+ target = mapped[identity]
+ require(type(item.get('poolIndex')) is int and item['poolIndex'] == options['pool'] == target['poolIndex'],
+ 'H06 pool selector/receipt disagrees with its actual drive group')
+ reported_set = item.get('setIndex')
+ require(reported_set is None and options['set'] is None or type(reported_set) is int
+ and reported_set == target['setIndex'] and (options['set'] is None or reported_set == options['set']),
+ 'H06 set selector/receipt contradicts the actual version authority')
+ incarnation = item.get('bucketIncarnationId')
+ require(isinstance(incarnation, str) and incarnation and incarnations.setdefault(identity[0], incarnation) == incarnation,
+ 'H06 version receipts lack or mix bucket incarnations')
+ require(row.get('disposition', {}).get('state') == ('repaired' if identity in repaired else 'verified_healthy'),
+ 'H06 selected version is unknown, falsely healthy or falsely repaired')
+ seen.add(identity)
+ counts = {'processed': len(selected), 'healed': len(repaired), 'unchanged': len(selected) - len(repaired),
+ 'skipped': 0, 'failed': 0, 'unknown': 0, 'attemptFailures': 0, 'overflowed': False}
+ require(seen == selected and all(type(outcome.get('counters', {}).get(name)) is type(value)
+ and outcome['counters'][name] == value for name, value in counts.items()),
+ 'H06 canonical counters do not cover every exact selected ACK version')
+ return {'exactSelectedVersionResponsibility': True, 'repaired': len(repaired), 'versions': len(selected),
+ 'rawUnrestrictedSetNotRewritten': True, 'physicalScopeBindings': [
+ {'bucket': identity[0], 'key': identity[1], 'versionId': identity[2],
+ 'poolIndex': mapped[identity]['poolIndex'], 'setIndex': mapped[identity]['setIndex'],
+ 'diskUUIDs': [row['diskUUID'] for row in mapped[identity]['cohort']]} for identity in sorted(selected)]}
+
+
+def pool_status(probe, fleet, endpoint_expressions, label):
+ observed = fleet.record(label, probe.admin('/rustfs/admin/v3/pools/list', [], b'', method='GET'))
+ require(observed.get('httpStatus') == 200, 'H06 pool capacity/list observation failed')
+ values = validate_pool_inventory(observed.get('response'), 2)
+ require([row['id'] for row in values] == [0, 1]
+ and [row['cmdline'] for row in values] == endpoint_expressions and all(row['totalSize'] > 0 for row in values),
+ 'H06 public pool identities/capacity differ from its original endpoint arguments')
+ for row in values:
+ specific = fleet.record(label + '-selector', probe.admin('/rustfs/admin/v3/pools/status',
+ [('pool', str(row['id'])), ('by-id', 'true')], b'', method='GET'))
+ require(specific.get('httpStatus') == 200, 'H06 selected public pool status failed')
+ selected = validate_pool_inventory(specific.get('response'), 1)[0]
+ require(all(selected[name] == row[name] for name in ('id', 'cmdline', 'status', 'totalSize')),
+ 'H06 selected pool identity/state/total capacity disagrees with its pool list')
+ return [{name: row[name] for name in ('id', 'cmdline', 'status', 'totalSize')} for row in values]
+
+
+def quota_config(probe, fleet, bucket, label):
+ deadline = time.monotonic() + 30
+ attempts = 0
+ while True:
+ attempts += 1
+ value = fleet.record(f'{label}-attempt-{attempts}', probe.admin(
+ REFERENCE['quotaConfig']['route'], [('bucket', bucket)], b'', method='GET'))
+ response = value.get('response')
+ if value.get('httpStatus') == 200:
+ require(isinstance(response, dict) and set(response) == {'quota', 'size', 'rate', 'requests'}
+ and all(type(response[name]) is int and response[name] == 0 for name in response),
+ 'H06 changed the owned bucket quota or queried a different logical bucket')
+ return dict(response)
+ same_bucket_not_ready = (value.get('httpStatus') == 404 and isinstance(response, str)
+ and 'NoSuchBucket' in response
+ and f'bucket not found: {bucket}' in response)
+ require(same_bucket_not_ready, 'H06 changed the owned bucket quota or queried a different logical bucket')
+ require(time.monotonic() < deadline, 'H06 owned bucket metadata did not converge on the bound management endpoint within 30 seconds')
+ time.sleep(1)
+
+
+def quota_status(probe, fleet, buckets, label):
+ # Scanner stays disabled to preserve unselected defects. The reviewed
+ # config-only route checks bucket existence and reads persisted policy
+ # without requiring a scanner usage snapshot or reading target objects.
+ result = {}
+ for bucket in buckets:
+ result[bucket] = quota_config(probe, fleet, bucket, label)
+ return result
+
+
+def run_branch(args, nodes, branch):
+ directory = args.evidence / 'H5' / branch
+ directory.mkdir(parents=True)
+ run_id = f"gh-{os.environ['GITHUB_RUN_ID']}-{os.environ.get('GITHUB_RUN_ATTEMPT', '1')}-h5"
+ fleet = Fleet(nodes, directory, run_id, args.tool_directory, runtime_profile='multi-pool')
+ candidate = json.loads(args.candidate.read_text())
+ require(candidate['source_sha'] == REFERENCE['candidateCommit'], 'H06 selector/placement semantics are not reviewed for this candidate')
+ bucket = 'rv-heal-' + hashlib.sha256(run_id.encode()).hexdigest()[:16]
+ buckets = [bucket, bucket + '-dv']
+ fleet.snapshot_buckets = buckets
+ access, secret = secrets.token_hex(16), secrets.token_hex(24)
+ os.environ.update(HEAL_ACCESS_KEY=access, HEAL_SECRET_KEY=secret)
+ binding = {'schemaVersion': 1, 'runId': run_id, 'jumpHostOnly': True, 'exclusiveHealService': True,
+ 'leaseExpiresAt': (datetime.now(timezone.utc) + timedelta(minutes=30)).isoformat(), 'bucketPrefix': bucket,
+ 'endpoints': {f'node{index}': f'http://rustfs-node{index}:9000' for index in range(1, 5)}, 'defaultEndpoint': 'node1',
+ 'candidateCommit': candidate['source_sha'], 'harnessCommit': args.testing_commit,
+ 'candidatePackageSha256': candidate['package_sha256'], 'candidateBinarySha256': args.binary_sha,
+ 'rcBinarySha256': hashlib.sha256(args.rc_bin.read_bytes()).hexdigest(), 'maxInventoryObjects': 256,
+ 'maxInventoryBytes': 512 * 1024**2}
+ fleet.record('binding', binding)
+ fleet.record('multi-pool-source-contract', REFERENCE)
+ fleet.record('inspector-build', verify_inspector(args.tool_directory, candidate))
+ probe, active, ledger = Probe(binding), [], []
+ result = {'topology': 'H5', 'branch': branch, 'verdict': 'RUNNING', 'cleanup': 'PENDING'}
+ try:
+ fleet.deploy(args.binary_sha, bucket, access, secret)
+ expected = seed_fixtures(probe, directory, buckets, ledger)
+ baseline = {name: fleet.record('full-baseline-S3', probe.inventory(name,
+ unversioned_current_tags=name == bucket)) for name in buckets}
+ for name in buckets:
+ fleet.record('fixture-ACK-oracle', {'bucket': name, **verify_seed(expected[name], baseline[name])})
+ before_capacity = pool_status(probe, fleet, volumes(run_id), 'baseline-capacity-pool-binding')
+ before_quota = quota_status(probe, fleet, buckets, 'baseline-logical-quota-binding')
+ fleet.stop()
+ original = fleet.snapshot('all-pool-physical-baseline', quiescent=True)
+ layout, mapped = map_versions(original, expected)
+ fleet.record('all-pool-set-version-authorities', {'layout': layout, 'versions': [
+ {'bucket': identity[0], 'key': identity[1], 'versionId': identity[2],
+ 'poolIndex': target['poolIndex'], 'setIndex': target['setIndex'], 'cohort': target['cohort']}
+ for identity, target in mapped.items()]})
+ fleet.start()
+ result['nodeRedundancy'] = witness_peer_loss(fleet, binding, expected, mapped)
+ fleet.stop()
+ require(physical_content(original) == physical_content(fleet.snapshot('after-H5-peer-loss-before-fault', quiescent=True)),
+ 'healthy H5 peer loss modified the physical baseline')
+ deltas, selected, targets = selection(mapped, branch)
+ options = {**BASE_OPTIONS, **deltas}
+ result['selectors'] = options
+ fleet.record('selected-and-unselected-fault-plan', {'options': options, 'targets': targets,
+ 'selectedVersions': [list(identity) for identity in sorted(selected)]})
+ current = original
+ registrations = {}
+ for index, target in enumerate(targets):
+ params = {'disk': target['disk'], 'faultId': f'H06-part-{index}'}
+ registration = target['node'], params
+ fleet.faults.append(registration)
+ registrations[(target['bucket'], target['key'], target['versionId'])] = registration
+ receipt = fleet.record('precise-pool-set-part-fault', fleet.remote(target['node'], 'apply-fault', **params,
+ relativePath=target['relativePath'], kind='missing', expectedSha256=target['sha256']))
+ after = fleet.snapshot('physical-exact-fault', quiescent=True)
+ validate_fault(current, after, target, receipt)
+ current = after
+ readonly = None
+ if branch == 'unselected-control-fault':
+ readonly = next(target for target in targets if target['poolIndex'] != options['pool'])
+ fleet.quorum_gates.add(readonly['node'])
+ fleet.record('F4-unselected-private-readonly-intent', fleet.remote(readonly['node'], 'quorum-gate', disks=[readonly['disk']]))
+ repaired = {(target['bucket'], target['key'], target['versionId']) for target in targets
+ if (target['bucket'], target['key'], target['versionId']) in selected}
+ fleet.start()
+ def witness_readonly(label):
+ relative = f"{readonly['bucket']}/{readonly['key']}/xl.meta"
+ raw = fleet.record(label, fleet.remote(readonly['node'], 'quorum-gate-probe', relativePath=relative,
+ expectedReads=expected_reads(current, readonly['node'], relative)))
+ require({row['disk'] for row in raw['disks'] if not row['writable']} == {readonly['disk']},
+ 'F4 unselected control did not witness the exact service-private read-only root')
+ return {'poolIndex': readonly['poolIndex'], 'setIndex': readonly['setIndex'], 'diskUUID': readonly['diskUUID'], **raw}
+ if readonly:
+ result['F4BeforeSelectedHeal'] = witness_readonly('F4-service-namespace-before-selected-Heal')
+ observed = run_task(probe, fleet, {'bucket': '', 'key': ''}, 2, active,
+ option_overrides=options, status_endpoints=tuple(binding['endpoints'])[1:])
+ result['selectedResponsibility'] = responsibility(observed, mapped, selected, repaired, options)
+ if readonly:
+ result['F4AfterSelectedHeal'] = witness_readonly('F4-service-namespace-after-selected-Heal')
+ idle_fleet(fleet, binding)
+ fleet.stop()
+ isolated = fleet.snapshot('selected-repaired-unselected-still-missing-before-GET', quiescent=True)
+ selected_keys = {identity[:2] for identity in selected}
+ result['physicalIsolation'] = physical_selection(original, isolated, targets, selected_keys)
+ for identity in repaired:
+ fleet.faults.remove(registrations[identity])
+ if readonly:
+ fleet.record('F4-release-after-unselected-physical-proof', fleet.remote(readonly['node'], 'quorum-gate', mode='clear'))
+ fleet.quorum_gates.remove(readonly['node'])
+ fleet.start()
+ require(pool_status(probe, fleet, volumes(run_id), 'selected-capacity-pool-binding') == before_capacity
+ and quota_status(probe, fleet, buckets, 'selected-logical-quota-binding') == before_quota,
+ 'selected Heal changed public pool capacity identity or logical quota policy')
+ for target in targets:
+ identity = target['bucket'], target['key'], target['versionId']
+ if identity in repaired:
+ continue
+ local_scope = {key for key in mapped if key[:2] == identity[:2]}
+ control_options = {**BASE_OPTIONS, 'pool': target['poolIndex'], 'set': target['setIndex']}
+ control = run_task(probe, fleet, {'bucket': target['bucket'], 'key': target['key']}, 2, active,
+ option_overrides=control_options, status_endpoints=tuple(binding['endpoints'])[1:])
+ fleet.record('independent-unselected-control-repair', responsibility(control, mapped, local_scope, {identity}, control_options))
+ idle_fleet(fleet, binding)
+ fleet.stop()
+ final = fleet.snapshot('all-pool-rebuilt-before-business-GET', quiescent=True)
+ result['allPoolPhysical'] = validate_repaired_physical(original, final, targets[0], repair_targets=targets)
+ result['allVersionStreaming'] = verify_parts(fleet, mapped, 'complete-all-pool-version-streaming')
+ for identity, registration in registrations.items():
+ if identity not in repaired:
+ fleet.faults.remove(registration)
+ fleet.start()
+ result['s3'] = {}
+ for endpoint in binding['endpoints']:
+ result['s3'][endpoint] = {}
+ for name in buckets:
+ observed = fleet.record('all-peer-all-pool-full-S3', Probe(binding, endpoint).inventory(name,
+ unversioned_current_tags=name == bucket))
+ result['s3'][endpoint][name] = {**verify_seed(expected[name], observed), **compare_inventories(baseline[name], observed)}
+ result.update(verdict='PASS', unselectedControlsPreservedBeforeRecovery=True, crossPoolCopyCountedAsRepair=False)
+ except (Exception, KeyboardInterrupt) as exc:
+ result.update(verdict='FAIL', failure={'type': type(exc).__name__, 'message': str(exc)})
+ finally:
+ for path, token in active:
+ try: fleet.record('stop-unresolved', probe.admin(path, [('clientToken', token), ('forceStop', 'true')], b''))
+ except Exception as exc: fleet.record('stop-unresolved-error', {'error': str(exc)})
+ pending = any(not row['resolved'] for row in ledger)
+ if pending:
+ try:
+ fleet.stop()
+ for node in tuple(fleet.quorum_gates):
+ fleet.record('failed-seed-gate-release', fleet.remote(node, 'quorum-gate', mode='clear'))
+ fleet.quorum_gates.remove(node)
+ fleet.start()
+ result['seedResolution'] = resolve_pending_fixture(fleet, binding, ledger)
+ except Exception as exc:
+ result['seedResolution'] = {'state': 'UNRESOLVED', 'error': str(exc)}
+ if any(not row['resolved'] for row in ledger):
+ try: fleet.stop()
+ except Exception as exc: fleet.record('retain-stop-error', {'error': str(exc)})
+ fleet.record('retained-unresolved-seed', {'state': 'DIRTY', 'namespacesRetained': True, 'requests': ledger})
+ clean = False
+ else:
+ clean = fleet.cleanup()
+ result['cleanup'] = 'PASS' if clean else 'FAIL'
+ fleet.record('branch-result', result)
+ return result
+
+
+def main():
+ def interrupted(signum, frame):
+ raise KeyboardInterrupt(f'H06 interrupted by signal {signum}')
+ signal.signal(signal.SIGTERM, interrupted)
+ parser = argparse.ArgumentParser(description=__doc__)
+ for name in ('candidate', 'rc-bin', 'evidence', 'tool-directory'):
+ parser.add_argument('--' + name, type=Path, required=True)
+ parser.add_argument('--case-id', choices=[CASE_ID], required=True)
+ parser.add_argument('--testing-commit', required=True)
+ parser.add_argument('--binary-sha', required=True)
+ args = parser.parse_args()
+ require(re.fullmatch(r'[0-9a-f]{64}', args.binary_sha) and re.fullmatch(r'[1-9][0-9]*', os.environ.get('GITHUB_RUN_ID', '')),
+ 'immutable workflow candidate identity required')
+ require(select(CASE_ID)[0]['variants'] == list(BRANCHES), 'H06 source branch omitted')
+ nodes = os.environ.get('RUSTFS_NODES', 'vm000 vm001 vm002 vm003').split()
+ require(len(nodes) >= 4 and len(nodes) == len(set(nodes)) and all(re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9_.-]*', node) for node in nodes),
+ 'four distinct current Heal nodes required')
+ results = []
+ try:
+ for branch in BRANCHES:
+ results.append(run_branch(args, nodes[:4], branch))
+ if results[-1]['cleanup'] != 'PASS': break
+ finally:
+ complete = len(results) == 3 and all(row['verdict'] == row['cleanup'] == 'PASS' for row in results)
+ save(args.evidence / 'healthy-summary.json', {'caseId': CASE_ID, 'results': results, 'complete': complete,
+ 'cleanup': 'PASS' if results and all(row['cleanup'] == 'PASS' for row in results) else 'FAIL'})
+ require(complete, 'H06 requires all actual H5 pool/set selectors, preserved control faults and full version/physical proof')
+
+
+if __name__ == '__main__':
+ try: main()
+ except Exception as exc:
+ print(f'H06 multi-pool validation failed: {exc}', file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_heal/multi_pool_layout.py b/scripts/validation_heal/multi_pool_layout.py
new file mode 100644
index 00000000..cb9f2dcb
--- /dev/null
+++ b/scripts/validation_heal/multi_pool_layout.py
@@ -0,0 +1,42 @@
+"""Actual H5 pools/sets over sixteen original independent endpoints."""
+
+try:
+ from .heal_probe import require
+except ImportError:
+ from heal_probe import require
+
+
+def volumes(run_id):
+ require(run_id.endswith('-h5'), 'two-pool deployment requires an explicit H5 namespace')
+ return [f'http://rustfs-node{{1...4}}:9000/data/rustfs{{{first}...{first + 1}}}/heal-validation/{run_id}/data'
+ for first in (1, 3)]
+
+
+def validate(snapshot):
+ require(len(snapshot) == 4, 'H5 requires the four actual original hosts')
+ rows = [(node, row) for node, value in snapshot.items() for row in value['formats']]
+ require(len(rows) == 16 and len({(node, row['disk']) for node, row in rows}) == 16
+ and all({row['disk'] for owner, row in rows if owner == node} == set(range(4)) for node in snapshot),
+ 'H5 omitted or duplicated an original independent drive')
+ pools, all_ids = [], set()
+ for pool_index in range(2):
+ selected = [(node, row) for node, row in rows if row['disk'] // 2 == pool_index]
+ value = selected[0][1]['value']
+ sets = value['xl']['sets']
+ identities = [identity for group in sets for identity in group]
+ require(value['format'] == 'xl' and len(sets) == 2 and all(len(group) == 4 for group in sets)
+ and len(identities) == len(set(identities)) == 8 and not all_ids.intersection(identities),
+ 'H5 needs two distinct pools, each with two complete four-member sets')
+ require(all(row['value']['format'] == 'xl' and row['value']['id'] == value['id']
+ and row['value']['xl']['sets'] == sets for _, row in selected)
+ and {row['value']['xl']['this'] for _, row in selected} == set(identities),
+ 'pool formats disagree or fail to cover their exact endpoint group')
+ require(all(sum(row['value']['xl']['this'] in group for owner, row in selected if owner == node) == 1
+ for node in snapshot for group in sets), 'each H5 set must span all four actual hosts')
+ all_ids.update(identities)
+ pools.append({'poolIndex': pool_index, 'formatId': value['id'], 'actualSets': sets,
+ 'members': [{'node': node, 'disk': row['disk'], 'diskUUID': row['value']['xl']['this']} for node, row in selected]})
+ require(set(pools[0]['actualSets'][0]).isdisjoint(set(pools[1]['actualSets'][0])), 'two pool selectors alias one physical set')
+ return {'topology': 'H5', 'nodes': 4, 'independentDrives': 16, 'pools': pools,
+ 'setCountPerPool': 2, 'minimumAvailableSlotsAfterOnePeerLoss': 3,
+ 'codingMustBeMeasuredPerObject': True}
diff --git a/scripts/validation_heal/native_regressions.py b/scripts/validation_heal/native_regressions.py
new file mode 100644
index 00000000..0cdfed66
--- /dev/null
+++ b/scripts/validation_heal/native_regressions.py
@@ -0,0 +1,382 @@
+#!/usr/bin/env python3
+"""Build/list/run exact candidate native regressions; never certify fleet cases."""
+
+import argparse
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import shutil
+import subprocess
+import sys
+import time
+
+
+CANDIDATE = "072b57b37559fbe626aeda8abf61617fdcbdd926"
+SOURCE = "crates/ecstore/src/set_disk/ops/heal.rs"
+SOURCE_SHA = "767c468b780cf5f4e863b078f46c63440549e75d88b6b43bb59ed83c3b4a47b2"
+PREFIX = "set_disk::ops::heal::heal_result_report_tests::"
+REGRESSIONS = {
+ "read-repair-final-rename": {
+ "test": PREFIX + "read_repair_commit_aborts_when_object_changes_before_final_rename",
+ "sourceAssertions": ["arrived.notified() reaches the real final commit pause within five seconds",
+ "overwrite ACK precedes release.notify_one()", "stale repair returns typed DiskError::Timeout",
+ "current ETag remains the overwrite ACK"],
+ "fenceKind": "deterministic assertion in the exact unmodified candidate test",
+ "relatedCases": ["heal.race-unversioned-overwrite", "heal.lock-timeout-and-caller-abort"],
+ "limits": "does not prove PUT cleanup-tail, Admin nolock compatibility or a fleet overlap window"},
+ "read-repair-lock-owner": {
+ "test": PREFIX + "object_lock_diagnostics_distinguish_read_repair_from_queued_put",
+ "sourceAssertions": ["read-repair owns its actual shared data-phase lock before PUT queues",
+ "PUT namespace remains pending while a historical GET times out",
+ "captured acquired lock event has no premature guard_dropped event",
+ "release permits acknowledged PUT and historical repair; all three versions retain bytes"],
+ "fenceKind": "deterministic assertions and in-test captured lock events",
+ "relatedCases": ["heal.race-versioned-put", "heal.lock-timeout-and-caller-abort"],
+ "limits": "captured event rows are asserted internally; this runner cannot invent emitted barrier logs"},
+ "overwrite-concurrency": {
+ "test": PREFIX + "heal_racing_unversioned_overwrites_preserves_the_last_commit",
+ "sourceAssertions": ["eight joined overwrite/Heal rounds preserve the last ACK ETag"],
+ "fenceKind": "uncontrolled concurrency, not deterministic fence evidence",
+ "relatedCases": ["heal.race-unversioned-overwrite"],
+ "limits": "best-effort damage and tokio::join! do not prove a commit/cleanup critical point"},
+ "version-delete-concurrency": {
+ "test": PREFIX + "heal_racing_version_delete_never_resurrects_the_deleted_version",
+ "sourceAssertions": ["confirmed deleted version remains VersionNotFound", "surviving version remains readable"],
+ "fenceKind": "uncontrolled concurrency, not deterministic fence evidence",
+ "relatedCases": ["heal.race-version-delete"],
+ "limits": "does not prove a deterministic delete/repair commit fence"},
+ "conflicting-authority-guard": {
+ "test": PREFIX + "dangling_delete_guard_preserves_conflicting_identities_without_writing_metadata",
+ "sourceAssertions": ["conflicting candidates share one explicit version ID but different data directories",
+ "both original and reversed candidate order return UnsafeToDelete",
+ "explicit-version regeneration refuses both conflicting authorities",
+ "missing drives retain FileNotFound; both original data-directory identities remain"],
+ "fenceKind": "deterministic authority decision assertions in the unmodified candidate test",
+ "relatedCases": ["heal.conflicting-subquorum-authority"],
+ "limits": "controlled guard decisions; does not emit or prove a concurrent runtime commit-pause fence"},
+ "reconstructable-authority-preservation": {
+ "test": PREFIX + "heal_meta_quorum_failure_preserves_reconstructable_uncommitted_candidate",
+ "sourceAssertions": ["the actual read_all_fileinfo result is UnsafeToDelete",
+ "no_lock Heal returns typed FileNotFound without deleting the surviving metadata",
+ "surviving data-directory identity is unchanged; missing drives receive no manufactured metadata"],
+ "fenceKind": "deterministic preservation assertions in the unmodified candidate test",
+ "relatedCases": ["heal.conflicting-subquorum-authority"],
+ "limits": "separate native regression; does not certify a live split, overlap or runtime pause"},
+}
+INCARNATION_CANDIDATE = "1aa9be87d18cf941e5020f19e8622c26ba4b10ac"
+INCARNATION_EVENTS = ["final_rename_paused", "caller_aborted_writer_pending",
+ "repair_drained_writer_acquired", "successor_rejects_obsolete_admission"]
+STORE_PREFIX = "store::heal::tests::"
+INCARNATION_REGRESSIONS = {
+ "incarnation-successor-storage": {
+ "test": STORE_PREFIX + "bucket_incarnation_heal_preserves_successor_shards_and_allows_fresh_repair",
+ "sourceAssertions": ["normal delete/recreate changes the authoritative bucket UUID",
+ "name-only positive control repairs a real missing successor shard",
+ "old-incarnation metadata/path/version/rename/bucket/object mutations are rejected",
+ "obsolete admission publishes no missing shard and leaves healthy metadata byte-identical",
+ "fresh successor admission independently repairs the missing shard"],
+ "fenceKind": "real physical storage incarnation guard decisions",
+ "relatedCases": ["heal.bucket-incarnation-recreation"],
+ "limits": "does not prove fleet checkpoint or MRF replay"},
+ "incarnation-cancelled-owner": {
+ "test": STORE_PREFIX + "bucket_incarnation_heal_owner_survives_cancelled_waiter_and_queued_writer",
+ "sourceAssertions": ["cancelled caller retains the original lifecycle owner",
+ "queued writer remains pending until storage proof drains",
+ "exact incarnation absence proof spans both selected pools without reacquiring behind the writer"],
+ "fenceKind": "deterministic held-owner and queued-writer assertions",
+ "relatedCases": ["heal.bucket-incarnation-recreation"],
+ "limits": "separate owner regression; not a physical final-rename pause"},
+ "incarnation-final-rename": {
+ "test": STORE_PREFIX + "bucket_incarnation_fence_survives_caller_abort_at_real_repair_commit",
+ "sourceAssertions": ["real external exact-version reconstruction pauses before final scope check and rename",
+ "missing target is unpublished at the pause",
+ "caller abort cannot admit the lifecycle writer before repair drains",
+ "the published exact version/data directory passes physical check_parts verification",
+ "healthy metadata remains byte-identical",
+ "normal version/bucket deletion and recreation reject obsolete admission without changing successor metadata"],
+ "fenceKind": "deterministic actual reconstruction/final-rename barrier with emitted assertion milestones",
+ "relatedCases": ["heal.bucket-incarnation-recreation"], "barrierEvents": INCARNATION_EVENTS,
+ "limits": "native storage commit evidence only; not canonical fleet checkpoint/MRF replay"},
+}
+CARGO_LOCK_SHA = "d8f7bc416ba67d19873d34e9389e5aa823b22d7d7552706a5a810cee26f6eda2"
+BUILD_ENV = {'CARGO_BUILD_JOBS': '2', 'CARGO_INCREMENTAL': '0', 'CARGO_PROFILE_RELEASE_DEBUG': '0',
+ 'CARGO_PROFILE_RELEASE_STRIP': 'debuginfo'}
+BUILD_COMMAND = ['cargo', 'test', '--locked', '--release', '-p', 'rustfs-ecstore', '--lib', '--no-run', '--message-format=json']
+REVIEWED_CANDIDATES = {
+ CANDIDATE: {'sourceTree': '8071fb0a9e5353eb7d3fa58c8a58c8622473f2bd',
+ 'sourceFiles': {SOURCE: SOURCE_SHA}, 'regressions': REGRESSIONS},
+ INCARNATION_CANDIDATE: {'sourceTree': 'd57bcda8b1551727ced7073ddd09f8c97c13485f',
+ 'sourceFiles': {
+ SOURCE: 'a375fbf105d2491e1582c65c417509ac3eff23db826db788eb0aeb629e07f6b8',
+ 'crates/ecstore/src/store/heal.rs': '5ecf643f43f6f61f5b13f777f68faafba37e58a98eecb8355254f2ea75a1e9b1',
+ 'crates/ecstore/src/store/bucket.rs': 'a264de25fc4d3693ed962ea3613055b90f7092d0eb1cc748895030ab8adf1ae3',
+ 'crates/ecstore/src/store/bucket_fence.rs': '6f9d97cdd580bbd91f79af5723ff4712065b3d054cb5590db35fc00c79e1f262',
+ 'crates/ecstore/src/store/peer.rs': '85636a2536efc9e14fef0e1fe3d11de53093c45df44951b8fd08e4ed1edbf251',
+ 'crates/ecstore/src/set_disk/mod.rs': 'd4280fb89752f6198cb5641ea8ed23f5a41a4c87688e10bfce80a548e302e14f'},
+ 'regressions': {**REGRESSIONS, **INCARNATION_REGRESSIONS}},
+}
+
+
+def runtime_environment(commit):
+ # Match the candidate's nextest ecstore stack setup for real storage tests.
+ return {'RUST_MIN_STACK': '33554432'} if commit == INCARNATION_CANDIDATE else {}
+
+
+def exact_command(binary, name, commit):
+ # Captured output keeps libtest's result line intact while retaining every
+ # successful test milestone; nocapture interleaves the first milestone.
+ output_flag = '--show-output' if commit == INCARNATION_CANDIDATE else '--nocapture'
+ return [str(binary), name, '--exact', output_flag, '--test-threads=1']
+
+
+def require(value, message):
+ if not value:
+ raise ValueError(message)
+
+
+def sha(path):
+ result = hashlib.sha256()
+ with path.open('rb') as stream:
+ while chunk := stream.read(1024**2):
+ result.update(chunk)
+ return result.hexdigest()
+
+
+def save(path, value):
+ with path.open('x') as stream:
+ json.dump(value, stream, indent=2)
+ stream.write('\n')
+
+
+def exact_listing(output, name):
+ tests = [line.removesuffix(': test') for line in output.splitlines() if line.endswith(': test')]
+ require(tests.count(name) == 1, "native test inventory must contain exactly one complete requested name")
+ require(len(tests) == len(set(tests)), "native inventory repeats a test identity")
+ return len(tests)
+
+
+def test_result(output, name, code):
+ # A successful zero-test filter or an ignored test is not a regression run.
+ require(type(code) is int and code == 0
+ and re.findall(r"(?m)^running .*", output) == ['running 1 test']
+ and re.findall(r"(?m)^test (?!result:).*", output) == [f'test {name} ... ok']
+ and len(re.findall(r"(?m)^test result:.*", output)) == 1
+ and re.search(r"(?m)^test result: ok\. 1 passed; 0 failed; 0 ignored;", output),
+ "exact native execution did not report one real passing test")
+ return {"exactTestExecuted": True, "nativeRegressionPassed": True,
+ "fleetPassClaimed": False, "emittedBarrierLogsClaimed": False}
+
+
+def barrier_events(output, spec):
+ events = re.findall(r"(?m)^incarnation_commit_barrier: ([a-z_]+)\s*$", output)
+ require(events == spec.get('barrierEvents', []), "native barrier milestones are absent, duplicated or out of order")
+ return events
+
+
+def verify_manifest(directory):
+ manifest = json.loads((directory / 'native-regressions.json').read_text())
+ contract = REVIEWED_CANDIDATES.get(manifest.get('candidateCommit'))
+ require(contract is not None and manifest.get('schemaVersion') == 1
+ and manifest.get('candidateSourceTree') == contract['sourceTree']
+ and manifest.get('candidateCargoLockSha256') == CARGO_LOCK_SHA
+ and manifest.get('sourceFiles') == contract['sourceFiles']
+ and manifest.get('regressions') == contract['regressions'] and manifest.get('sourceUnmodified') is True
+ and manifest.get('optimized') is True and manifest.get('buildEnvironment') == BUILD_ENV
+ and manifest.get('buildCommand') == BUILD_COMMAND
+ and manifest.get('binaryKind') == 'native-regression-not-released-service'
+ and manifest.get('runtimeEnvironment') == runtime_environment(manifest.get('candidateCommit'))
+ and manifest.get('collectorSha256') == sha(Path(__file__)) and manifest.get('fleetPassClaimed') is False,
+ "native artifact identity or reviewed contract differs")
+ require(manifest.get('binaryName') == 'ecstore-native-tests', "native binary path is not allowlisted")
+ binary = directory / manifest['binaryName']
+ require(binary.is_file() and not binary.is_symlink() and sha(binary) == manifest.get('binarySha256'), "native binary changed")
+ return manifest, binary, contract
+
+
+def build(source, commit, out):
+ contract = REVIEWED_CANDIDATES.get(commit)
+ require(contract is not None and subprocess.check_output(['git', '-C', str(source), 'rev-parse', 'HEAD'], text=True).strip() == commit,
+ "native regression contract requires the reviewed immutable candidate checkout")
+ require(subprocess.run(['git', '-C', str(source), 'diff', '--quiet']).returncode == 0
+ and subprocess.run(['git', '-C', str(source), 'diff', '--cached', '--quiet']).returncode == 0,
+ "native source has tracked modifications")
+ require(not subprocess.check_output(['git', '-C', str(source), 'ls-files', '--others', '--exclude-standard'], text=True).strip(),
+ "native candidate checkout has untracked source/configuration")
+ require(subprocess.check_output(['git', '-C', str(source), 'rev-parse', 'HEAD^{tree}'], text=True).strip() == contract['sourceTree']
+ and sha(source / 'Cargo.lock') == CARGO_LOCK_SHA
+ and all(sha(source / path) == digest for path, digest in contract['sourceFiles'].items()),
+ "native source/tree/lock differs from its reviewed fence assertions")
+ require(not out.exists() and shutil.disk_usage(source).free >= 12 * 1024**3,
+ "fresh output and at least 12 GiB build space required")
+ out.mkdir(parents=True)
+ save(out / 'native-build-request.json', {'candidateCommit': commit, 'sourceFiles': contract['sourceFiles'],
+ 'candidateCargoLockSha256': sha(source / 'Cargo.lock'), 'regressions': contract['regressions'],
+ 'runId': os.environ.get('GITHUB_RUN_ID'), 'runAttempt': os.environ.get('GITHUB_RUN_ATTEMPT'),
+ 'fleetPassClaimed': False})
+ target = out / 'target'
+ build_env = BUILD_ENV
+ env = {**os.environ, **build_env, 'CARGO_TARGET_DIR': str(target)}
+ cargo = BUILD_COMMAND
+ # Preserve diagnostics in files; native artifacts never require modifying
+ # candidate source or adding a fake fence/event to an existing test.
+ with (out / 'cargo.jsonl').open('x') as stdout, (out / 'cargo.stderr').open('x') as stderr:
+ result = subprocess.run(cargo, cwd=source, env=env, stdout=stdout, stderr=stderr, timeout=150 * 60)
+ require(result.returncode == 0, "native regression build failed; retained Cargo diagnostics")
+ require(subprocess.run(['git', '-C', str(source), 'diff', '--quiet']).returncode == 0
+ and subprocess.run(['git', '-C', str(source), 'diff', '--cached', '--quiet']).returncode == 0,
+ "native build changed tracked candidate source")
+ executables = []
+ for line in (out / 'cargo.jsonl').read_text().splitlines():
+ try:
+ row = json.loads(line)
+ except json.JSONDecodeError:
+ continue
+ if row.get('reason') == 'compiler-artifact' and row.get('profile', {}).get('test') is True \
+ and row.get('target', {}).get('name') == 'rustfs_ecstore' and row.get('executable'):
+ executables.append(Path(row['executable']))
+ require(len(executables) == 1 and executables[0].resolve().is_relative_to(target.resolve()),
+ "Cargo did not expose one owned candidate ecstore test executable")
+ binary = out / 'ecstore-native-tests'
+ shutil.copyfile(executables[0], binary)
+ binary.chmod(0o500)
+ listing = subprocess.run([str(binary), '--list'], capture_output=True, text=True, timeout=60)
+ (out / 'test-inventory.log').write_text(listing.stdout + listing.stderr)
+ require(listing.returncode == 0, "candidate native inventory failed")
+ for spec in contract['regressions'].values():
+ exact_listing(listing.stdout, spec['test'])
+ manifest = {'schemaVersion': 1, 'candidateCommit': commit, 'candidateSourceTree': subprocess.check_output(
+ ['git', '-C', str(source), 'rev-parse', 'HEAD^{tree}'], text=True).strip(),
+ 'sourceFiles': contract['sourceFiles'], 'candidateCargoLockSha256': sha(source / 'Cargo.lock'),
+ 'binarySha256': sha(binary), 'binaryName': binary.name, 'binaryKind': 'native-regression-not-released-service',
+ 'collectorSha256': sha(Path(__file__)), 'testingCommit': os.environ.get('AUTO_TESTING_SHA'),
+ 'sourceUnmodified': True, 'buildCommand': cargo, 'buildEnvironment': build_env, 'optimized': True, 'buildJobs': 2,
+ 'runtimeEnvironment': runtime_environment(commit),
+ 'toolchain': subprocess.check_output(['rustc', '-vV'], cwd=source, text=True),
+ 'runId': os.environ.get('GITHUB_RUN_ID'), 'runAttempt': os.environ.get('GITHUB_RUN_ATTEMPT'),
+ 'regressions': contract['regressions'], 'fleetPassClaimed': False,
+ 'limits': 'native and fleet evidence are independently scored; only reviewed tests with verified original milestones claim emitted barriers'}
+ save(out / 'native-regressions.json', manifest)
+ # Remove only the build cache created under this fresh task output after
+ # copying and identifying the executable and every build diagnostic.
+ shutil.rmtree(target)
+ return manifest
+
+
+def run(directory, selected, out):
+ manifest, binary, contract = verify_manifest(directory)
+ regressions = contract['regressions']
+ selected = list(regressions) if selected is None else selected
+ require(selected and len(selected) == len(set(selected)) and set(selected) <= set(regressions), "invalid native selection")
+ require(not out.exists(), "native evidence directory must be fresh")
+ out.mkdir(parents=True)
+ listing = subprocess.run([str(binary), '--list'], capture_output=True, text=True, timeout=60)
+ (out / 'runtime-inventory.log').write_text(listing.stdout + listing.stderr)
+ require(listing.returncode == 0, "native runtime inventory failed")
+ results = []
+ for key in selected:
+ spec = regressions[key]
+ exact_listing(listing.stdout, spec['test'])
+ require(not binary.is_symlink() and sha(binary) == manifest['binarySha256'], "native executable changed before exact execution")
+ args = exact_command(binary, spec['test'], manifest['candidateCommit'])
+ started = time.monotonic_ns()
+ record = {'regression': key, 'test': spec['test'], 'command': args, 'sourceAssertions': spec['sourceAssertions'],
+ 'binarySha256': manifest['binarySha256'], 'candidateCommit': manifest['candidateCommit'], 'fleetPassClaimed': False,
+ 'emittedBarrierLogsClaimed': False, 'fenceKind': spec['fenceKind'], 'limitations': spec['limits']}
+ record['runtimeEnvironment'] = manifest['runtimeEnvironment']
+ try:
+ child = subprocess.run(args, capture_output=True, text=True, timeout=120,
+ env={**os.environ, **manifest['runtimeEnvironment']})
+ output = child.stdout + child.stderr
+ (out / (key + '.log')).write_text(output)
+ record['exitCode'] = child.returncode
+ test_result(output, spec['test'], child.returncode)
+ events = barrier_events(output, spec)
+ if events:
+ record.update(observedBarrierEvents=events, emittedBarrierLogsClaimed=True)
+ record['verdict'] = 'PASS'
+ except subprocess.TimeoutExpired as error:
+ def text_part(value):
+ return value.decode(errors='replace') if isinstance(value, bytes) else value or ''
+ output = text_part(error.stdout) + text_part(error.stderr)
+ (out / (key + '.log')).write_text(output)
+ record.update(verdict='FAIL', reason='native test exceeded its fixed 120-second deadline')
+ except ValueError as error:
+ record.update(verdict='FAIL', reason=str(error))
+ except OSError as error:
+ (out / (key + '.log')).write_text(str(error))
+ record.update(verdict='INFRA_ERROR', reason='native test executable could not start: ' + str(error))
+ record['elapsedNanoseconds'] = time.monotonic_ns() - started
+ save(out / (key + '.json'), record)
+ results.append(record)
+ save(out / 'native-summary.json', {'results': results, 'fleetPassClaimed': False,
+ 'complete': len(results) == len(selected) and all(row['verdict'] == 'PASS' for row in results),
+ 'canonicalCasePassClaimed': False, 'sourceUnmodified': True})
+ require(all(row['verdict'] == 'PASS' for row in results), 'native regression failure remains in separate evidence')
+
+
+def verify_evidence(directory, evidence, candidate, run_id, run_attempt, helper_sha):
+ manifest, binary, contract = verify_manifest(directory)
+ require(manifest.get('candidateCommit') == candidate['source_sha'] and manifest.get('runId') == str(run_id)
+ and manifest.get('runAttempt') == str(run_attempt) and manifest.get('testingCommit') == helper_sha
+ and isinstance(helper_sha, str) and re.fullmatch(r'[0-9a-f]{40}', helper_sha),
+ "native evidence belongs to another candidate, run attempt or helper")
+ summary = json.loads((evidence / 'native-summary.json').read_text())
+ rows = summary.get('results', [])
+ regressions = contract['regressions']
+ require(summary.get('complete') is True and summary.get('sourceUnmodified') is True
+ and summary.get('fleetPassClaimed') is False and summary.get('canonicalCasePassClaimed') is False
+ and len(rows) == len(regressions) and {row.get('regression') for row in rows} == set(regressions),
+ "native summary omits or duplicates a required test or claims fleet success")
+ listings = [(directory / 'test-inventory.log').read_text(), (evidence / 'runtime-inventory.log').read_text()]
+ observed = {}
+ for row in rows:
+ key = row['regression']
+ spec = regressions[key]
+ require(row == json.loads((evidence / (key + '.json')).read_text()) and row.get('verdict') == 'PASS'
+ and row.get('candidateCommit') == manifest['candidateCommit'] and row.get('binarySha256') == manifest['binarySha256']
+ and all(row.get(name) == spec[name] for name in ('test', 'sourceAssertions', 'fenceKind'))
+ and row.get('limitations') == spec['limits'] and row.get('fleetPassClaimed') is False,
+ "native result changed exact source, scope or executable identity")
+ command = row.get('command', [])
+ require(len(command) == 5 and Path(command[0]).name == binary.name
+ and command[1:] == exact_command(binary, spec['test'], manifest['candidateCommit'])[1:]
+ and row.get('runtimeEnvironment') == manifest['runtimeEnvironment'],
+ "native result has no exact one-test invocation")
+ for listing in listings: exact_listing(listing, spec['test'])
+ output = (evidence / (key + '.log')).read_text()
+ test_result(output, spec['test'], row.get('exitCode'))
+ events = barrier_events(output, spec)
+ require(row.get('emittedBarrierLogsClaimed') is bool(events)
+ and (row.get('observedBarrierEvents') == events if events else 'observedBarrierEvents' not in row),
+ "native report invented or omitted original barrier evidence")
+ if events: observed[key] = events
+ return {'complete': True, 'candidateCommit': manifest['candidateCommit'], 'binarySha256': manifest['binarySha256'],
+ 'exactTests': len(rows), 'observedBarrierEvents': observed, 'fleetPassClaimed': False, 'canonicalCasePassClaimed': False}
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ commands = parser.add_subparsers(dest='command', required=True)
+ compiling = commands.add_parser('build')
+ compiling.add_argument('--source', type=Path, required=True)
+ compiling.add_argument('--commit', required=True)
+ compiling.add_argument('--out', type=Path, required=True)
+ running = commands.add_parser('run')
+ running.add_argument('--directory', type=Path, required=True)
+ running.add_argument('--selection')
+ running.add_argument('--out', type=Path, required=True)
+ args = parser.parse_args()
+ if args.command == 'build':
+ build(args.source.resolve(strict=True), args.commit, args.out.resolve())
+ else:
+ run(args.directory.resolve(strict=True), args.selection.split(',') if args.selection is not None else None, args.out.resolve())
+
+
+if __name__ == '__main__':
+ try:
+ main()
+ except Exception as error:
+ print(f'native validation failed: {error}', file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_heal/network.py b/scripts/validation_heal/network.py
new file mode 100644
index 00000000..73b91fa1
--- /dev/null
+++ b/scripts/validation_heal/network.py
@@ -0,0 +1,764 @@
+#!/usr/bin/env python3
+"""I03: actual bidirectional partitions, independent quorum witnesses and ACK reconciliation."""
+
+import argparse
+import copy
+from datetime import datetime, timedelta, timezone
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import secrets
+import signal
+import sys
+import time
+from urllib.parse import quote
+
+try:
+ from .heal_probe import Probe, canonical, compare_inventories, payload, require, save
+ from .heal_workload import Recorder, RecordedCallError, seed, verify_seed
+ from .healthy import BASE_OPTIONS, Fleet, physical_content, validate_topology
+ from .inspector import verify as verify_inspector
+ from .matrix import select
+ from .shards import files, map_target, run_task, validate_fault, validate_repaired_physical, verify_cohort
+ from .versions import public_version_id, suspend_fixture, validate_version_receipts, verify_quorum_contract, validate_unrecoverable_get
+ from .markers import add_marker, verify_markers
+ from .write_quorum import expected_reads, resolve_put, negative_put, STORAGE_REFERENCE
+ from .quorum_boundary import LOCK_REFERENCE, lock_reference
+ from .stop import idle_fleet
+ from .network_gate import TTL_SECONDS
+ from .scanner_repair import JournalWindow
+ from .network_fixtures import db_parameters, seed_db, map_db, prove_db, verify_db
+ from .network_load_session import LoadSession
+ from .network_load_repair import source_reference as delete_source_reference, classify_deletes, recover_controls
+ from .network_retention import prepare as prepare_retention, isolate as isolate_retention
+except ImportError:
+ from heal_probe import Probe, canonical, compare_inventories, payload, require, save
+ from heal_workload import Recorder, RecordedCallError, seed, verify_seed
+ from healthy import BASE_OPTIONS, Fleet, physical_content, validate_topology
+ from inspector import verify as verify_inspector
+ from matrix import select
+ from shards import files, map_target, run_task, validate_fault, validate_repaired_physical, verify_cohort
+ from versions import public_version_id, suspend_fixture, validate_version_receipts, verify_quorum_contract, validate_unrecoverable_get
+ from markers import add_marker, verify_markers
+ from write_quorum import expected_reads, resolve_put, negative_put, STORAGE_REFERENCE
+ from quorum_boundary import LOCK_REFERENCE, lock_reference
+ from stop import idle_fleet
+ from network_gate import TTL_SECONDS
+ from scanner_repair import JournalWindow
+ from network_fixtures import db_parameters, seed_db, map_db, prove_db, verify_db
+ from network_load_session import LoadSession
+ from network_load_repair import source_reference as delete_source_reference, classify_deletes, recover_controls
+ from network_retention import prepare as prepare_retention, isolate as isolate_retention
+
+
+CASE_ID = 'heal.network-partition-and-lock-quorum'
+BRANCHES = ('minority-partition', 'read-boundary', 'write-boundary', 'lock-boundary', 'rejoin')
+SIZE = 5 * 1024**2 + 97
+REFERENCE = {'candidateCommit': LOCK_REFERENCE['candidateCommit'], 'lock': LOCK_REFERENCE,
+ 'storage': STORAGE_REFERENCE, 'topology': 'four original nodes, sixteen original members, explicit EC:8',
+ 'network': {'servicePort': 9000, 'managementPort': 22, 'ttlSeconds': TTL_SECONDS,
+ 'rules': 'one owned nft table per host; exact peer IPv4 and TCP source/destination port',
+ 'mutation': 'all expiry timers armed before the first atomic table transaction'},
+ 'load': {'fixtureObjects': 4, 'versionsPerObject': 3, 'retainedNullVersions': 4, 'DBObjects': 128,
+ 'currentAndHistoricalMarkers': True, 'partitionVersioning': 'Enabled after witnessed Suspended/null operations',
+ 'partitionPUTs': 2, 'recoveredPUTs': 1,
+ 'automaticRetries': 0, 'negativeTaskDeadlineSeconds': 330},
+ 'writePolicy': 'three-peer groups retain R but use independent private gates to measure W-1; two-peer groups lose W and exclusive lock quorum'}
+
+
+# Storage geometry is reviewed independently of the namespace lock policy.
+STORAGE_REFERENCES = {
+ STORAGE_REFERENCE['candidateCommit']: STORAGE_REFERENCE,
+ '1c7fc818dd858247c45b39542abd8291b1e6e31b': {
+ **STORAGE_REFERENCE, 'candidateCommit': '1c7fc818dd858247c45b39542abd8291b1e6e31b'},
+}
+
+
+GET_SOURCE_FILES = {
+ '072b57b37559fbe626aeda8abf61617fdcbdd926': {
+ 'rustfs/src/error.rs': '656d38972c06339015c174405571a94f19d8dd64a680e1a7ca631f80205f99a7',
+ 'crates/ecstore/src/set_disk/ops/object.rs': 'f0f407509b5a12803a191cc1c3a9eb079a15a80a4bbcacca70141150364628ca',
+ 'crates/heal/src/heal/task/heal_bucket.rs': '2f89bc694d78b68cc2c505fdd854770176fb87cc97d31ea80a1b810f47d8c881'},
+ '1c7fc818dd858247c45b39542abd8291b1e6e31b': {
+ 'rustfs/src/error.rs': 'e7cff0e28978b49d3ee0e8c24da1fa439b3118a7d3f7304edf00dc0535691598',
+ 'crates/ecstore/src/set_disk/ops/object.rs': 'fdcfda1a8811a93537f9cd47cadac7380bee66b3d14242ca91500bbf38e6bedb',
+ 'crates/heal/src/heal/task/heal_bucket.rs': 'fa38b21ee7997a909fc5f0671d777fd49983ccaaa18ced85bae8de76b75835ad'},
+}
+
+
+STORAGE_REFERENCES['85c706cac09d53d0649487b8f1865f18beb53225'] = {
+ **STORAGE_REFERENCES['1c7fc818dd858247c45b39542abd8291b1e6e31b'],
+ 'candidateCommit': '85c706cac09d53d0649487b8f1865f18beb53225'}
+GET_SOURCE_FILES['85c706cac09d53d0649487b8f1865f18beb53225'] = {
+ **GET_SOURCE_FILES['1c7fc818dd858247c45b39542abd8291b1e6e31b'],
+ 'rustfs/src/error.rs': 'c6a8d569b295659cf5a26a44bb3d8ba809c4ca3405d87525624699d161e08970',
+ 'rustfs/src/storage/ecfs.rs': '68d8ae2428504010adeb7bc51d2309496260898e4c74e5577a0af4f1bfe13c85'}
+
+STORAGE_REFERENCES['62ff96774fa4139d45f14d70b70519aae8070ac0'] = {
+ **STORAGE_REFERENCES['85c706cac09d53d0649487b8f1865f18beb53225'],
+ 'candidateCommit': '62ff96774fa4139d45f14d70b70519aae8070ac0'}
+GET_SOURCE_FILES['62ff96774fa4139d45f14d70b70519aae8070ac0'] = {
+ **GET_SOURCE_FILES['85c706cac09d53d0649487b8f1865f18beb53225'],
+ 'rustfs/src/error.rs': '267cf292f0af47bca782ebb4de119a5bbe119af82111dac0bbf60f1311ef8383',
+ 'rustfs/src/storage/ecfs.rs': 'e0c4ce6569f75771c73de043f8416645bcd1de86a1192a79f45d7418953086f9'}
+
+
+def source_reference(candidate_commit):
+ require(isinstance(candidate_commit, str) and candidate_commit in STORAGE_REFERENCES,
+ 'network storage policy is not reviewed for this candidate')
+ return {**copy.deepcopy(REFERENCE), 'candidateCommit': candidate_commit,
+ 'lock': lock_reference(candidate_commit),
+ 'storage': copy.deepcopy(STORAGE_REFERENCES[candidate_commit]),
+ 'getPolicy': {'lockBeforeObjectMetadata': True,
+ 'lockUnavailableGet': {'httpStatus': 503, 'code': 'ServiceUnavailable'},
+ 'allVersionRecovery': {'recursive': True},
+ 'sourceFiles': {**GET_SOURCE_FILES[candidate_commit],
+ 'crates/heal/src/heal/channel.rs': '7f669ca43b9559db9b97fba6b419a6c76a94d76acd8526db132d74b7c534d6b0'}}}
+
+
+def negative_get_contract(contract, side, plan):
+ policy = source_reference(contract['candidateCommit'])['getPolicy']
+ require(contract.get('recognized') is True and contract['sourceFiles'].get('rustfs/src/error.rs')
+ == policy['sourceFiles']['rustfs/src/error.rs'], 'partition GET error mapping differs from exact candidate')
+ require(type(side.get('lockClients')) is int and 0 <= side['lockClients'] <= 4
+ and plan.get('sharedLockQuorum') == 2, 'partition GET has an unknown shared-lock domain')
+ if side['lockClients'] < plan['sharedLockQuorum']:
+ return {**contract, 'unrecoverableGet': policy['lockUnavailableGet']}
+ return contract
+
+
+def recover_versions(probe, fleet, binding, bucket, expected, target, active):
+ require(len(expected) >= 3 and len({entry['key'] for entry in expected}) == 1
+ and len({entry['versionId'] for entry in expected}) == len(expected)
+ and (target['key'], target['versionId']) in {(entry['key'], entry['versionId']) for entry in expected}
+ and sum(not entry['deleteMarker'] and entry['versionId'] != 'null' for entry in expected) >= 3,
+ 'network recovery requires all acknowledged versions of one exact three-version key')
+ current = run_task(probe, fleet, {'bucket': bucket, 'key': target['key']}, 2, active,
+ option_overrides={'recursive': True}, status_endpoints=tuple(binding['endpoints'])[1:])
+ return validate_version_receipts(current, bucket, expected, 2,
+ required_repairs={(target['key'], target['versionId'])})
+
+
+def seed_versions(recorder, fleet, bucket):
+ expected = seed(recorder, bucket, 'DV', 4, [SIZE])
+ expected = suspend_fixture(recorder, bucket, expected)
+ suspended = fleet.record('suspended-null-S3-baseline', recorder.probe.inventory(bucket))
+ fleet.record('suspended-null-ACK-manifest', {'expected': expected, **verify_seed(expected, suspended)})
+ # Null overwrites and marker removals are real S3 operations. Re-enable
+ # before the partition so new writes must return distinct non-null IDs.
+ recorder.call('put_bucket_versioning', {'Bucket': bucket, 'VersioningConfiguration': {'Status': 'Enabled'}})
+ require(recorder.call('get_bucket_versioning', {'Bucket': bucket}).get('Status') == 'Enabled',
+ 'network fixture did not persist re-enabled versioning')
+ key = 'objects/0000.bin'
+ expected = add_marker(expected, key, recorder.call('delete_object', {'Bucket': bucket, 'Key': key}))
+ fleet.record('version-fixture-coverage', version_coverage(expected))
+ return expected
+
+
+def version_coverage(expected):
+ keys = {f'objects/{index:04d}.bin' for index in range(4)}
+ require({entry['key'] for entry in expected} == keys
+ and len({(entry['key'], entry['versionId']) for entry in expected}) == len(expected),
+ 'network fixture omits a key or duplicates a version identity')
+ counts = {}
+ for key in sorted(keys):
+ entries = [entry for entry in expected if entry['key'] == key]
+ data = [entry for entry in entries if not entry['deleteMarker'] and entry['versionId'] != 'null']
+ require(len(data) == 3 and all(isinstance(entry['versionId'], str) and entry['versionId'] for entry in data)
+ and len({entry['payload']['sha256'] for entry in data}) == 3
+ and sum(entry['isLatest'] is True for entry in entries) == 1
+ and sum(entry['versionId'] == 'null' for entry in entries) == 1,
+ 'network fixture lacks three distinct data ACKs, one latest identity or its retained null version')
+ counts[key] = len(entries)
+ require(any(entry['deleteMarker'] and entry['isLatest'] for entry in expected)
+ and any(entry['deleteMarker'] and not entry['isLatest'] for entry in expected)
+ and any(entry['versionId'] == 'null' and entry['deleteMarker'] for entry in expected)
+ and any(entry['versionId'] == 'null' and not entry['deleteMarker'] for entry in expected),
+ 'network fixture lacks current/historical markers or live/deleted null identities')
+ return {'acknowledgedIdentitiesPerKey': counts, 'threeDistinctDataVersionsPerKey': True,
+ 'currentAndHistoricalMarkers': True, 'retainedLiveAndMarkerNullVersions': True,
+ 'partitionVersioning': 'Enabled', 'suspendedPartitionClaimed': False,
+ 'remainingSourceFixtures': ['DB boundary matrix', 'DL concurrent operation matrix'],
+ 'canonicalCasePassClaimed': False}
+
+
+def version_heads(probe, fleet, bucket, expected, label):
+ result = []
+ for entry in expected:
+ if entry['deleteMarker']:
+ continue
+ observed = fleet.record(label, probe.s3('head_object', {
+ 'Bucket': bucket, 'Key': entry['key'], 'VersionId': entry['versionId']}))
+ response = observed.get('response') or {}
+ require(observed.get('acknowledged') is True and observed.get('resultUnknown') is False
+ and response.get('ResponseMetadata', {}).get('HTTPStatusCode') == 200
+ and response.get('ContentLength') == entry['payload']['size']
+ and response.get('ETag') and response.get('LastModified')
+ and response.get('VersionId', 'null') == entry['versionId'],
+ 'version HEAD lost its ACK identity, length, ETag or Last-Modified')
+ result.append({'key': entry['key'], 'versionId': entry['versionId'],
+ 'properties': {name: response.get(name) for name in
+ ('ContentLength', 'ETag', 'LastModified', 'ContentType', 'Metadata')}})
+ return result
+
+
+def plan_for(snapshot, target, branch):
+ nodes = list(snapshot)
+ geometry = target['logical']['erasure']
+ cohort = target['cohort']
+ require(branch in BRANCHES and len(nodes) == 4 and geometry['data_blocks'] == geometry['parity_blocks'] == 8
+ and len(cohort) == len({(row['node'], row['disk']) for row in cohort}) == 16
+ and all({row['disk'] for row in cohort if row['node'] == node} == set(range(4)) for node in nodes),
+ 'I03 requires the observed balanced sixteen-member cross-node set')
+ groups = [[0, 1], [2, 3]] if branch in {'read-boundary', 'lock-boundary'} else [[0, 1, 2], [3]]
+ require(target['node'] == nodes[3], 'partition defect must be on the isolated fourth original member')
+ gates = {nodes[0]: [0, 1, 2, 3]} if len(groups[0]) == 3 else {}
+ sides = []
+ for group in groups:
+ slots = [row for row in cohort if nodes.index(row['node']) in group]
+ sources = len(slots) - int(3 in group)
+ writable = len(slots) - sum(len(gates.get(nodes[index], [])) for index in group)
+ require(writable < 9 and len(group) < 3 or writable == 8 and len(group) == 3,
+ 'partition side still has safe write admission')
+ sides.append({'members': group, 'endpoint': f'node{group[0] + 1}', 'readableSources': sources,
+ 'metadataAuthorities': len(slots), 'writableDrives': writable, 'lockClients': len(group),
+ 'GETAllowed': sources >= 8, 'PUTAllowed': False, 'exclusiveLockQuorumPresent': len(group) >= 3})
+ require(sides[0]['readableSources'] >= 8 and (len(groups[0]) != 2 or sides[0]['readableSources'] == 8),
+ 'read boundary does not preserve the exact actual R sources')
+ return {'groups': groups, 'sides': sides, 'readQuorum': 8, 'writeQuorum': 9,
+ 'sharedLockQuorum': 2, 'exclusiveLockQuorum': 3, 'gates': gates,
+ 'blocked': {nodes[index]: [other for other in range(4) if not any(index in group and other in group for group in groups)]
+ for index in range(4)}}
+
+
+def validate_witness(observed, plan, nodes):
+ require(set(observed) == set(nodes), 'network witness omitted an original node')
+ for index, node in enumerate(nodes):
+ value = observed[node]
+ require(value.get('state') == 'ACTIVE' and value.get('localIndex') == index
+ and value.get('expiryArmed') is True and value.get('process', {}).get('pid', 0) > 1,
+ 'network witness has no actual original service or active expiry')
+ rows = value.get('probes', [])
+ require(len(rows) == 3 and {row['peerIndex'] for row in rows} == set(range(4)) - {index},
+ 'network probes omitted or duplicated a peer direction')
+ require(all(row['service']['connected'] is (row['peerIndex'] not in plan['blocked'][node])
+ and row['management']['connected'] is True for row in rows),
+ 'partition matrix or management-channel preservation differs')
+ require(sum(row['packets'] for row in value['countersAfter']) > sum(row['packets'] for row in value['countersBefore']),
+ 'network probe has no packet-counter evidence')
+ return {'allTwelvePeerDirectionsVerified': True, 'actualPacketCounters': True,
+ 'allManagementDirectionsReachable': True, 'servicesNotStoppedToSimulatePartition': True}
+
+
+def validate_partial(value, peers):
+ coverage = value.get('coverage') or {}
+ require(value.get('clusterStatusComplete') is False and value.get('state') in {'degraded', 'active'}
+ and all(type(coverage.get(name)) is int for name in ('expected', 'responded', 'unknown'))
+ and coverage['expected'] == 4 and coverage['responded'] == peers and coverage['unknown'] == 4 - peers
+ and isinstance(coverage.get('reasons'), list) and coverage['reasons'],
+ 'partitioned endpoint falsely reports complete/global health or hides actual peer coverage')
+ return {'responded': peers, 'unknown': 4 - peers, 'globalCompleteClaimed': False}
+
+
+def validate_negative_task(current, bucket, expected, target):
+ outcome = current.get('outcome') or {}
+ execution = outcome.get('execution', {})
+ require(current.get('summary') in {'finished', 'stopped'} and execution.get('state') in {
+ 'completed', 'completed_with_errors', 'aborted'} and outcome.get('coverage') in {'partial', 'complete', 'unknown'}
+ and current.get('truncated', False) is False and outcome.get('objectsTruncated') is False,
+ 'partition task has no bounded explicit unresolved terminal result')
+ wanted = {(row['key'], row['versionId']) for row in expected}
+ target_pair = target['key'], target['versionId']
+ counters = outcome.get('counters', {})
+ for name in ('processed', 'healed', 'unchanged', 'skipped', 'failed', 'unknown', 'attemptFailures'):
+ require(type(counters.get(name)) is int and counters[name] >= 0, 'partition task omits typed error counters')
+ require(counters.get('overflowed') is False and counters['healed'] == 0
+ and counters['processed'] == sum(counters[name] for name in ('healed', 'unchanged', 'skipped', 'failed'))
+ and counters['unknown'] <= counters['skipped'] and counters['processed'] <= len(wanted),
+ 'partition task hides unresolved work or claims an unsafe repair')
+ seen, unresolved, states = set(), set(), []
+ for row in outcome.get('objects', []):
+ identity = row.get('identity', {})
+ pair = identity.get('object'), public_version_id(identity.get('versionId'))
+ require(identity.get('bucket') == bucket and pair in wanted and pair not in seen,
+ 'partition task reports foreign or duplicate responsibility')
+ state = row.get('disposition', {}).get('state')
+ require(state in {'verified_healthy', 'failed', 'deferred', 'unknown', 'cancelled'},
+ 'partition task reports repaired/absent or an unrelated disposition')
+ if state == 'verified_healthy':
+ require(pair != target_pair and identity.get('bucketIncarnationId')
+ and type(identity.get('poolIndex')) is int and identity['poolIndex'] == 0
+ and type(identity.get('setIndex')) is int and identity['setIndex'] == 0,
+ 'missing target was reported healthy, or a healthy receipt lacks authority')
+ else:
+ unresolved.add(pair)
+ seen.add(pair)
+ states.append(state)
+ require(counters['processed'] == len(seen), 'bounded partition counters differ from exact version receipts')
+ require(counters['unchanged'] == states.count('verified_healthy') and counters['failed'] == states.count('failed')
+ and counters['skipped'] == sum(states.count(state) for state in ('deferred', 'unknown', 'cancelled'))
+ and counters['unknown'] == states.count('unknown') and counters['attemptFailures'] >= counters['failed'],
+ 'partition counters conceal the actual unresolved receipt dispositions or failed attempts')
+ require(target_pair in unresolved or execution == {'state': 'aborted', 'reason': 'untraversable'}
+ and outcome['coverage'] != 'complete' and counters['processed'] == 0,
+ 'partition task lost the damaged version obligation or silently completed it')
+ require(not counters['failed'] or execution['state'] == 'completed_with_errors',
+ 'partition task erases its failed execution status')
+ return {'missingVersionUnresolved': True, 'unsafeRepairClaimed': False,
+ 'execution': execution, 'coverage': outcome['coverage'], 'counters': counters}
+
+
+def negative_heal(probe, fleet, bucket, expected, target, active):
+ path = '/rustfs/admin/v3/heal/' + quote(bucket, safe='') + '/' + quote(target['key'], safe='/')
+ admission = fleet.record('partition-Heal-admission', probe.admin(path, [], json.dumps({**BASE_OPTIONS, 'recursive': True}).encode()))
+ if admission.get('httpStatus') != 200:
+ # No retry: require an actual structured storage/lock refusal. An
+ # absent token or transport exception alone cannot prove safe refusal.
+ body = admission.get('response')
+ if isinstance(body, str):
+ import xml.etree.ElementTree as ET
+ body = {item.tag.rsplit('}', 1)[-1]: item.text for item in ET.fromstring(body).iter()}
+ require(admission.get('httpStatus') in {500, 503} and isinstance(body, dict)
+ and (body.get('Code') or body.get('code')) in {'InternalError', 'XMinioWriteQuorum', 'XMinioLockTimeout', 'ServiceUnavailable'}
+ and not body.get('clientToken'), 'partition Heal admission did not return a structured safe refusal')
+ return {'admissionRefused': True, 'repairClaimed': False, 'requestRetried': False}
+ token = admission.get('response', {}).get('clientToken')
+ require(isinstance(token, str) and token, 'partition Heal admitted without an actual token')
+ active.append((probe.endpoint_name, path, token))
+ deadline = time.monotonic() + 330
+ while True:
+ response = fleet.record('partition-Heal-status', probe.admin(path, [('clientToken', token)], b''))
+ require(response.get('httpStatus') == 200 and isinstance(response.get('response'), dict),
+ 'partition task status disappeared or has an unknown response')
+ current = response['response']
+ if current.get('summary') != 'running':
+ break
+ require(time.monotonic() < deadline, 'partition task exceeded its frozen unresolved-task deadline')
+ time.sleep(0.2)
+ proof = validate_negative_task(current, bucket, expected, target)
+ repeated = fleet.record('partition-terminal-repeat', probe.admin(path, [('clientToken', token)], b''))
+ require(repeated.get('httpStatus') == 200 and canonical(repeated.get('response', {}).get('outcome')) == canonical(current['outcome']),
+ 'negative partition responsibility changed at its original token')
+ active.remove((probe.endpoint_name, path, token))
+ return {**proof, 'taskId': token, 'requestRetried': False}
+
+
+def project(snapshot, bucket, keys):
+ result = copy.deepcopy(snapshot)
+ prefixes = tuple(bucket + '/' + key + '/' for key in keys)
+ for value in result.values():
+ value['files'] = [row for row in value['files'] if not row['path'].startswith(prefixes)]
+ return result
+
+
+def unique_put(probe, directory, bucket, key, rows):
+ body = payload(f'{bucket}/{key}/0', SIZE)
+ entry = {'endpoint': probe.endpoint_name, 'key': key, 'payload': {'size': SIZE, 'sha256': hashlib.sha256(body).hexdigest()}}
+ rows.append(entry) # Register before invocation, including collector failures.
+ recorder = Recorder(probe, directory / key.replace('/', '-'))
+ try:
+ response = recorder.call('put_object', {'Bucket': bucket, 'Key': key,
+ 'ContentType': 'application/octet-stream', 'Metadata': {'heal-fixture': 'I03', 'nonce': key},
+ 'Tagging': 'suite=heal&fixture=I03'}, body)
+ except RecordedCallError as exc:
+ entry['observed'] = exc.observation
+ else:
+ entry['observed'] = {'acknowledged': True, 'resultUnknown': False, 'clientCallInvoked': True,
+ 'response': response, 'payload': entry['payload']}
+ return entry
+
+
+def resolve_writes(fleet, binding, bucket, rows):
+ for row in rows:
+ reads = [fleet.record('all-peer-unique-write-resolution', Probe(binding, endpoint).s3('get_object', {
+ 'Bucket': bucket, 'Key': row['key']})) for endpoint in binding['endpoints']]
+ observed = row.get('observed', {'acknowledged': False, 'resultUnknown': True})
+ resolution = resolve_put(observed, reads, row['payload'])
+ if resolution['state'] == 'COMMITTED':
+ versions = {read.get('response', {}).get('VersionId') for read in reads}
+ require(len(versions) == 1 and next(iter(versions)) not in {None, 'null'},
+ 'peers disagree on the unique committed version identity')
+ row['versionId'] = next(iter(versions))
+ require(not observed.get('acknowledged') or observed['response'].get('VersionId') == row['versionId'],
+ 'unique PUT ACK was replaced by a different committed version')
+ row['resolution'] = resolution
+ return {'allOriginalRequestsResolved': True, 'automaticRetries': 0}
+
+
+def release_network(fleet):
+ for node in tuple(fleet.network_gates):
+ value = fleet.record('release-owned-network-table', {'node': node, **fleet.remote(node, 'network-release')})
+ require(value['state'] == 'CLEAR' and value.get('rulesRemaining') == 0,
+ 'network recovery did not remove every owned rule')
+ fleet.network_gates.remove(node)
+ require(value.get('expiryTriggered') is False, 'partition expired before its complete evidence window')
+
+
+def partition_get(probe, fleet, params):
+ """Retain one GET's raw response and journal window before its oracle can fail."""
+ identity_fields = ('pid', 'startTicks', 'bootId', 'unit', 'executable', 'binarySha256',
+ 'mountNamespace', 'commandSha256', 'environmentFileSha256', 'settings')
+ first_sequence = fleet.sequence + 1
+ before = fleet.record('partition-GET-process-before', {node: fleet.remote(node, 'process') for node in fleet.nodes})
+ require(all(all(field in value for field in identity_fields) and type(value['pid']) is int and value['pid'] > 1
+ and value['binarySha256'] == probe.binding['candidateBinarySha256'] for value in before.values()),
+ 'partition GET diagnostic lacks a live candidate process')
+ # Raw pages are retained; parsed events are not a causal proof or oracle.
+ window = JournalWindow(fleet, 'partition-GET-journal', event_targets=set())
+ observed, request_sequence = None, None
+ try:
+ observed = fleet.record('partition-explicit-version-GET', probe.s3('get_object', params))
+ request_sequence = fleet.sequence
+ finally:
+ errors, after = [], {}
+ try:
+ window.capture()
+ except Exception as exc:
+ errors.append({'stage': 'journal', 'type': type(exc).__name__, 'message': str(exc)})
+ for node in fleet.nodes:
+ try:
+ after[node] = fleet.remote(node, 'process')
+ require(all(field in after[node] and after[node][field] == before[node][field] for field in identity_fields),
+ 'partition GET crossed a candidate process identity change')
+ except Exception as exc:
+ errors.append({'stage': 'process', 'node': node, 'type': type(exc).__name__, 'message': str(exc)})
+ fleet.record('partition-GET-process-after', after)
+ if observed is None:
+ errors.append({'stage': 'request', 'message': 'GET raised before returning a raw observation'})
+ response = (observed or {}).get('response') or {}
+ diagnostics = fleet.record('partition-GET-diagnostics', {
+ 'complete': not errors, 'endpoint': probe.endpoint_name, 'params': params,
+ 'requestId': response.get('ResponseMetadata', {}).get('RequestId'),
+ 'firstEvidenceSequence': first_sequence, 'requestEvidenceSequence': request_sequence,
+ 'lastEvidenceSequence': fleet.sequence, 'errors': errors,
+ 'causalAttributionClaimed': False, 'automaticRequestRetries': 0})
+ return observed, diagnostics
+
+
+def partition_db(probe, fleet, bucket, representatives, side, plan, contract):
+ results = []
+ for row in representatives:
+ entry = row['expected']
+ observed, diagnostics = partition_get(probe, fleet, {
+ 'Bucket': bucket, 'Key': entry['key'], 'VersionId': entry['versionId']})
+ read_quorum = row['target']['logical']['erasure']['data_blocks']
+ readable = side['lockClients'] >= plan['sharedLockQuorum'] and side['metadataAuthorities'] >= read_quorum
+ if readable:
+ require(observed.get('acknowledged') is True and observed.get('resultUnknown') is False
+ and observed.get('response', {}).get('ResponseMetadata', {}).get('HTTPStatusCode') == 200
+ and observed.get('payload') == entry['payload'],
+ 'R-capable partition lost an acknowledged DB boundary control')
+ else:
+ validate_unrecoverable_get(observed, negative_get_contract(contract, side, plan))
+ require(diagnostics['complete'], 'DB boundary GET diagnostics are incomplete')
+ results.append({'key': entry['key'], 'shape': row['shape'], 'size': entry['payload']['size'],
+ 'readQuorum': read_quorum, 'availableSources': side['metadataAuthorities'], 'readable': readable})
+ return results
+
+
+def run_branch(args, nodes, branch):
+ directory = args.evidence / 'H2' / branch
+ directory.mkdir(parents=True)
+ run_id = f"gh-{os.environ['GITHUB_RUN_ID']}-{os.environ.get('GITHUB_RUN_ATTEMPT', '1')}-h2"
+ fleet = Fleet(nodes, directory, run_id, args.tool_directory, runtime_profile='lock-quorum')
+ candidate = json.loads(args.candidate.read_text())
+ reference = source_reference(candidate['source_sha'])
+ bucket = 'rv-heal-' + hashlib.sha256(run_id.encode()).hexdigest()[:16]
+ access, secret = secrets.token_hex(16), secrets.token_hex(24)
+ os.environ.update(HEAL_ACCESS_KEY=access, HEAL_SECRET_KEY=secret)
+ binding = {'schemaVersion': 1, 'runId': run_id, 'jumpHostOnly': True, 'exclusiveHealService': True,
+ 'leaseExpiresAt': (datetime.now(timezone.utc) + timedelta(minutes=30)).isoformat(), 'bucketPrefix': bucket,
+ 'endpoints': {f'node{index}': f'http://rustfs-node{index}:9000' for index in range(1, 5)}, 'defaultEndpoint': 'node1',
+ 'candidateCommit': candidate['source_sha'], 'harnessCommit': args.testing_commit,
+ 'candidatePackageSha256': candidate['package_sha256'], 'candidateBinarySha256': args.binary_sha,
+ 'rcBinarySha256': hashlib.sha256(args.rc_bin.read_bytes()).hexdigest(), 'maxInventoryObjects': 128,
+ 'maxInventoryBytes': 512 * 1024**2}
+ fleet.record('binding', binding)
+ fleet.record('network-source-contract', reference)
+ fleet.record('DL-delete-recovery-source-contract', delete_source_reference(candidate['source_sha']))
+ manifest = fleet.record('inspector-build', verify_inspector(args.tool_directory, candidate))
+ contract = verify_quorum_contract(manifest, candidate)
+ result = {'topology': 'H2', 'branch': branch, 'verdict': 'RUNNING', 'cleanup': 'PENDING'}
+ probe, active, recovery_active, rows, dl = Probe(binding), [], [], [], None
+ try:
+ fleet.deploy(args.binary_sha, bucket, access, secret)
+ dl_endpoints = ['node1', 'node3' if branch in {'read-boundary', 'lock-boundary'} else 'node4']
+ dl = LoadSession(binding, bucket, dl_endpoints, directory / 'DL')
+ recorder = Recorder(probe, directory / 'fixture')
+ expected = seed_versions(recorder, fleet, bucket)
+ key_versions = [entry for entry in expected if entry['key'] == 'objects/0001.bin']
+ selected = next(entry for entry in key_versions if not entry['deleteMarker'] and entry['versionId'] == 'null')
+ dl_fixture = dl.seed()
+ expected += dl_fixture['expected']
+ calibration = fleet.snapshot('DB-size-calibration-live')
+ calibration_target = map_target(calibration, bucket, selected['key'], 'data', 1, selected['versionId'])
+ db_plan = fleet.record('DB-source-size-plan', db_parameters(manifest, candidate, calibration_target, calibration))
+ db_bucket = bucket + '-db'
+ db_expected = seed_db(Recorder(probe, directory / 'DB-fixture'), db_bucket, db_plan)
+ db_baseline = fleet.record('DB-S3-baseline', probe.inventory(db_bucket, unversioned_current_tags=True))
+ fleet.record('DB-ACK-manifest', {'expected': db_expected, **verify_seed(db_expected, db_baseline)})
+ db_heads = version_heads(probe, fleet, db_bucket, db_expected, 'DB-original-HEAD')
+ baseline = fleet.record('all-version-S3-baseline', probe.inventory(bucket))
+ fleet.record('exact-ACK-manifest', {'expected': expected, **verify_seed(expected, baseline)})
+ dl.register_baseline(baseline)
+ original_heads = version_heads(probe, fleet, bucket, expected, 'original-version-HEAD')
+ for key in sorted({entry['key'] for entry in expected if entry['deleteMarker']}):
+ verify_markers(probe, fleet, bucket, key, expected)
+ fleet.stop()
+ physical_buckets = [bucket, db_bucket]
+ original = fleet.snapshot('physical-baseline', quiescent=True, buckets=physical_buckets)
+ dl.register_business_baseline(original)
+ dl.capture_mpu(fleet, 'baseline')
+ validate_topology(original, 'H2')
+ target = map_target(original, bucket, selected['key'], 'data', 1, selected['versionId'], latest_required=False)
+ target = {**target, **next(row for row in target['cohort'] if row['node'] == nodes[3])}
+ cohorts = [map_target(original, bucket, entry['key'], 'data', 1, entry['versionId'], latest_required=False)
+ for entry in expected if not entry['deleteMarker']]
+ db_representatives, db_witnesses = map_db(original, db_bucket, db_expected, db_plan)
+ fleet.record('DB-all128-physical-shapes', {'objects': db_witnesses, 'allBoundariesObserved': True})
+ plan = fleet.record('actual-partition-quorum-plan', plan_for(original, target, branch))
+ require(tuple(side['endpoint'] for side in plan['sides']) == dl.endpoints,
+ 'DL endpoints differ from the actual original cohort partition')
+ params = {'disk': target['disk'], 'faultId': 'I03-missing-target'}
+ registration = target['node'], params
+ fleet.faults.append(registration)
+ receipt = fleet.record('exact-missing-target', fleet.remote(target['node'], 'apply-fault', **params,
+ relativePath=target['relativePath'], kind='missing', expectedSha256=target['sha256']))
+ faulted = fleet.snapshot('physical-exact-fault', quiescent=True, buckets=physical_buckets)
+ validate_fault(original, faulted, target, receipt)
+ for node, disks in plan['gates'].items():
+ fleet.quorum_gates.add(node)
+ fleet.record('independent-W-minus-one-gate', {'node': node, **fleet.remote(node, 'quorum-gate', disks=disks)})
+ fleet.start()
+ preflight = fleet.record('network-actual-address-process-attestation', {node: fleet.remote(node, 'network-preflight') for node in nodes})
+ addresses = preflight[nodes[0]]['addresses']
+ require(all(value['addresses'] == addresses and value['localIndex'] == index
+ and value['exclusiveServicePort'] == 9000 for index, value in enumerate(preflight.values())),
+ 'actual host address, service ownership or peer alias order differs')
+ # Every independent expiry is ready before the first rule is installed.
+ for index, node in enumerate(nodes):
+ fleet.network_gates.add(node)
+ fleet.record('network-expiry-and-rule-intent', {'node': node, **fleet.remote(node, 'network-prepare',
+ addresses=addresses, localIndex=index, blockedIndices=plan['blocked'][node])})
+ for node in nodes:
+ fleet.record('atomic-owned-table-activation', {'node': node, **fleet.remote(node, 'network-activate')})
+ def witness(label):
+ raw = fleet.record(label, {node: fleet.remote(node, 'network-observe') for node in nodes})
+ return validate_witness(raw, plan, nodes)
+ result['networkBefore'] = witness('all-directed-peer-probes-before')
+ gates = fleet.record('actual-service-readable-writable-roots', {node: fleet.remote(node, 'quorum-gate-probe',
+ relativePath=f"{bucket}/{selected['key']}/xl.meta", expectedReads=expected_reads(original, node, f"{bucket}/{selected['key']}/xl.meta")) for node in nodes})
+ for side in plan['sides']:
+ require(sum(row['writable'] for index in side['members'] for row in gates[nodes[index]]['disks']) == side['writableDrives']
+ and all(row['readableMetadata'] and row['sameServiceUidAndMountNamespace'] for index in side['members']
+ for row in gates[nodes[index]]['disks']), 'actual independent R/W witnesses differ from partition plan')
+ result['DL-load'] = fleet.record('DL-partition-concurrent-load', dl.run_load({
+ side['endpoint']: side['lockClients'] >= plan['sharedLockQuorum']
+ and side['metadataAuthorities'] >= plan['readQuorum'] for side in plan['sides']}))
+ require(result['DL-load']['complete'] is True,
+ 'DL partition load is incomplete or has an uncertain/unexpected original response')
+ for side in plan['sides']:
+ endpoint = side['endpoint']
+ local_probe = Probe(binding, endpoint)
+ for version in key_versions:
+ read, diagnostics = partition_get(local_probe, fleet, {
+ 'Bucket': bucket, 'Key': version['key'], 'VersionId': version['versionId']})
+ available = side['metadataAuthorities'] - int(3 in side['members'] and version['versionId'] == selected['versionId'])
+ if available >= plan['readQuorum']:
+ require(read.get('acknowledged') is True and read.get('resultUnknown') is False
+ and read.get('payload') == version['payload'], 'R-capable partition cannot redeem its exact original ACK')
+ else:
+ scoped_contract = negative_get_contract(contract, side, plan)
+ fleet.record('partition-GET-error-contract', {'endpoint': endpoint, 'versionId': version['versionId'],
+ 'lockClients': side['lockClients'], 'sharedLockQuorum': plan['sharedLockQuorum'],
+ 'availableSources': available, 'readQuorum': plan['readQuorum'],
+ 'requiredError': scoped_contract['unrecoverableGet']})
+ validate_unrecoverable_get(read, scoped_contract)
+ require(diagnostics['complete'], 'partition GET diagnostics are incomplete; inspect retained raw response')
+ row = unique_put(local_probe, directory, bucket, 'partition/' + endpoint + '.bin', rows)
+ row['negativeProof'] = negative_put(row['observed'])
+ result.setdefault('DB-partition-controls', {})[endpoint] = partition_db(
+ local_probe, fleet, db_bucket, db_representatives, side, plan, contract)
+ result.setdefault('partitionTasks', {})[endpoint] = negative_heal(local_probe, fleet, bucket, key_versions, target, active)
+ status = fleet.record('partition-raw-background', local_probe.admin('/rustfs/admin/v3/background-heal/status', [], b''))
+ require(status.get('httpStatus') == 200 and isinstance(status.get('response'), dict), 'partition background status unavailable')
+ result.setdefault('coverage', {})[endpoint] = validate_partial(status['response'], len(side['members']))
+ result['networkAfter'] = witness('all-directed-peer-probes-after')
+ fleet.record('partition-process-resource-observations', {node: fleet.remote(node, 'process-metrics') for node in nodes})
+ fleet.stop()
+ scene = fleet.snapshot('negative-scene-before-release', quiescent=True, buckets=physical_buckets)
+ dl.capture_mpu(fleet, 'failure')
+ delete_records = fleet.record('DL-stopped-original-delete-records', {node: fleet.remote(node, 'network-delete-records',
+ bucket=bucket, keys=sorted(side['controlKey'] for side in dl_fixture['sides'].values()),
+ expectedBinarySha256=binding['candidateBinarySha256']) for node in nodes})
+ damage = fleet.record('DL-original-delete-failure-obligations', classify_deletes(faulted,
+ project(scene, bucket, [row['key'] for row in rows]), dl_fixture, dl.registered_receipts(), plan, nodes, delete_records))
+ for node in nodes:
+ fleet.record('partition-peer-logs', {'node': node, **fleet.remote(node, 'logs')})
+ release_network(fleet)
+ for node in tuple(fleet.quorum_gates):
+ fleet.record('release-owned-W-gate', fleet.remote(node, 'quorum-gate', mode='clear'))
+ fleet.quorum_gates.remove(node)
+ fleet.start()
+ require(physical_content(scene) == physical_content(fleet.snapshot('all-peer-return-before-independent-repair', buckets=physical_buckets)),
+ 'network recovery repaired a target before the independent product task')
+ result['recoveryResponsibility'] = recover_versions(probe, fleet, binding, bucket, key_versions, selected, recovery_active)
+ result['DL-recoveryResponsibility'] = recover_controls(probe, fleet, binding, dl_fixture, damage, recovery_active)
+ idle_fleet(fleet, binding)
+ fleet.stop()
+ after = fleet.snapshot('physical-rebuilt-before-any-recovered-GET', quiescent=True, buckets=physical_buckets)
+ repair_targets = [target, *damage['repairTargets']]
+ result['physical'] = validate_repaired_physical(original, project(after, bucket, [row['key'] for row in rows]), target,
+ repair_targets=repair_targets)
+ result['streaming'] = [verify_cohort(fleet, cohort, 'every-original-version-streaming') for cohort in cohorts]
+ result['DB-physical'] = prove_db(fleet, db_representatives)
+ fleet.faults.remove(registration)
+ dl.capture_mpu(fleet, 'pre-reconcile')
+ fleet.start()
+ result['writeResolution'] = resolve_writes(fleet, binding, bucket, rows)
+ require(all(row['resolution']['state'] == 'ABSENT' for row in rows), 'a W/lock-negative partition nevertheless committed a new version')
+ result['DL-public-reconciliation'] = dl.reconcile()
+ fleet.stop()
+ dl.capture_mpu(fleet, 'post-reconcile')
+ final_business = fleet.snapshot('DL-stopped-final-business-closure', quiescent=True, buckets=physical_buckets)
+ result['DL-closure'] = dl.finish_reconciliation(final_business, repair_targets=repair_targets)
+ require(dl.safe_to_cleanup, 'DL original mutation effects remain unresolved')
+ fleet.start()
+ released = unique_put(probe, directory, bucket, 'recovered/unique.bin', rows)
+ require(released['observed']['acknowledged'] is True and released['observed']['response'].get('VersionId') not in {None, 'null'},
+ 'fully recovered cluster cannot acknowledge a distinct safe PUT')
+ resolve_writes(fleet, binding, bucket, [released])
+ augmented = expected + [{'key': released['key'], 'versionId': released['versionId'], 'isLatest': True,
+ 'deleteMarker': False, 'payload': released['payload']}]
+ inventories = []
+ for endpoint in binding['endpoints']:
+ peer = Probe(binding, endpoint)
+ inventory = fleet.record('all-peer-complete-final-S3', peer.inventory(bucket))
+ verify_seed(augmented, inventory)
+ require(version_heads(peer, fleet, bucket, expected, 'all-peer-original-version-HEAD') == original_heads,
+ 'network recovery changed an original version HEAD or Last-Modified')
+ subset = {**inventory, 'entries': [row for row in inventory['entries'] if row['key'] != released['key']]}
+ compare_inventories(baseline, subset)
+ inventories.append(inventory)
+ for inventory in inventories[1:]:
+ compare_inventories(inventories[0], inventory)
+ result['markers'] = [verify_markers(probe, fleet, bucket, key, expected)
+ for key in sorted({entry['key'] for entry in expected if entry['deleteMarker']})]
+ result['DB-final'] = verify_db(probe, fleet, db_bucket, db_expected, db_baseline, db_representatives, db_plan)
+ for endpoint in binding['endpoints']:
+ require(version_heads(Probe(binding, endpoint), fleet, db_bucket, db_expected, 'DB-final-HEAD') == db_heads,
+ 'network recovery changed an acknowledged DB HEAD or Last-Modified')
+ require(all(row['properties'].get('Metadata') == {'heal-fixture': 'I03', 'nonce': released['key']}
+ and row['tags'] == [{'Key': 'fixture', 'Value': 'I03'}, {'Key': 'suite', 'Value': 'heal'}]
+ for inventory in inventories for row in inventory['entries'] if row['key'] == released['key']),
+ 'recovered PUT lost its public metadata or tags')
+ result.update(verdict='PASS', everyOriginalVersionPreserved=True, noForkOrStaleOverwrite=True)
+ except (Exception, KeyboardInterrupt) as exc:
+ result.update(verdict='FAIL', failure={'type': type(exc).__name__, 'message': str(exc)})
+ finally:
+ dirty = False
+ active.extend(('node1', path, token) for path, token in recovery_active)
+ for endpoint, path, token in active:
+ try:
+ fleet.record('stop-original-partition-task', Probe(binding, endpoint).admin(path, [('clientToken', token), ('forceStop', 'true')], b''))
+ except Exception as exc:
+ fleet.record('stop-partition-task-error', {'error': str(exc)})
+ if dl is not None and not dl.safe_to_cleanup:
+ # No S3 observation, rescue or ordinary cleanup may erase an
+ # incomplete seed or uncertain mutation. Preserve the stopped
+ # original scene, including refused Abort effects, by inode.
+ dirty = True
+ try:
+ fleet.stop()
+ original_failure = fleet.snapshot('DL-original-unresolved-business-before-gate-release',
+ quiescent=True, buckets=[bucket, bucket + '-db'])
+ original_mpu = dl.capture_mpu(fleet, 'failure')
+ for node in nodes:
+ fleet.record('DL-original-unresolved-service-logs', {'node': node, **fleet.remote(node, 'logs')})
+ for node in tuple(fleet.network_gates):
+ proof = fleet.record('DL-failure-owned-network-release', {'node': node,
+ **fleet.remote(node, 'network-release', expired=True)})
+ require(proof.get('state') == 'CLEAR' and proof.get('rulesRemaining') == 0,
+ 'DL failure retained an owned network rule')
+ fleet.network_gates.remove(node)
+ for node in tuple(fleet.quorum_gates):
+ proof = fleet.record('DL-failure-owned-W-release', {'node': node,
+ **fleet.remote(node, 'quorum-gate', mode='clear')})
+ require(proof.get('state') == 'CLEAR', 'DL failure retained its private W gate')
+ fleet.quorum_gates.remove(node)
+ failed_scene = fleet.snapshot('DL-original-unresolved-business-scene', quiescent=True, buckets=[bucket, bucket + '-db'])
+ failed_mpu = dl.capture_mpu(fleet, 'failure')
+ require(physical_content(original_failure) == physical_content(failed_scene)
+ and all(original_mpu['nodes'][node]['sceneSha256'] == failed_mpu['nodes'][node]['sceneSha256']
+ for node in nodes), 'DL gate recovery changed the original failed business or MPU bytes')
+ retained_plan = prepare_retention(fleet, binding, dl.keys, failed_scene, failed_mpu['nodes'])
+ result['retention'] = isolate_retention(fleet, retained_plan)
+ except Exception as exc:
+ result['retentionError'] = {'type': type(exc).__name__, 'message': str(exc)}
+ clean = False
+ else:
+ try:
+ if fleet.network_gates or fleet.quorum_gates:
+ fleet.stop()
+ release_network(fleet)
+ for node in tuple(fleet.quorum_gates):
+ fleet.record('failed-branch-W-release', fleet.remote(node, 'quorum-gate', mode='clear'))
+ fleet.quorum_gates.remove(node)
+ fleet.start()
+ pending = [row for row in rows if 'resolution' not in row]
+ if pending:
+ fleet.stop()
+ fleet.start()
+ resolve_writes(fleet, binding, bucket, pending)
+ except Exception as exc:
+ dirty = True
+ result['recoveryError'] = str(exc)
+ if dirty:
+ fleet.stop()
+ fleet.record('retain-unresolved-network-or-write', {'state': 'DIRTY', 'namespacesRetained': True, 'writes': rows})
+ clean = False
+ else:
+ clean = fleet.cleanup()
+ result.update(cleanup='PASS' if clean else 'FAIL', writes=rows)
+ fleet.record('branch-result', result)
+ return result
+
+
+def main():
+ def interrupted(signum, frame):
+ raise KeyboardInterrupt(f'I03 interrupted by signal {signum}')
+ signal.signal(signal.SIGTERM, interrupted)
+ parser = argparse.ArgumentParser(description=__doc__)
+ for name in ('candidate', 'rc-bin', 'evidence', 'tool-directory'):
+ parser.add_argument('--' + name, type=Path, required=True)
+ parser.add_argument('--case-id', choices=[CASE_ID], required=True)
+ parser.add_argument('--testing-commit', required=True)
+ parser.add_argument('--binary-sha', required=True)
+ args = parser.parse_args()
+ require(re.fullmatch(r'[0-9a-f]{64}', args.binary_sha) and re.fullmatch(r'[1-9][0-9]*', os.environ.get('GITHUB_RUN_ID', '')),
+ 'immutable workflow candidate identity required')
+ require(select(CASE_ID)[0]['variants'] == list(BRANCHES), 'I03 source branch omitted')
+ nodes = os.environ.get('RUSTFS_NODES', 'vm000 vm001 vm002 vm003').split()
+ require(len(nodes) >= 4 and len(nodes) == len(set(nodes)) and all(re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9_.-]*', node) for node in nodes),
+ 'four distinct current Heal nodes required')
+ results = []
+ try:
+ for branch in BRANCHES:
+ results.append(run_branch(args, nodes[:4], branch))
+ if results[-1]['cleanup'] != 'PASS':
+ break
+ finally:
+ complete = len(results) == len(BRANCHES) and all(row['verdict'] == row['cleanup'] == 'PASS' for row in results)
+ save(args.evidence / 'healthy-summary.json', {'caseId': CASE_ID, 'results': results, 'complete': complete,
+ 'cleanup': 'PASS' if results and all(row['cleanup'] == 'PASS' for row in results) else 'FAIL'})
+ require(complete, 'I03 requires all five real partition/quorum/recovery branches and complete ACK resolution')
+
+
+if __name__ == '__main__':
+ try:
+ main()
+ except Exception as exc:
+ print(f'I03 network validation failed: {exc}', file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_heal/network_delete_records.py b/scripts/validation_heal/network_delete_records.py
new file mode 100644
index 00000000..083a5b0e
--- /dev/null
+++ b/scripts/validation_heal/network_delete_records.py
@@ -0,0 +1,113 @@
+"""Read only the original stopped DL controls' bounded delete-prepared records."""
+
+import hashlib
+import json
+import os
+import re
+import stat
+import uuid
+
+
+FIELDS = {'protocol_version', 'volume', 'object', 'transaction', 'bucket_incarnation', 'original_digest',
+ 'target_digest', 'data_dirs', 'deleted_versions', 'requests', 'original_versions', 'predecessors'}
+
+
+def require(value, message):
+ if not value: raise ValueError(message)
+
+
+def stable(info):
+ return (info.st_dev, info.st_ino, info.st_mode, info.st_nlink, info.st_size, info.st_mtime_ns, info.st_ctime_ns)
+
+
+def canonical_uuid(value):
+ require(isinstance(value, str) and str(uuid.UUID(value)) == value and uuid.UUID(value).int != 0,
+ 'DL delete record has a missing/noncanonical generation or transaction')
+ return value
+
+
+def checksum(value):
+ require(isinstance(value, list) and len(value) == 32 and all(type(x) is int and 0 <= x <= 255 for x in value),
+ 'DL delete record digest is not an exact SHA-256 byte array')
+ return bytes(value).hex()
+
+
+def unique_fields(pairs):
+ result = {}
+ for key, value in pairs:
+ require(key not in result, 'DL prepared delete contains duplicate JSON fields')
+ result[key] = value
+ return result
+
+
+def run(request, node):
+ require(set(request) == {'runId', 'action', 'bucket', 'keys', 'expectedBinarySha256'}
+ and request['action'] == 'network-delete-records', 'DL delete census accepts only its exact owned controls')
+ state, roots, owner = node['owned'](request['runId'])
+ require(owner.get('runtimeProfile') == 'lock-quorum' and owner.get('bucket') == request['bucket']
+ and owner.get('binarySha256') == request['expectedBinarySha256'] and len(roots) == 4
+ and node['digest'](node['checked'](state / 'rustfs', state)) == owner['binarySha256'],
+ 'DL delete census candidate/runtime/bucket differs')
+ node['stopped'](owner)
+ keys = request['keys']
+ require(isinstance(keys, list) and len(keys) == len(set(keys)) == 2 and keys == sorted(keys)
+ and keys[0] == 'dl/node1/control.bin' and keys[1] in {'dl/node3/control.bin', 'dl/node4/control.bin'},
+ 'DL delete census escaped its original two control keys')
+ records = []
+ for disk, root in enumerate(roots):
+ data = node['checked'](root / 'data', root)
+ for key in keys:
+ folder = node['checked'](data / request['bucket'] / key, data)
+ if not folder.exists(): continue
+ children = sorted(folder.iterdir())
+ require(len(children) <= 64, 'DL control directory census exceeds its source fixture bound')
+ for child in children:
+ child = node['checked'](child, data)
+ if not stat.S_ISDIR(child.lstat().st_mode): continue
+ # Data directories without a prepared delete are not read.
+ path = node['checked'](child / 'delete-prepared', data)
+ if not path.exists(): continue
+ canonical_uuid(child.name)
+ before = path.lstat()
+ require(stat.S_ISREG(before.st_mode) and before.st_nlink == 1 and before.st_size <= 1024**2
+ and len(records) < 32, 'DL prepared delete has an unsafe file type or excessive size/count')
+ fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
+ try:
+ require(stable(os.fstat(fd)) == stable(before), 'DL prepared delete changed before reading')
+ raw = bytearray()
+ while chunk := os.read(fd, min(65536, before.st_size + 1 - len(raw))):
+ raw.extend(chunk)
+ require(len(raw) <= before.st_size, 'DL prepared delete grew while reading')
+ require(len(raw) == before.st_size and stable(os.fstat(fd)) == stable(before),
+ 'DL prepared delete changed while reading')
+ finally:
+ os.close(fd)
+ require(stable(path.lstat()) == stable(before), 'DL prepared delete changed after reading')
+ value = json.loads(raw, object_pairs_hook=unique_fields)
+ require(isinstance(value, dict) and set(value) == FIELDS and type(value['protocol_version']) is int
+ and value['protocol_version'] == 1 and value['volume'] == request['bucket']
+ and value['object'] == key and value['transaction'] == child.name
+ and isinstance(value['requests'], list) and len(value['requests']) == 1
+ and len(value['original_versions']) == 3 and value['predecessors'] == [],
+ 'DL prepared delete escaped the reviewed one-request three-version protocol')
+ request_row = value['requests'][0]
+ require(request_row.get('name') == key and request_row.get('deleted') is False
+ and request_row.get('mark_deleted') is False
+ and len(value['data_dirs']) == len(value['deleted_versions']) == 1
+ and all(row.get('deleted') is False for row in value['original_versions']),
+ 'DL prepared delete is not an exact ordinary historical-version delete')
+ records.append({'disk': disk, 'key': key, 'path': path.relative_to(data).as_posix(),
+ 'size': len(raw), 'sha256': hashlib.sha256(raw).hexdigest(), 'protocolVersion': 1,
+ 'transaction': child.name, 'bucketIncarnation': canonical_uuid(value['bucket_incarnation']),
+ 'originalMetadataSha256': checksum(value['original_digest']),
+ 'targetMetadataSha256': checksum(value['target_digest']),
+ 'dataDirs': [canonical_uuid(x) for x in value['data_dirs']],
+ 'deletedVersions': [canonical_uuid(x) for x in value['deleted_versions']],
+ 'requestVersion': canonical_uuid(request_row.get('version_id')),
+ 'originalVersions': [canonical_uuid(x.get('version_id')) for x in value['original_versions']],
+ 'originalDataDirs': [canonical_uuid(x.get('data_dir')) for x in value['original_versions']]})
+ again = node['owned'](request['runId']);node['stopped'](again[2])
+ require(again == (state, roots, owner), 'DL delete census original owner/roots changed')
+ return {'runId': request['runId'], 'bucket': request['bucket'], 'keys': keys,
+ 'binarySha256': owner['binarySha256'], 'quiescent': True, 'readOnly': True, 'records': records,
+ 'rawContentsExported': False, 'credentialContentsRead': False, 'commitOrRollbackClaimed': False}
diff --git a/scripts/validation_heal/network_fixtures.py b/scripts/validation_heal/network_fixtures.py
new file mode 100644
index 00000000..f1e9fa5b
--- /dev/null
+++ b/scripts/validation_heal/network_fixtures.py
@@ -0,0 +1,97 @@
+"""Source-bound I03 DB fixtures; only public S3 writes and decoded physical witnesses."""
+
+try:
+ from .boundaries import boundary_sizes, verify_size_contract, verify_boundary_ranges
+ from .heal_probe import require
+ from .heal_workload import seed, verify_seed
+ from .shards import map_target
+ from .versions import map_inline_target, verify_proof
+except ImportError:
+ from boundaries import boundary_sizes, verify_size_contract, verify_boundary_ranges
+ from heal_probe import require
+ from heal_workload import seed, verify_seed
+ from shards import map_target
+ from versions import map_inline_target, verify_proof
+
+
+DB_COUNT = 128
+MPU_MINIMUM = 5 * 1024**2
+# GLOBAL_MIN_PART_SIZE and is_min_allowed_part_size in these exact source files.
+MPU_SOURCE = {
+ '072b57b37559fbe626aeda8abf61617fdcbdd926': 'e60ff0ab886a5578512a123eb1757352ae3e5204d7b69b5aaf14e9e530ec2d31',
+ '1c7fc818dd858247c45b39542abd8291b1e6e31b': '6975b05976e88667065dd4fd2d7649ff61ce0887504e2790bc68fc73c9ba048a',
+ '85c706cac09d53d0649487b8f1865f18beb53225': '6975b05976e88667065dd4fd2d7649ff61ce0887504e2790bc68fc73c9ba048a',
+ '62ff96774fa4139d45f14d70b70519aae8070ac0': '6975b05976e88667065dd4fd2d7649ff61ce0887504e2790bc68fc73c9ba048a',
+}
+
+
+def db_parameters(manifest, candidate, calibration, processes):
+ contract = verify_size_contract(manifest, candidate)
+ require(candidate['source_sha'] in MPU_SOURCE
+ and contract['sourceFiles']['crates/ecstore/src/set_disk/mod.rs'] == MPU_SOURCE[candidate['source_sha']],
+ 'network DB multipart boundary is not bound to the exact reviewed candidate source')
+ parameters = boundary_sizes(contract, calibration, processes, standard_class='EC:8')
+ parameters['groups']['multipart-minimum-minus-exact-plus'] = [MPU_MINIMUM - 1, MPU_MINIMUM, MPU_MINIMUM + 1]
+ sizes = [size for group in parameters['groups'].values() for size in group]
+ require(len(sizes) <= DB_COUNT and max(sizes) <= 64 * 1024**2, 'DB boundary plan exceeds its fixed count or body cap')
+ return {**parameters, 'objectCount': DB_COUNT, 'sizeCycle': sizes,
+ 'payloadBytes': sum(sizes[index % len(sizes)] for index in range(DB_COUNT)),
+ 'multipartMinimumPartBytes': MPU_MINIMUM, 'multipartBoundarySourceSha256': MPU_SOURCE[candidate['source_sha']],
+ 'multipartBoundaryIsObjectSizeControl': True,
+ 'completedMultipartProtocolClaimed': False}
+
+
+def seed_db(recorder, bucket, parameters):
+ require(parameters['objectCount'] == DB_COUNT
+ and parameters['payloadBytes'] <= recorder.probe.binding['maxInventoryBytes']
+ and DB_COUNT <= recorder.probe.binding['maxInventoryObjects'],
+ 'network DB plan exceeds the frozen inventory budget')
+ return seed(recorder, bucket, 'DB', DB_COUNT, parameters['sizeCycle'])
+
+
+def map_db(snapshot, bucket, expected, parameters):
+ require(len(expected) == DB_COUNT and len({entry['key'] for entry in expected}) == DB_COUNT,
+ 'DB boundary fixture omits or duplicates a control')
+ representatives, witnessed, selected_sizes = [], [], set()
+ for index, entry in enumerate(expected):
+ size = parameters['sizeCycle'][index % len(parameters['sizeCycle'])]
+ require(entry['key'] == f'objects/{index:04d}.bin' and entry['versionId'] == 'null'
+ and entry['isLatest'] is True and entry['deleteMarker'] is False and entry['payload']['size'] == size,
+ 'DB control identity or ACK size differs from its frozen cycle')
+ shape = 'zero-metadata' if size == 0 else 'inline' if size <= parameters['actualLogicalInlineLimit'] else 'external'
+ target = (map_target(snapshot, bucket, entry['key'], 'data', 1) if shape == 'external'
+ else map_inline_target(snapshot, bucket, entry, allow_empty=size == 0))
+ require(len(target['cohort']) == 16 and target['logical']['size'] == size
+ and target['logical']['erasure']['data_blocks'] == target['logical']['erasure']['parity_blocks'] == 8,
+ 'DB boundary lacks its actual complete sixteen-member size/shape authority')
+ witnessed.append({'key': entry['key'], 'size': size, 'shape': shape,
+ 'versionId': entry['versionId'], 'members': len(target['cohort']),
+ 'dataBlocks': target['logical']['erasure']['data_blocks'],
+ 'parityBlocks': target['logical']['erasure']['parity_blocks']})
+ if size not in selected_sizes:
+ selected_sizes.add(size)
+ representatives.append({'expected': entry, 'target': target, 'shape': shape})
+ require(selected_sizes == set(parameters['sizeCycle']), 'physical DB representatives omit a required boundary')
+ return representatives, witnessed
+
+
+def prove_db(fleet, representatives):
+ return [{'key': row['expected']['key'], 'shape': row['shape'],
+ 'proof': verify_proof(fleet, row['target'], 'DB-boundary-physical-proof')}
+ for row in representatives]
+
+
+def verify_db(probe, fleet, bucket, expected, baseline, representatives, parameters):
+ try:
+ from .heal_probe import Probe, compare_inventories
+ except ImportError:
+ from heal_probe import Probe, compare_inventories
+ peers = []
+ for endpoint in probe.binding['endpoints']:
+ peer = Probe(probe.binding, endpoint)
+ inventory = fleet.record('all-peer-DB-final-S3', {'endpoint': endpoint,
+ **peer.inventory(bucket, unversioned_current_tags=True)})
+ peers.append({'endpoint': endpoint, **verify_seed(expected, inventory), **compare_inventories(baseline, inventory)})
+ ranges = verify_boundary_ranges(probe, fleet, bucket,
+ [row['expected'] for row in representatives], parameters['actualECBlockSize'])
+ return {'allDefault128ControlsPreserved': True, 'peers': peers, 'boundaryRanges': ranges}
diff --git a/scripts/validation_heal/network_gate.py b/scripts/validation_heal/network_gate.py
new file mode 100644
index 00000000..26e11be3
--- /dev/null
+++ b/scripts/validation_heal/network_gate.py
@@ -0,0 +1,327 @@
+"""I03: one owned nftables table, with a pre-armed independent expiry timer."""
+
+from datetime import datetime, timedelta, timezone
+import hashlib
+import ipaddress
+import json
+import os
+from pathlib import Path
+import re
+import socket
+import stat
+import subprocess
+import sys
+
+
+MARKER = 'network-gate.json'
+SCRIPT = 'network-gate-expiry.py'
+EXPIRY = 'network-gate-expired.json'
+TTL_SECONDS = 1200
+PORT = 9000
+
+
+def require(value, message):
+ if not value:
+ raise ValueError(message)
+
+
+def command(*args, input=None):
+ result = subprocess.run(args, input=input, capture_output=True, text=True, timeout=30)
+ require(result.returncode == 0, f'{args[0]} network operation failed: {result.stderr[-600:]}')
+ return result.stdout
+
+
+def private_file(path, limit=128 * 1024):
+ info = path.lstat()
+ require(not path.is_symlink() and stat.S_ISREG(info.st_mode) and info.st_uid == 0
+ and info.st_nlink == 1 and stat.S_IMODE(info.st_mode) == 0o600 and info.st_size <= limit,
+ 'network artifact is not a bounded private root-owned regular file')
+ with os.fdopen(os.open(path, os.O_RDONLY | os.O_NOFOLLOW), 'rb') as stream:
+ before = os.fstat(stream.fileno())
+ raw = stream.read(limit + 1)
+ after = os.fstat(stream.fileno())
+ require(len(raw) == before.st_size <= limit and all(getattr(before, name) == getattr(after, name)
+ for name in ('st_ino', 'st_dev', 'st_size', 'st_mtime_ns', 'st_ctime_ns', 'st_nlink')),
+ 'network artifact changed while reading')
+ return raw
+
+
+def write(path, value, *, replace=False):
+ target = path.with_suffix('.pending') if replace else path
+ fd = os.open(target, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
+ with os.fdopen(fd, 'wb') as stream:
+ stream.write(value if isinstance(value, bytes) else json.dumps(value).encode())
+ stream.flush()
+ os.fsync(stream.fileno())
+ if replace:
+ os.replace(target, path)
+ descriptor = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
+ try:
+ os.fsync(descriptor)
+ finally:
+ os.close(descriptor)
+
+
+def identities(owner):
+ require(re.fullmatch(r'gh-[1-9][0-9]*-[1-9][0-9]*-h2', owner.get('runId', ''))
+ and owner.get('runtimeProfile') == 'lock-quorum', 'network fault requires its exact H2 EC:8 service')
+ suffix = hashlib.sha256((owner['runId'] + '/' + owner['hostname']).encode()).hexdigest()[:24]
+ return 'rv_i03_' + suffix, 'rv-i03-expiry-' + suffix
+
+
+def addresses(values):
+ require(isinstance(values, list) and len(values) == len(set(values)) == 4,
+ 'network topology requires four distinct IPv4 addresses')
+ for value in values:
+ address = ipaddress.ip_address(value)
+ require(address.version == 4 and str(address) == value and address.is_private
+ and not (address.is_loopback or address.is_unspecified or address.is_link_local or address.is_multicast),
+ 'network target must be a canonical private unicast fleet address')
+ return values
+
+
+def resolve():
+ values = []
+ for index in range(1, 5):
+ rows = socket.getaddrinfo(f'rustfs-node{index}', PORT, socket.AF_UNSPEC, socket.SOCK_STREAM)
+ require(rows and all(row[0] == socket.AF_INET for row in rows),
+ 'peer alias exposes an unguarded IPv6 path; no partition will be installed')
+ found = {row[4][0] for row in rows}
+ require(len(found) == 1, 'peer alias has ambiguous IPv4 resolution')
+ values.append(found.pop())
+ return addresses(values)
+
+
+def spec(table, blocked):
+ result = [{'add': {'table': {'family': 'inet', 'name': table}}}]
+ for chain in ('input', 'output'):
+ result.append({'add': {'chain': {'family': 'inet', 'table': table, 'name': chain,
+ 'type': 'filter', 'hook': chain, 'prio': -10, 'policy': 'accept'}}})
+ for peer in blocked:
+ for field in ('sport', 'dport'):
+ result.append({'add': {'rule': {'family': 'inet', 'table': table, 'chain': chain,
+ 'comment': f'{table}:{chain}:{peer}:{field}', 'expr': [
+ # IP/TCP payload selectors imply their protocol guards.
+ # nft removes redundant explicit meta guards on export.
+ {'match': {'op': '==', 'left': {'payload': {'protocol': 'ip',
+ 'field': 'saddr' if chain == 'input' else 'daddr'}}, 'right': peer}},
+ {'match': {'op': '==', 'left': {'payload': {'protocol': 'tcp', 'field': field}}, 'right': PORT}},
+ {'counter': None}, {'drop': None}]}}})
+ return {'nftables': result}
+
+
+def table_present(table):
+ values = json.loads(command('nft', '-j', 'list', 'tables'))['nftables']
+ return any(row.get('table', {}).get('family') == 'inet' and row['table'].get('name') == table for row in values if 'table' in row)
+
+
+def validate_table(value, table, blocked):
+ expected = spec(table, blocked)['nftables']
+ chains = {row['add']['chain']['name']: row['add']['chain'] for row in expected if 'chain' in row['add']}
+ wanted = {row['add']['rule']['comment']: row['add']['rule'] for row in expected if 'rule' in row['add']}
+ seen_chains, seen_rules, tables, counters = set(), set(), 0, []
+ for row in value.get('nftables', []):
+ if 'metainfo' in row:
+ continue
+ require(len(row) == 1 and next(iter(row)) in {'table', 'chain', 'rule'}, 'foreign network table object')
+ kind, item = next(iter(row.items()))
+ require(item.get('family') == 'inet' and (item.get('name') if kind == 'table' else item.get('table')) == table,
+ 'network table identity changed')
+ if kind == 'table':
+ require(set(item) <= {'family', 'name', 'handle'} and tables == 0, 'network table flags or ownership changed')
+ tables += 1
+ elif kind == 'chain':
+ name = item.get('name')
+ require(name in chains and name not in seen_chains and {k: v for k, v in item.items() if k != 'handle'} == chains[name],
+ 'network chain has an unexpected hook, priority or policy')
+ seen_chains.add(name)
+ else:
+ name = item.get('comment')
+ require(name in wanted and name not in seen_rules, 'foreign or duplicate network rule')
+ normalized = dict(item)
+ normalized.pop('handle', None)
+ normalized.pop('index', None)
+ expressions = []
+ for expression in normalized.get('expr', []):
+ if 'counter' in expression:
+ count = expression['counter']
+ require(set(expression) == {'counter'} and isinstance(count, dict) and set(count) == {'packets', 'bytes'}
+ and all(type(count[k]) is int and count[k] >= 0 for k in count), 'invalid network packet counter')
+ counters.append({'rule': name, **count})
+ expressions.append({'counter': None})
+ else:
+ expressions.append(expression)
+ normalized['expr'] = expressions
+ require(normalized == wanted[name], 'network rule changed its addresses, TCP port or drop expression: '
+ + json.dumps({'observed': normalized, 'expected': wanted[name]}, sort_keys=True))
+ seen_rules.add(name)
+ require(tables == 1 and seen_chains == set(chains) and seen_rules == set(wanted)
+ and len(counters) == len(wanted), 'network table omits a registered rule or chain')
+ return counters
+
+
+def read_marker(state, owner):
+ if not (state / MARKER).exists():
+ return None
+ value = json.loads(private_file(state / MARKER))
+ table, timer = identities(owner)
+ values = addresses(value.get('addresses'))
+ local, blocked = value.get('localIndex'), value.get('blockedIndices')
+ require(type(local) is int and 0 <= local < 4 and isinstance(blocked, list) and blocked == sorted(set(blocked))
+ and 0 < len(blocked) <= 3 and all(type(i) is int and 0 <= i < 4 and i != local for i in blocked)
+ and value.get('runId') == owner['runId'] and value.get('hostname') == owner['hostname']
+ and value.get('binarySha256') == owner['binarySha256'] and value.get('unit') == owner['unit']
+ and value.get('schemaVersion') == 1 and value.get('table') == table and value.get('timer') == timer
+ and value.get('phase') in {'armed', 'active'} and value.get('ttlSeconds') == TTL_SECONDS
+ and value.get('spec') == spec(table, [values[i] for i in blocked])
+ and value.get('scriptSha256') == hashlib.sha256(private_file(state / SCRIPT)).hexdigest(),
+ 'network ownership, exact rules or expiry script changed; retain namespace')
+ require(datetime.fromisoformat(value['expiresAt']).tzinfo is not None, 'network timer expiry has no timezone')
+ return value
+
+
+def prepare(state, owner, request, source):
+ table, timer = identities(owner)
+ values = addresses(request.get('addresses'))
+ local, blocked = request.get('localIndex'), request.get('blockedIndices')
+ require(values == resolve() and type(local) is int and 0 <= local < 4
+ and isinstance(blocked, list) and blocked == sorted(set(blocked)) and 0 < len(blocked) <= 3
+ and all(type(i) is int and 0 <= i < 4 and i != local for i in blocked), 'network peer binding differs')
+ # Binding a socket to this address is an independent ownership check.
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as test:
+ test.bind((values[local], 0))
+ require(not (state / MARKER).exists() and not (state / SCRIPT).exists() and not (state / EXPIRY).exists()
+ and not table_present(table), 'network namespace or table is already occupied')
+ for suffix in ('.timer', '.service'):
+ require(command('systemctl', 'show', timer + suffix, '-p', 'LoadState', '--value').strip() == 'not-found',
+ 'network expiry unit is already occupied')
+ command('nft', '-j', '-c', '-f', '-', input=json.dumps(spec(table, [values[i] for i in blocked])))
+ write(state / SCRIPT, source.encode())
+ value = {'schemaVersion': 1, 'runId': owner['runId'], 'hostname': owner['hostname'],
+ 'unit': owner['unit'], 'binarySha256': owner['binarySha256'], 'table': table, 'timer': timer,
+ 'addresses': values, 'localIndex': local, 'blockedIndices': blocked, 'ttlSeconds': TTL_SECONDS,
+ 'expiresAt': (datetime.now(timezone.utc) + timedelta(seconds=TTL_SECONDS)).isoformat(), 'phase': 'armed',
+ 'scriptSha256': hashlib.sha256(source.encode()).hexdigest(), 'spec': spec(table, [values[i] for i in blocked])}
+ write(state / MARKER, value)
+ command('systemd-run', '--quiet', '--unit=' + timer, '--on-active=' + str(TTL_SECONDS) + 's',
+ '--timer-property=AccuracySec=1s', '--property=RemainAfterExit=yes', '/usr/bin/python3', str(state / SCRIPT), str(state))
+ require(command('systemctl', 'show', timer + '.timer', '-p', 'ActiveState', '--value').strip() == 'active',
+ 'network expiry timer was not armed before mutation')
+ return {**value, 'state': 'ARMED', 'rulesApplied': False, 'expiryArmedBeforeMutation': True}
+
+
+def activate(state, owner):
+ value = read_marker(state, owner)
+ require(value and value['phase'] == 'armed' and not table_present(value['table'])
+ and not (state / EXPIRY).exists()
+ and datetime.now(timezone.utc) < datetime.fromisoformat(value['expiresAt'])
+ and command('systemctl', 'show', value['timer'] + '.timer', '-p', 'ActiveState', '--value').strip() == 'active',
+ 'network activation requires its still-armed independent timer')
+ # nft's whole transaction creates only this table; it never flushes rules.
+ command('nft', '-j', '-f', '-', input=json.dumps(value['spec']))
+ counters = validate_table(json.loads(command('nft', '-j', 'list', 'table', 'inet', value['table'])),
+ value['table'], [value['addresses'][i] for i in value['blockedIndices']])
+ value['phase'] = 'active'
+ write(state / MARKER, value, replace=True)
+ return {'state': 'ACTIVE', 'table': value['table'], 'counters': counters,
+ 'onlyExactFleetTCP9000': True, 'unrelatedTablesChanged': False}
+
+
+def observe(state, owner):
+ value = read_marker(state, owner)
+ require(value and value['phase'] == 'active' and not (state / EXPIRY).exists()
+ and datetime.now(timezone.utc) < datetime.fromisoformat(value['expiresAt'])
+ and command('systemctl', 'show', value['timer'] + '.timer', '-p', 'ActiveState', '--value').strip() == 'active',
+ 'network partition expired or lost its independent recovery timer')
+ counters = validate_table(json.loads(command('nft', '-j', 'list', 'table', 'inet', value['table'])),
+ value['table'], [value['addresses'][i] for i in value['blockedIndices']])
+ probes = []
+ for index, address in enumerate(value['addresses']):
+ if index == value['localIndex']:
+ continue
+ row = {'peerIndex': index, 'peerAddress': address}
+ for port, label in ((PORT, 'service'), (22, 'management')):
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client:
+ client.settimeout(1)
+ client.bind((value['addresses'][value['localIndex']], 0))
+ try:
+ client.connect((address, port))
+ except OSError as exc:
+ row[label] = {'connected': False, 'error': type(exc).__name__, 'errno': exc.errno}
+ else:
+ row[label] = {'connected': True}
+ require(row['service']['connected'] is (index not in value['blockedIndices'])
+ and row['management']['connected'] is True,
+ 'actual service isolation or preserved management connectivity differs')
+ probes.append(row)
+ after = validate_table(json.loads(command('nft', '-j', 'list', 'table', 'inet', value['table'])),
+ value['table'], [value['addresses'][i] for i in value['blockedIndices']])
+ require(sum(row['packets'] for row in after) > sum(row['packets'] for row in counters),
+ 'network isolation did not hit a real owned packet rule')
+ return {'state': 'ACTIVE', 'localIndex': value['localIndex'], 'table': value['table'],
+ 'probes': probes, 'countersBefore': counters, 'countersAfter': after, 'managementPort': 22,
+ 'allServicesRemainRunning': True, 'expiryArmed': True}
+
+
+def release(state, owner, *, expired=False):
+ value = read_marker(state, owner)
+ if value is None:
+ table, _ = identities(owner)
+ require(not table_present(table), 'network table has no ownership intent; preserve namespace')
+ return {'state': 'CLEAR', 'gateWasAbsent': True}
+ expired = expired or (state / EXPIRY).exists()
+ # Stop the expiry worker before inspecting/deleting, avoiding a concurrent
+ # deletion. An armed timer need not have loaded its service yet.
+ for suffix in ('.timer', '.service'):
+ unit_name = value['timer'] + suffix
+ load_state = command('systemctl', 'show', unit_name, '-p', 'LoadState', '--value').strip()
+ require(load_state in {'not-found', 'loaded'}, 'network expiry unit identity is unknown')
+ require(expired or suffix != '.timer' or load_state == 'loaded',
+ 'network expiry timer disappeared before release')
+ active_state = command('systemctl', 'show', unit_name, '-p', 'ActiveState', '--value').strip()
+ if load_state == 'loaded' and active_state not in {'inactive', 'failed', 'unknown'}:
+ command('systemctl', 'stop', unit_name)
+ allowed = {'inactive', 'failed', 'unknown'} if expired or load_state == 'not-found' else {'inactive', 'failed'}
+ require(command('systemctl', 'show', unit_name, '-p', 'ActiveState', '--value').strip() in allowed,
+ 'network expiry unit remains active')
+ counters = []
+ if table_present(value['table']):
+ counters = validate_table(json.loads(command('nft', '-j', 'list', 'table', 'inet', value['table'])),
+ value['table'], [value['addresses'][i] for i in value['blockedIndices']])
+ command('nft', '-j', '-f', '-', input=json.dumps({'nftables': [{'delete': {'table': {
+ 'family': 'inet', 'name': value['table']}}}]}))
+ require(not table_present(value['table']), 'owned network table was not removed')
+ receipt = {'state': 'CLEAR', 'table': value['table'], 'rulesRemaining': 0,
+ 'removedCounters': counters, 'expiryTriggered': expired or (state / EXPIRY).exists(),
+ 'unrelatedTablesChanged': False}
+ if expired:
+ if not (state / EXPIRY).exists():
+ write(state / EXPIRY, receipt)
+ (state / MARKER).unlink()
+ (state / SCRIPT).unlink()
+ else:
+ (state / MARKER).unlink()
+ (state / SCRIPT).unlink()
+ return receipt
+
+
+def expiry_main():
+ require(os.geteuid() == 0 and len(sys.argv) == 2, 'root-owned expiry invocation required')
+ state = Path(sys.argv[1])
+ require(state.is_absolute() and state.parent == Path('/var/lib/rustfs-auto-testing/heal')
+ and re.fullmatch(r'gh-[1-9][0-9]*-[1-9][0-9]*-h2', state.name), 'expiry namespace is not a registered H2 run')
+ for entry in (state, *state.parents):
+ require(not entry.is_symlink() and stat.S_ISDIR(entry.stat().st_mode) and entry.stat().st_uid == 0,
+ 'expiry namespace ownership changed')
+ # owner.json is created by the existing node reserve path (mode 0644).
+ path = state / 'owner.json'
+ info = path.lstat()
+ require(stat.S_ISREG(info.st_mode) and info.st_uid == 0 and info.st_nlink == 1 and info.st_size < 65536
+ and info.st_mode & 0o022 == 0 and not path.is_symlink(), 'expiry owner changed')
+ owner = json.loads(path.read_text())
+ require(owner['runId'] == state.name and owner['hostname'] == socket.gethostname(), 'expiry owner differs from actual host')
+ print(json.dumps(release(state, owner, expired=True)))
+
+
+if __name__ == '__main__':
+ expiry_main()
diff --git a/scripts/validation_heal/network_load.py b/scripts/validation_heal/network_load.py
new file mode 100644
index 00000000..5ca3442c
--- /dev/null
+++ b/scripts/validation_heal/network_load.py
@@ -0,0 +1,410 @@
+"""I03 DL request-bound fixtures and bounded concurrent calls; no repair verdict."""
+
+from concurrent.futures import ThreadPoolExecutor
+import hashlib
+import json
+from pathlib import Path
+import threading
+import time
+
+try:
+ from .heal_probe import payload, require, validate_s3_request
+ from .heal_workload import Recorder
+ from .network_fixtures import MPU_MINIMUM, MPU_SOURCE
+except ImportError:
+ from heal_probe import payload, require, validate_s3_request
+ from heal_workload import Recorder
+ from network_fixtures import MPU_MINIMUM, MPU_SOURCE
+
+
+BODY_SIZE = 512 * 1024 + 97
+FIRST_PART_SIZE = MPU_MINIMUM + 17
+FINAL_PART_SIZE = 211
+ADMISSION_SECONDS = 180
+SOURCE_FILES = {
+ '072b57b37559fbe626aeda8abf61617fdcbdd926': {
+ 'rustfs/src/error.rs': '656d38972c06339015c174405571a94f19d8dd64a680e1a7ca631f80205f99a7',
+ 'crates/ecstore/src/set_disk/ops/object.rs': 'f0f407509b5a12803a191cc1c3a9eb079a15a80a4bbcacca70141150364628ca'},
+ '1c7fc818dd858247c45b39542abd8291b1e6e31b': {
+ 'rustfs/src/error.rs': 'e7cff0e28978b49d3ee0e8c24da1fa439b3118a7d3f7304edf00dc0535691598',
+ 'crates/ecstore/src/set_disk/ops/object.rs': 'fdcfda1a8811a93537f9cd47cadac7380bee66b3d14242ca91500bbf38e6bedb'},
+}
+MULTIPART_SOURCE = '1ac5c17a94845fb16f6d75b8d3692cb18f81556086d4cc4a96b63deae106ab8d'
+MPU_LAYOUT_SOURCE = '479a3b3f8c9f7b43b25b3f41d1b6769a67c6dc58c15845331da87e1fbc762679'
+DISK_SOURCE = {
+ '072b57b37559fbe626aeda8abf61617fdcbdd926': '7ab3137947d0a4f9007a40abc3e1735ec83ca820f6dc03a3a9426ae1b4f096d7',
+ '1c7fc818dd858247c45b39542abd8291b1e6e31b': 'cd045a322ad7e41e2253ac08554f10634192345cbd923f2854553a229c036f4b',
+}
+MUTATIONS = frozenset({'put_object', 'copy_object', 'delete_object', 'create_multipart_upload',
+ 'upload_part', 'complete_multipart_upload', 'abort_multipart_upload'})
+READS = frozenset({'get_object', 'head_object', 'list_objects_v2', 'list_object_versions',
+ 'list_multipart_uploads', 'list_parts'})
+
+
+SOURCE_FILES['85c706cac09d53d0649487b8f1865f18beb53225'] = {
+ **SOURCE_FILES['1c7fc818dd858247c45b39542abd8291b1e6e31b'],
+ 'rustfs/src/error.rs': 'c6a8d569b295659cf5a26a44bb3d8ba809c4ca3405d87525624699d161e08970',
+ 'rustfs/src/storage/ecfs.rs': '68d8ae2428504010adeb7bc51d2309496260898e4c74e5577a0af4f1bfe13c85'}
+DISK_SOURCE['85c706cac09d53d0649487b8f1865f18beb53225'] = DISK_SOURCE['1c7fc818dd858247c45b39542abd8291b1e6e31b']
+
+SOURCE_FILES['62ff96774fa4139d45f14d70b70519aae8070ac0'] = {
+ **SOURCE_FILES['85c706cac09d53d0649487b8f1865f18beb53225'],
+ 'rustfs/src/error.rs': '267cf292f0af47bca782ebb4de119a5bbe119af82111dac0bbf60f1311ef8383',
+ 'rustfs/src/storage/ecfs.rs': 'e0c4ce6569f75771c73de043f8416645bcd1de86a1192a79f45d7418953086f9'}
+DISK_SOURCE['62ff96774fa4139d45f14d70b70519aae8070ac0'] = DISK_SOURCE['85c706cac09d53d0649487b8f1865f18beb53225']
+
+
+def source_reference(candidate):
+ require(candidate in SOURCE_FILES, 'DL operations are not reviewed for this exact candidate')
+ return {'candidateCommit': candidate, 'sourceFiles': {**SOURCE_FILES[candidate],
+ 'crates/ecstore/src/set_disk/mod.rs': MPU_SOURCE[candidate],
+ 'crates/ecstore/src/set_disk/metadata.rs': MPU_LAYOUT_SOURCE,
+ 'crates/ecstore/src/disk/mod.rs': DISK_SOURCE[candidate],
+ 'crates/ecstore/src/set_disk/ops/multipart.rs': MULTIPART_SOURCE},
+ 'multipartLayout': {'systemBucket': '.rustfs.sys/multipart', 'keyRoot': 'sha256(bucket + "/" + object)',
+ 'uploadIdDecodedForScope': False},
+ 'negativeObjectMutation': {'httpStatus': 503, 'code': 'ServiceUnavailable'},
+ 'abortRefusalIsNotRollback': True, 'listDuringPartitionIsNotGlobalCompleteness': True,
+ 'maximumWorkers': 4, 'admissionSeconds': ADMISSION_SECONDS,
+ 'automaticMutationRetries': 0, 'serverCommitFenceClaimed': False}
+
+
+def digest(body):
+ return {'size': len(body), 'sha256': hashlib.sha256(body).hexdigest()}
+
+
+def binding_digest(binding):
+ return hashlib.sha256(json.dumps(binding, sort_keys=True, separators=(',', ':')).encode()).hexdigest()
+
+
+def control_body(bucket, key, nonce):
+ return payload(f'{bucket}/{key}/{nonce}', BODY_SIZE)
+
+
+def part_body(bucket, key, number, size):
+ return payload(f'{bucket}/{key}/part-{number}', size)
+
+
+def seed_load(recorder, bucket, endpoints):
+ reference = source_reference(recorder.probe.binding['candidateCommit'])
+ require(len(endpoints) == len(set(endpoints)) == 2
+ and set(endpoints) <= set(recorder.probe.binding['endpoints']), 'DL requires the two actual partition-side endpoints')
+ require(recorder.call('get_bucket_versioning', {'Bucket': bucket}).get('Status') == 'Enabled',
+ 'DL requires the original enabled versioned bucket')
+ expected, sides, seen_uploads = [], {}, set()
+ for endpoint in endpoints:
+ key = f'dl/{endpoint}/control.bin'
+ versions, heads = [], {}
+ for nonce in range(3):
+ body = control_body(bucket, key, nonce)
+ response = recorder.call('put_object', {'Bucket': bucket, 'Key': key,
+ 'Metadata': {'heal-fixture': 'DL', 'nonce': str(nonce)}, 'Tagging': 'suite=heal&fixture=DL',
+ 'ContentType': 'application/octet-stream'}, body, identity={'endpoint': endpoint, 'phase': 'seed', 'nonce': nonce})
+ version = response.get('VersionId')
+ require(isinstance(version, str) and version not in {'', 'null'}
+ and version not in {entry['versionId'] for entry in versions} and response.get('ETag'),
+ 'DL control lacks a distinct exact PUT version/ETag ACK')
+ entry = {'key': key, 'versionId': version, 'deleteMarker': False,
+ 'isLatest': nonce == 2, 'payload': digest(body)}
+ versions.append(entry)
+ expected.append(entry)
+ head = recorder.call('head_object', {'Bucket': bucket, 'Key': key, 'VersionId': version})
+ require(head.get('VersionId') == version and head.get('ContentLength') == len(body)
+ and head.get('ETag') == response['ETag'] and head.get('LastModified')
+ and head.get('Metadata') == {'heal-fixture': 'DL', 'nonce': str(nonce)},
+ 'DL original HEAD does not redeem its exact data-version ACK')
+ heads[version] = json.loads(json.dumps({name: head.get(name) for name in
+ ('ContentLength', 'ContentType', 'ETag', 'LastModified', 'Metadata')}, default=str))
+ uploads = {}
+ for name, sizes in [('complete', [FIRST_PART_SIZE, FINAL_PART_SIZE]),
+ ('upload', [FIRST_PART_SIZE]), ('abort', [FINAL_PART_SIZE])]:
+ upload_key = f'dl/{endpoint}/{name}.bin'
+ response = recorder.call('create_multipart_upload', {'Bucket': bucket, 'Key': upload_key,
+ 'Metadata': {'heal-fixture': 'DL', 'nonce': name}, 'ContentType': 'application/octet-stream'})
+ upload_id = response.get('UploadId')
+ require(isinstance(upload_id, str) and upload_id and upload_id not in seen_uploads,
+ 'DL MPU lacks a distinct exact CreateMultipartUpload ACK')
+ seen_uploads.add(upload_id)
+ parts, full_digest, total = [], hashlib.sha256(), 0
+ for number, size in enumerate(sizes, 1):
+ body = part_body(bucket, upload_key, number, size)
+ response = recorder.call('upload_part', {'Bucket': bucket, 'Key': upload_key,
+ 'UploadId': upload_id, 'PartNumber': number}, body)
+ require(isinstance(response.get('ETag'), str) and response['ETag'], 'DL part ACK omitted its ETag')
+ parts.append({'PartNumber': number, 'ETag': response['ETag'], 'payload': digest(body)})
+ full_digest.update(body)
+ total += size
+ uploads[name] = {'key': upload_key, 'uploadId': upload_id, 'parts': parts,
+ 'completePayload': {'size': total, 'sha256': full_digest.hexdigest()}}
+ sides[endpoint] = {'controlKey': key, 'versions': versions, 'heads': heads, 'uploads': uploads}
+ return {'bucket': bucket, 'runId': recorder.probe.binding['runId'], 'bindingSha256': binding_digest(recorder.probe.binding),
+ 'sourceContract': reference,
+ 'expected': expected, 'sides': sides,
+ 'seedPayloadBytes': recorder.bytes_written, 'canonicalCasePassClaimed': False}
+
+
+def mutation_steps(fixture, endpoint):
+ bucket, side = fixture['bucket'], fixture['sides'][endpoint]
+ key, versions, uploads = side['controlKey'], side['versions'], side['uploads']
+ steps = []
+ for nonce in (3, 4):
+ steps.append({'operation': 'put_object', 'params': {'Bucket': bucket, 'Key': key,
+ 'ContentType': 'application/octet-stream', 'Metadata': {'heal-fixture': 'DL', 'nonce': str(nonce)},
+ 'Tagging': 'suite=heal&fixture=DL'}, 'body': control_body(bucket, key, nonce), 'nonce': nonce})
+ steps.extend([
+ {'operation': 'copy_object', 'params': {'Bucket': bucket, 'Key': f'dl/{endpoint}/copy.bin',
+ 'CopySource': {'Bucket': bucket, 'Key': key, 'VersionId': versions[0]['versionId']},
+ 'MetadataDirective': 'REPLACE', 'Metadata': {'heal-fixture': 'DL', 'nonce': '5'},
+ 'TaggingDirective': 'REPLACE', 'Tagging': 'suite=heal&fixture=DL'}, 'nonce': 5},
+ {'operation': 'delete_object', 'params': {'Bucket': bucket, 'Key': key, 'VersionId': versions[0]['versionId']}, 'nonce': 6},
+ {'operation': 'create_multipart_upload', 'params': {'Bucket': bucket, 'Key': f'dl/{endpoint}/create.bin',
+ 'Metadata': {'heal-fixture': 'DL', 'nonce': '7'}}, 'nonce': 7},
+ {'operation': 'upload_part', 'params': {'Bucket': bucket, 'Key': uploads['upload']['key'],
+ 'UploadId': uploads['upload']['uploadId'], 'PartNumber': 2},
+ 'body': part_body(bucket, uploads['upload']['key'], 2, FINAL_PART_SIZE), 'nonce': 8},
+ {'operation': 'complete_multipart_upload', 'params': {'Bucket': bucket, 'Key': uploads['complete']['key'],
+ 'UploadId': uploads['complete']['uploadId'], 'MultipartUpload': {'Parts': [
+ {name: part[name] for name in ('PartNumber', 'ETag')} for part in uploads['complete']['parts']]}}, 'nonce': 9},
+ {'operation': 'abort_multipart_upload', 'params': {'Bucket': bucket, 'Key': uploads['abort']['key'],
+ 'UploadId': uploads['abort']['uploadId']}, 'nonce': 10},
+ ])
+ return steps
+
+
+def read_steps(fixture, endpoint):
+ bucket, side = fixture['bucket'], fixture['sides'][endpoint]
+ current = side['versions'][-1]
+ params = {'Bucket': bucket, 'Key': current['key'], 'VersionId': current['versionId']}
+ upload = side['uploads']['complete']
+ return [
+ {'operation': 'get_object', 'params': params},
+ {'operation': 'head_object', 'params': params},
+ {'operation': 'list_objects_v2', 'params': {'Bucket': bucket, 'Prefix': f'dl/{endpoint}/', 'MaxKeys': 32}},
+ {'operation': 'list_object_versions', 'params': {'Bucket': bucket, 'Prefix': side['controlKey'], 'MaxKeys': 32}},
+ {'operation': 'list_multipart_uploads', 'params': {'Bucket': bucket, 'Prefix': f'dl/{endpoint}/', 'MaxUploads': 32}},
+ {'operation': 'list_parts', 'params': {'Bucket': bucket, 'Key': upload['key'], 'UploadId': upload['uploadId'], 'MaxParts': 32}},
+ ]
+
+
+def read_receipts(directory, binding, endpoint):
+ """Reconstruct from original durable bytes, including requests with no ACK."""
+ require(endpoint in binding['endpoints'], 'unknown DL endpoint ownership')
+ directory = Path(directory)
+ requests, acks = sorted(directory.glob('*-request.json')), sorted(directory.glob('*-ack.json'))
+ require({p.name for p in acks} <= {p.name.replace('-request.json', '-ack.json') for p in requests},
+ 'DL has a detached ACK without its exact request')
+ rows = []
+ for index, path in enumerate(requests, 1):
+ request = json.loads(path.read_text())
+ require(path.name == f'{index:06d}-request.json' and type(request['sequence']) is int and request['sequence'] == index
+ and request.get('schemaVersion') == 2
+ and request['runId'] == binding['runId'], 'DL request sequence/run ownership differs')
+ validate_s3_request(binding, request['operation'], request['params'])
+ row = {'request': request, 'requestFile': path.name, 'requestSha256': hashlib.sha256(path.read_bytes()).hexdigest(),
+ 'prepared': True, 'ackPresent': False, 'resolution': 'UNKNOWN_PREPARED_REQUEST'}
+ ack_path = path.with_name(path.name.replace('-request.json', '-ack.json'))
+ if ack_path.exists():
+ ack_bytes = ack_path.read_bytes()
+ ack = json.loads(ack_bytes)
+ require(ack.get('schemaVersion') == 2 and ack['requestFile'] == path.name and ack['requestSha256'] == row['requestSha256']
+ and ack['runId'] == request['runId'] and type(ack['sequence']) is int and ack['sequence'] == index
+ and ack['identity'] == request['identity'] and ack['clockDomain'] == request['clockDomain'],
+ 'DL ACK is not bound to its exact durable request')
+ observed = ack['observation']
+ require(observed.get('resultUnknown') is True or observed.get('operation') == request['operation']
+ and observed.get('params') == request['params'] and observed.get('endpoint') == endpoint,
+ 'DL known observation lost its actual operation/selector/endpoint')
+ require(all(type(value) is int for value in (request['preparedMonotonicNs'],
+ ack['collectorCallStartedMonotonicNs'], ack['collectorCallFinishedMonotonicNs'], ack['recordedMonotonicNs']))
+ and request['preparedMonotonicNs'] < ack['collectorCallStartedMonotonicNs']
+ <= ack['collectorCallFinishedMonotonicNs'] <= ack['recordedMonotonicNs'],
+ 'DL collector timing differs from its original clock domain')
+ if observed.get('clientCallInvoked') is True:
+ require(observed.get('clockDomain') == request['clockDomain']
+ and type(observed.get('callStartedMonotonicNs')) is int
+ and type(observed.get('callFinishedMonotonicNs')) is int
+ and ack['collectorCallStartedMonotonicNs'] <= observed['callStartedMonotonicNs']
+ < observed['callFinishedMonotonicNs'] <= ack['collectorCallFinishedMonotonicNs'],
+ 'DL actual client interval escaped the original collection window')
+ row.update(ackPresent=True, ackFile=ack_path.name, ackSha256=hashlib.sha256(ack_bytes).hexdigest(),
+ ack=ack, resolution='UNKNOWN_RESULT' if observed.get('resultUnknown') is True
+ else 'REQUIRES_INDEPENDENT_RECONCILIATION')
+ rows.append(row)
+ return rows
+
+
+def read_journal(directory, binding, endpoint, role):
+ require(role in {'writer', 'reader'}, 'unknown DL worker role')
+ rows = read_receipts(directory, binding, endpoint)
+ for index, row in enumerate(rows, 1):
+ request, identity = row['request'], row['request']['identity']
+ require(isinstance(identity, dict) and identity.get('endpoint') == endpoint and identity.get('role') == role
+ and type(identity.get('step')) is int and identity['step'] == index,
+ 'DL request worker ownership or ordinal differs')
+ require(request['operation'] in (MUTATIONS if role == 'writer' else READS), 'DL operation escaped its worker role')
+ return rows
+
+
+class LoadJournal(Recorder):
+ """One worker serializes calls; the base Recorder persists every raw ACK."""
+ def __init__(self, probe, output, role, *, writer=None):
+ require(role in {'writer', 'reader'}, 'invalid DL worker role')
+ super().__init__(probe, output, writer=writer)
+ self.role, self.serial, self.attempted = role, threading.Lock(), 0
+
+ def request(self, step, expected_http, deadline):
+ with self.serial:
+ self.attempted += 1
+ if self.mutations.poisoned is not None:
+ return {'attempt': self.attempted, 'operation': step['operation'], 'row': None,
+ 'error': {'type': 'NotAdmitted', 'message': 'DL worker stopped after its original failure'}}
+ error = None
+ try:
+ super().call(step['operation'], step['params'], step.get('body'),
+ identity={'endpoint': self.probe.endpoint_name, 'role': self.role,
+ 'step': self.attempted, 'nonce': step.get('nonce')},
+ expect_http=expected_http, admission_deadline=deadline)
+ except Exception as exc:
+ error = {'type': type(exc).__name__, 'message': str(exc)}
+ self.mutations.poison('DL original call failed; no further calls')
+ rows = read_journal(self.output, self.probe.binding, self.probe.endpoint_name, self.role)
+ # No new durable request means no invocation by Recorder. Do not
+ # reuse the previous call's receipt after admission/validation fails.
+ row = rows[-1] if rows and rows[-1]['request']['identity']['step'] == self.attempted else None
+ return {'attempt': self.attempted, 'operation': step['operation'], 'error': error, 'row': row}
+
+
+def validate_planned_request(request, fixture, endpoint, *, readable):
+ operation = request['operation']
+ role, ordinal = request['identity']['role'], request['identity']['step']
+ steps = mutation_steps(fixture, endpoint) if role == 'writer' else read_steps(fixture, endpoint)
+ require(type(ordinal) is int and 1 <= ordinal <= len(steps), 'DL request escaped its exact planned ordinal')
+ step = steps[ordinal - 1]
+ expected_http = 503 if role == 'writer' else [200, 503] if operation.startswith('list_') else 200 if readable else 503
+ require(operation == step['operation'] and request['params'] == step['params']
+ and request['identity']['nonce'] == step.get('nonce') and request['expectHttp'] == expected_http
+ and (request.get('payload') == digest(step['body']) if 'body' in step else 'payload' not in request),
+ 'DL original selector/nonce/body/status policy differs from its planned request')
+
+
+def validate_call(result, fixture, endpoint, *, readable):
+ row = result['row']
+ require(row is not None and row['ackPresent'], 'DL call has no exact durable ACK; retain its original request')
+ observation = row['ack']['observation']
+ request, operation = row['request'], row['request']['operation']
+ validate_planned_request(request, fixture, endpoint, readable=readable)
+ response = observation.get('response') or {}
+ status = response.get('ResponseMetadata', {}).get('HTTPStatusCode')
+ require(observation.get('clientCallInvoked') is True and observation.get('resultUnknown') is False
+ and row['ack'].get('operationCheck') == 'PASS'
+ and not observation.get('clientCleanupError') and type(status) is int
+ and observation.get('acknowledged') is (200 <= status < 300),
+ 'DL request is uninvoked, uncertain, inconsistent or incompletely collected')
+ if operation in MUTATIONS:
+ require(status == 503 and response.get('Error', {}).get('Code') == 'ServiceUnavailable',
+ 'DL W/lock-negative mutation returned an unsafe ACK or a different frozen refusal')
+ return {'kind': 'REFUSED_REQUIRES_RECONCILIATION', 'originalInvocationProved': True,
+ 'rollbackClaimed': False}
+ if status == 503:
+ code = response.get('Error', {}).get('Code')
+ require((operation == 'head_object' and code == '503')
+ or operation == 'get_object' and code == 'ServiceUnavailable'
+ or operation.startswith('list_') and code in {'ServiceUnavailable', 'SlowDownRead'},
+ 'DL read lacks its exact structured candidate refusal')
+ require(not readable or operation.startswith('list_'), 'DL lost its R-capable stable current control')
+ require('payload' not in observation, 'refused DL read returned a payload')
+ return {'kind': 'READ_REFUSED', 'globalCompletenessClaimed': False}
+ require(status == 200, 'DL read returned an unexpected S3 status')
+ current = fixture['sides'][endpoint]['versions'][-1]
+ if operation in {'get_object', 'head_object'}:
+ require(readable and request['params']['VersionId'] == current['versionId']
+ and response.get('VersionId') == current['versionId']
+ and response.get('ContentLength') == current['payload']['size'], 'DL stable version identity or length differs')
+ if operation == 'get_object':
+ require(observation.get('payload') == current['payload'], 'DL GET changed the complete original payload')
+ else:
+ require({name: response.get(name) for name in fixture['sides'][endpoint]['heads'][current['versionId']]}
+ == fixture['sides'][endpoint]['heads'][current['versionId']],
+ 'DL HEAD changed an original ACK property or Last-Modified')
+ else:
+ require(operation in READS and response.get('IsTruncated') is False, 'DL scoped listing is incomplete or unrecognized')
+ if operation == 'list_parts':
+ wanted = fixture['sides'][endpoint]['uploads']['complete']
+ require(response.get('Key') == wanted['key'] and response.get('UploadId') == wanted['uploadId'],
+ 'DL parts list refers to another upload')
+ known = {p['PartNumber']: p for p in wanted['parts']}
+ identities = []
+ for part in response.get('Parts', []):
+ number = part['PartNumber'];identities.append(number)
+ require(number in known and part.get('ETag') == known[number]['ETag']
+ and part.get('Size') == known[number]['payload']['size'], 'DL parts list changed an acknowledged part')
+ else:
+ items = response.get('Contents', []) if operation == 'list_objects_v2' else (
+ response.get('Versions', []) + response.get('DeleteMarkers', []) if operation == 'list_object_versions'
+ else response.get('Uploads', []))
+ identities = []
+ for item in items:
+ require(isinstance(item.get('Key'), str) and item['Key'].startswith(f'dl/{endpoint}/'), 'DL list escaped its exact owned prefix')
+ identities.append((item['Key'], item.get('VersionId'), item.get('UploadId')))
+ require(len(identities) <= 32 and len(set(identities)) == len(identities), 'DL list exceeded its cap or duplicated an identity')
+ return {'kind': 'READ_OBSERVED', 'globalCompletenessClaimed': False}
+
+
+def client_overlap_pairs(readers, writers):
+ pairs = []
+ for a in readers:
+ for b in writers:
+ if a['ackPresent'] and b['ackPresent']:
+ x, y = a['ack']['observation'], b['ack']['observation']
+ if (x.get('clientCallInvoked') is True and y.get('clientCallInvoked') is True
+ and x['clockDomain'] == y['clockDomain']
+ and max(x['callStartedMonotonicNs'], y['callStartedMonotonicNs'])
+ < min(x['callFinishedMonotonicNs'], y['callFinishedMonotonicNs'])):
+ pairs.append({'readerRequestSha256': a['requestSha256'], 'writerRequestSha256': b['requestSha256']})
+ return pairs
+
+
+def concurrent_load(journals, fixture, readable):
+ """The controller owns Fleet evidence. Workers only append their own ACKs."""
+ require(len(journals) == 4 and set(journals) == {(endpoint, role) for endpoint in fixture['sides']
+ for role in ('reader', 'writer')}, 'DL requires exactly four independent original endpoint/role journals')
+ require(len({j.output for j in journals.values()}) == 4 and len({id(j) for j in journals.values()}) == 4,
+ 'DL workers must not share request sequences or mutable journal state')
+ require(set(readable) == set(fixture['sides']) and all(type(value) is bool for value in readable.values())
+ and all(journal.probe.endpoint_name == endpoint and journal.role == role
+ and journal.probe.binding['runId'] == fixture['runId']
+ and binding_digest(journal.probe.binding) == fixture['bindingSha256']
+ and source_reference(journal.probe.binding['candidateCommit']) == fixture['sourceContract']
+ for (endpoint, role), journal in journals.items()), 'DL original run/source/endpoint binding differs')
+ gate = threading.Barrier(4)
+ deadline = time.monotonic() + ADMISSION_SECONDS
+ def worker(endpoint, role):
+ journal = journals[endpoint, role]
+ steps = mutation_steps(fixture, endpoint) if role == 'writer' else read_steps(fixture, endpoint)
+ results, error = [], None
+ try:
+ gate.wait(timeout=5)
+ for step in steps:
+ expected = 503 if role == 'writer' else (200, 503) if step['operation'].startswith('list_') else 200 if readable[endpoint] else 503
+ result = journal.request(step, expected, deadline)
+ results.append(result)
+ result['observationProof'] = validate_call(result, fixture, endpoint, readable=readable[endpoint])
+ require(result['error'] is None, 'DL collector failed; preserve the original bound observation')
+ except Exception as exc:
+ journal.mutations.poison('DL worker stopped; drain only')
+ error = {'type': type(exc).__name__, 'message': str(exc)}
+ return {'endpoint': endpoint, 'role': role, 'plannedCalls': len(steps), 'results': results,
+ 'complete': error is None and len(results) == len(steps), 'error': error}
+ with ThreadPoolExecutor(max_workers=4) as pool:
+ futures = [pool.submit(worker, endpoint, role) for endpoint, role in journals]
+ results = [future.result() for future in futures]
+ overlaps = []
+ for endpoint in fixture['sides']:
+ rows = {role: read_journal(journals[endpoint, role].output, journals[endpoint, role].probe.binding,
+ endpoint, role) for role in ('reader', 'writer')}
+ pairs = client_overlap_pairs(rows['reader'], rows['writer'])
+ overlaps.append({'endpoint': endpoint, 'overlappingClientCalls': pairs, 'serverCommitFenceClaimed': False})
+ return {'workers': results, 'overlaps': overlaps,
+ 'complete': all(r['complete'] for r in results) and all(r['overlappingClientCalls'] for r in overlaps),
+ 'allCallsDrained': True, 'automaticRetries': 0, 'mutationReconciliationRequired': True,
+ 'canonicalCasePassClaimed': False}
diff --git a/scripts/validation_heal/network_load_repair.py b/scripts/validation_heal/network_load_repair.py
new file mode 100644
index 00000000..2e0ef258
--- /dev/null
+++ b/scripts/validation_heal/network_load_repair.py
@@ -0,0 +1,190 @@
+"""Bind refused DL deletes to exact historical generations and product recovery."""
+
+import copy
+import uuid
+
+try:
+ from .heal_probe import require
+ from .network_load import validate_call
+ from .shards import files, map_target, run_task
+ from .versions import public_version_id, validate_version_receipts
+except ImportError:
+ from heal_probe import require
+ from network_load import validate_call
+ from shards import files, map_target, run_task
+ from versions import public_version_id, validate_version_receipts
+
+
+CANDIDATE = '1c7fc818dd858247c45b39542abd8291b1e6e31b'
+SOURCE_FILES = {
+ 'crates/ecstore/src/disk/local.rs': 'c715c122c06eafb5861ab560e1acac087e5326cd72e2063f0a437786fb4a965b',
+ 'crates/ecstore/src/disk/local/delete_recovery.rs': 'dc631db4794b0189d42636247f407e32d1d94fc65253c17b6f4942655f8ae7e3',
+ 'crates/ecstore/src/set_disk/ops/heal.rs': 'fc576119f455848ce279772ffca6d2943cb9f909968cae367860a9887d3e020e',
+ 'crates/ecstore/src/disk/mod.rs': 'cd045a322ad7e41e2253ac08554f10634192345cbd923f2854553a229c036f4b',
+ 'crates/filemeta/src/fileinfo.rs': 'e2553d6b0210ad15d723896735c5a5a40528d9bce10d9a6e02f7ac2fff271c24',
+}
+
+
+CANDIDATES = frozenset((CANDIDATE, '85c706cac09d53d0649487b8f1865f18beb53225',
+ '62ff96774fa4139d45f14d70b70519aae8070ac0'))
+
+
+def source_reference(candidate):
+ require(candidate in CANDIDATES, 'full I03 DL delete recovery is not reviewed for this exact candidate')
+ return {'candidateCommit': candidate, 'sourceFiles': SOURCE_FILES.copy(),
+ 'deleteWriteQuorum': 'configured set / 2 + 1', 'failedDeleteAttemptsProductRollback': True,
+ 'rollbackFailureIsNotSuccessfulDelete': True, 'healSettlesDeleteBeforeReadingVersion': True,
+ 'preparedTransactionAloneProvesNeitherCommitNorRollback': True,
+ 'finalOriginalBusinessPhysicalAndPayloadProofRequired': True}
+
+
+def classify_deletes(before, after, fixture, receipts, plan, nodes, delete_records):
+ """Preserve all control assertions; only the original refused old-version delete may leave residue."""
+ source_reference(fixture['sourceContract']['candidateCommit'])
+ require(list(before) == list(after) == list(nodes), 'DL failure scene changed original node ordering')
+ left, right = files(before), files(after)
+ scopes, targets = {}, []
+ for worker in receipts['workers']:
+ if worker['role'] != 'writer': continue
+ endpoint = worker['endpoint']
+ for row in worker['rows']:
+ if row['request']['operation'] != 'delete_object': continue
+ validate_call({'row': row}, fixture, endpoint, readable=True)
+ side = next(side for side in plan['sides'] if side['endpoint'] == endpoint)
+ if side['lockClients'] < plan['exclusiveLockQuorum']: continue
+ entry = fixture['sides'][endpoint]['versions'][0]
+ require(row['request']['params']['VersionId'] == entry['versionId'], 'DL delete escaped its first exact historical ACK')
+ target = map_target(before, fixture['bucket'], entry['key'], 'data', 1, entry['versionId'], latest_required=False)
+ allowed = {(nodes[index], disk) for index in side['members'] for disk in range(4)
+ if disk not in plan['gates'].get(nodes[index], [])}
+ scopes[entry['key']] = {'target': target, 'allowed': allowed, 'changed': set(), 'lost': set(), 'transactions': {}}
+ for node in before:
+ require(before[node]['formats'] == after[node]['formats'], 'DL refused mutation changed an original disk format')
+ def scope_for(identity):
+ node, disk, path = identity
+ for key, scope in scopes.items():
+ prefix = fixture['bucket'] + '/' + key + '/'
+ if path.startswith(prefix) and (node, disk) in scope['allowed']:
+ return scope, path[len(prefix):]
+ return None, None
+ for identity, original in left.items():
+ actual = right.get(identity)
+ if actual == original: continue
+ scope, relative = scope_for(identity)
+ require(scope is not None, 'DL refused mutation changed an unrelated original version, DB control or damaged DV target')
+ slot = identity[:2];target = scope['target']
+ if relative == 'xl.meta':
+ require(actual is not None and original.get('metadata') and actual.get('metadata'), 'DL delete lost all original control metadata')
+ old, new = original['metadata'], actual['metadata']
+ reduced = {**old, 'versions': [version for version in old['versions']
+ if public_version_id(version.get('versionId')) != target['versionId']]}
+ require(new == old or new == reduced, 'DL delete changed another original generation or introduced metadata/version content')
+ if new == reduced: scope['lost'].add(slot)
+ else:
+ require(relative.startswith(target['dataDir'] + '/') and actual is None,
+ 'DL delete changed bytes or removed a different acknowledged data directory')
+ scope['lost'].add(slot)
+ scope['changed'].add(slot)
+ for identity in set(right) - set(left):
+ scope, relative = scope_for(identity)
+ require(scope is not None, 'DL refused mutation created an unbound business file')
+ parts = relative.split('/');target = scope['target'];slot = identity[:2]
+ require(len(parts) in {2, 3} and str(uuid.UUID(parts[0])) == parts[0]
+ and parts[0] not in {version['dataDir'] for version in left[(identity[0], identity[1],
+ fixture['bucket'] + '/' + target['key'] + '/xl.meta')]['metadata']['versions']},
+ 'DL refused delete residue is not a distinct source transaction directory')
+ transaction = scope['transactions'].setdefault(parts[0], {})
+ transaction.setdefault(slot, set()).add('/'.join(parts[1:]))
+ if len(parts) == 2:
+ require(parts[1] in {'xl.meta.bkp', 'delete-prepared', 'delete-target', 'delete-commit-finished'},
+ 'DL delete residue contains a foreign transaction file')
+ if parts[1] == 'xl.meta.bkp':
+ original = left[(identity[0], identity[1], fixture['bucket'] + '/' + target['key'] + '/xl.meta')]
+ require(all(right[identity][name] == original[name] for name in ('size', 'sha256')),
+ 'DL transaction backup is not the exact original metadata bytes')
+ else:
+ require(parts[1] == target['dataDir'], 'DL transaction staged a different acknowledged generation')
+ original = left.get((identity[0], identity[1], fixture['bucket'] + '/' + target['key'] + '/' + '/'.join(parts[1:])))
+ require(original is not None and all(right[identity][name] == original[name] for name in ('size', 'sha256')),
+ 'DL transaction staged bytes differ from the exact original file')
+ scope['changed'].add(slot)
+ require(set(delete_records) == set(before), 'DL delete record census omitted an original peer')
+ prepared = {}
+ control_keys = sorted(side['controlKey'] for side in fixture['sides'].values())
+ for node, census in delete_records.items():
+ require(census.get('readOnly') is True and census.get('quiescent') is True
+ and census.get('runId') == fixture['runId'] and census.get('bucket') == fixture['bucket']
+ and census.get('binarySha256') == before[node]['binarySha256'] and census.get('keys') == control_keys
+ and census.get('credentialContentsRead') is False,
+ 'DL prepared delete records lack original stopped candidate/control ownership')
+ for row in census['records']:
+ identity = (node, row['disk'], row['path'])
+ scope, relative = scope_for(identity)
+ require(identity not in prepared and scope is not None and identity in right and identity not in left,
+ 'DL prepared delete record is duplicated, pre-existing or unrelated to the original invocation')
+ target = scope['target'];parts = relative.split('/')
+ original = left[(node, row['disk'], fixture['bucket'] + '/' + target['key'] + '/xl.meta')]
+ versions = original['metadata']['versions']
+ require(len(parts) == 2 and parts[1] == 'delete-prepared' and row['transaction'] == parts[0]
+ and row.get('protocolVersion') == 1 and row['key'] == target['key']
+ and all(row[name] == right[identity][name] for name in ('size', 'sha256'))
+ and row['requestVersion'] == target['versionId'] and row['deletedVersions'] == [target['versionId']]
+ and row['dataDirs'] == [target['dataDir']] and row['originalMetadataSha256'] == original['sha256']
+ and len(row['originalVersions']) == len(row['originalDataDirs']) == 3
+ and set(zip(row['originalVersions'], row['originalDataDirs']))
+ == {(public_version_id(version.get('versionId')), version['dataDir']) for version in versions}
+ and isinstance(row.get('bucketIncarnation'), str) and row['bucketIncarnation'],
+ 'DL prepared delete journal does not bind its raw bytes to the exact original generation/backup')
+ target_identity = (node, row['disk'], fixture['bucket'] + '/' + target['key'] + '/' + parts[0] + '/delete-target')
+ require(target_identity in right and row['targetMetadataSha256'] == right[target_identity]['sha256'],
+ 'DL prepared delete target bytes differ from the original journal digest')
+ prepared[identity] = row
+ require(set(prepared) == {identity for identity in set(right) - set(left) if identity[2].endswith('/delete-prepared')},
+ 'DL original prepared delete file lacks an independent stopped raw-byte record')
+ policies = {}
+ for key, scope in scopes.items():
+ require(len(scope['transactions']) <= 1, 'one original DL delete cannot explain multiple transaction IDs')
+ complete_backups = {slot for transaction, locations in scope['transactions'].items() for slot, names in locations.items()
+ if {'xl.meta.bkp', 'delete-prepared', 'delete-target'} <= names
+ and (*slot, fixture['bucket'] + '/' + key + '/' + transaction + '/delete-prepared') in prepared}
+ # A staged journal is only a recovery obligation. The subsequent product
+ # task and final physical/payload proofs must redeem the original ACKs.
+ missing = scope['lost']
+ policies[key] = {'versionId': scope['target']['versionId'],
+ 'requiredRepair': bool(missing - complete_backups),
+ 'productSettlementAllowed': bool(missing) and missing <= complete_backups,
+ 'changedOriginalSlots': sorted(scope['changed']),
+ 'originalBucketIncarnations': sorted({row['bucketIncarnation'] for identity, row in prepared.items()
+ if row['key'] == key}),
+ 'originalTransactionIds': sorted(scope['transactions']), 'finalPhysicalProofRequired': True}
+ for slot in scope['changed']:
+ member = next(member for member in scope['target']['cohort'] if (member['node'], member['disk']) == slot)
+ targets.append({**scope['target'], **member})
+ return {'policies': policies, 'repairTargets': targets, 'refusalProvesRollback': False,
+ 'originalFailureSceneRetained': True, 'canonicalCasePassClaimed': False}
+
+
+def recover_controls(probe, fleet, binding, fixture, damage, active):
+ result = {}
+ for side in fixture['sides'].values():
+ expected, key = side['versions'], side['controlKey']
+ policy = damage['policies'].get(key, {})
+ status = run_task(probe, fleet, {'bucket': fixture['bucket'], 'key': key}, 2, active,
+ option_overrides={'recursive': True}, status_endpoints=tuple(binding['endpoints'])[1:])
+ pair = (key, expected[0]['versionId'])
+ required = {pair} if policy.get('requiredRepair') else set()
+ settlement = policy.get('productSettlementAllowed')
+ if settlement:
+ for row in status.get('outcome', {}).get('objects', []):
+ identity = row.get('identity', {})
+ if (identity.get('object'), public_version_id(identity.get('versionId'))) == pair:
+ require(row.get('disposition', {}).get('state') in {'repaired', 'verified_healthy'},
+ 'DL original delete generation remains unresolved after product settlement')
+ if row['disposition']['state'] == 'repaired': required.add(pair)
+ proof = validate_version_receipts(status, fixture['bucket'], expected, 2, required_repairs=required)
+ if policy.get('originalBucketIncarnations'):
+ require(policy['originalBucketIncarnations'] == [proof['bucketIncarnationId']],
+ 'DL recovered task responsibility differs from the original delete journal incarnation')
+ result[key] = {**proof, 'originalDeletePolicy': copy.deepcopy(policy),
+ 'productSettlementMayPrecedeShardCheck': bool(settlement), 'finalPhysicalProofRequired': True}
+ return result
diff --git a/scripts/validation_heal/network_load_session.py b/scripts/validation_heal/network_load_session.py
new file mode 100644
index 00000000..9354a4e4
--- /dev/null
+++ b/scripts/validation_heal/network_load_session.py
@@ -0,0 +1,355 @@
+"""Own I03 DL intents and reconcile public effects before any namespace cleanup."""
+
+import copy
+import hashlib
+import json
+from pathlib import Path
+import threading
+
+try:
+ from .heal_probe import Probe, compare_inventories, require, save
+ from .heal_workload import Recorder, verify_seed
+ from .network_load import (LoadJournal, MUTATIONS, binding_digest, concurrent_load, read_journal,
+ read_receipts, seed_load, source_reference, validate_planned_request)
+ from .mpu_scene import keys_for
+ from .shards import files, validate_repaired_physical
+except ImportError:
+ from heal_probe import Probe, compare_inventories, require, save
+ from heal_workload import Recorder, verify_seed
+ from network_load import (LoadJournal, MUTATIONS, binding_digest, concurrent_load, read_journal,
+ read_receipts, seed_load, source_reference, validate_planned_request)
+ from mpu_scene import keys_for
+ from shards import files, validate_repaired_physical
+
+
+def json_hash(value):
+ return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(',', ':'), default=str).encode()).hexdigest()
+
+
+class LoadSession:
+ def __init__(self, binding, bucket, endpoints, directory, *, probe_factory=Probe):
+ self.binding, self.bucket = copy.deepcopy(binding), bucket
+ self.endpoints, self.keys = tuple(endpoints), keys_for(endpoints)
+ require(bucket == binding['bucketPrefix'] and set(endpoints) <= set(binding['endpoints'])
+ and len(binding['endpoints']) == 4, 'DL session requires its original four-peer primary bucket binding')
+ self.directory, self.probe_factory = Path(directory), probe_factory
+ self.directory.mkdir(parents=True, exist_ok=False)
+ self.serial = threading.Lock()
+ self.fixture, self.baseline, self.load_result = None, None, None
+ self.safe_to_cleanup, self.pre_read, self.capture_count, self.attempt = False, None, 0, 0
+ self.phase, self.post_mpu, self.business_baseline = 'REGISTERED', None, None
+ # The intent names every recorder before the first SDK call, including
+ # a seed that fails before it can return a complete fixture.
+ save(self.directory / 'intent.json', {'runId': binding['runId'], 'bucket': bucket,
+ 'bindingSha256': binding_digest(binding), 'sourceContract': source_reference(binding['candidateCommit']),
+ 'keys': self.keys, 'seedJournal': 'seed', 'workers': [
+ {'endpoint': endpoint, 'role': role, 'directory': endpoint + '-' + role}
+ for endpoint in self.endpoints for role in ('reader', 'writer')], 'cleanupAllowed': False})
+ self.seed_recorder = Recorder(probe_factory(self.binding), self.directory / 'seed')
+ self.journals = {(endpoint, role): LoadJournal(probe_factory(self.binding, endpoint),
+ self.directory / (endpoint + '-' + role), role) for endpoint in self.endpoints for role in ('reader', 'writer')}
+
+ def seed(self):
+ with self.serial:
+ require(self.phase == 'REGISTERED' and self.seed_recorder.sequence == 0, 'DL seed cannot be retried or replaced')
+ self.phase = 'SEED_STARTED'
+ try:
+ self.fixture = seed_load(self.seed_recorder, self.bucket, self.endpoints)
+ except BaseException:
+ self.phase = 'SEED_FAILED'
+ raise
+ save(self.directory / 'fixture.json', self.fixture)
+ self.fixture_sha = json_hash(self.fixture)
+ self.phase = 'SEEDED'
+ return copy.deepcopy(self.fixture)
+
+ def register_baseline(self, inventory):
+ with self.serial:
+ require(self.phase == 'SEEDED' and self.baseline is None and inventory['bucket'] == self.bucket,
+ 'DL baseline lacks its original seeded bucket or was already frozen')
+ subset = {**inventory, 'entries': [row for row in inventory['entries'] if row['key'].startswith('dl/')]}
+ verify_seed(self.fixture['expected'], subset)
+ self.baseline = copy.deepcopy(inventory)
+ save(self.directory / 'original-baseline.json', self.baseline)
+ self.baseline_sha = json_hash(self.baseline)
+ self.phase = 'BASELINED'
+
+ def _stopped_business_scope(self, snapshot):
+ require(len(snapshot) == 4 and all(value.get('quiescent') is True
+ and type(value.get('pid')) is int and value['pid'] == 0
+ and value.get('binarySha256') == self.binding['candidateBinarySha256']
+ and value.get('unit') == 'rustfs-validation-heal-' + self.binding['runId'] + '.service'
+ and len(value['formats']) == 4 for value in snapshot.values()),
+ 'DL business scene lacks the stopped original sixteen-drive candidate')
+ require(all(entry['path'].split('/')[0] in {self.bucket, self.bucket + '-db'}
+ for entry in files(snapshot).values()), 'DL business scene escaped its primary/DB buckets')
+
+ def register_business_baseline(self, snapshot):
+ with self.serial:
+ require(self.phase == 'BASELINED' and self.business_baseline is None,
+ 'DL original business scene must be frozen before any load')
+ self._stopped_business_scope(snapshot)
+ self.business_baseline = copy.deepcopy(snapshot)
+ self.business_baseline_sha = json_hash(snapshot)
+ save(self.directory / 'original-business-physical.json', snapshot)
+
+ def run_load(self, readable):
+ with self.serial:
+ require(self.phase == 'BASELINED' and self.business_baseline is not None
+ and not (self.directory / 'load-started.json').exists(), 'DL requires one original seeded/baselined load')
+ save(self.directory / 'load-started.json', {'readable': readable, 'bindingSha256': binding_digest(self.binding)})
+ self.phase = 'LOAD_STARTED'
+ try:
+ self.load_result = concurrent_load(self.journals, self.fixture, readable)
+ finally:
+ self.phase = 'LOAD_FINISHED'
+ save(self.directory / 'load-result.json', self.load_result)
+ return self.load_result
+
+ def registered_receipts(self):
+ return {'seed': read_receipts(self.seed_recorder.output, self.binding, self.seed_recorder.probe.endpoint_name),
+ 'workers': [{'endpoint': endpoint, 'role': role,
+ 'rows': read_journal(journal.output, self.binding, endpoint, role)}
+ for (endpoint, role), journal in self.journals.items()]}
+
+ def _validate_seed(self, rows):
+ require(len(rows) == 27, 'DL complete seed omits or adds an original request')
+ versions = {(r['key'], r['versionId']): r for r in self.fixture['expected']}
+ uploads = {u['key']: u for side in self.fixture['sides'].values() for u in side['uploads'].values()}
+ seen, counts = set(), {}
+ for row in rows:
+ require(row['ackPresent'], 'DL complete seed has a prepared request without its ACK')
+ req, obs = row['request'], row['ack']['observation']
+ op, params, response = req['operation'], req['params'], obs.get('response') or {}
+ require(params['Bucket'] == self.bucket and obs.get('clientCallInvoked') is True
+ and obs.get('acknowledged') is True and obs.get('resultUnknown') is False
+ and row['ack']['operationCheck'] == 'PASS', 'DL seed ACK is missing, foreign or uncertain')
+ counts[op] = counts.get(op, 0) + 1
+ if op in {'put_object', 'head_object'}:
+ pair = params['Key'], response.get('VersionId') if op == 'put_object' else params['VersionId']
+ require(pair in versions and (op, *pair) not in seen, 'DL seed omitted or duplicated a control version ACK')
+ seen.add((op, *pair))
+ if op == 'put_object': require(req.get('payload') == versions[pair]['payload'], 'DL original seed body changed')
+ elif op == 'create_multipart_upload':
+ require(params['Key'] in uploads and response.get('UploadId') == uploads[params['Key']]['uploadId']
+ and (op, params['Key']) not in seen, 'DL seed Create ACK belongs to another upload')
+ seen.add((op, params['Key']))
+ elif op == 'upload_part':
+ upload = uploads.get(params['Key'])
+ part = next((p for p in upload['parts'] if p['PartNumber'] == params['PartNumber']), None) if upload else None
+ require(part is not None and params['UploadId'] == upload['uploadId']
+ and req.get('payload') == part['payload'] and response.get('ETag') == part['ETag']
+ and (op, params['Key'], params['PartNumber']) not in seen, 'DL seed part ACK changed its upload/number/body')
+ seen.add((op, params['Key'], params['PartNumber']))
+ else:
+ require(op == 'get_bucket_versioning' and params == {'Bucket': self.bucket}
+ and response.get('Status') == 'Enabled', 'DL seed operation escaped its original plan')
+ require(counts == {'get_bucket_versioning': 1, 'put_object': 6, 'head_object': 6,
+ 'create_multipart_upload': 6, 'upload_part': 8}, 'DL seed request matrix differs')
+
+ def capture_mpu(self, fleet, stage):
+ with self.serial:
+ require(stage in {'baseline', 'failure', 'pre-reconcile', 'post-reconcile'} and len(fleet.nodes) == len(set(fleet.nodes)) == 4
+ and (stage != 'post-reconcile' or self.phase == 'PUBLIC_RECONCILED'),
+ 'DL MPU capture requires the complete original stopped fleet and a named phase')
+ scenes = {node: fleet.remote(node, 'mpu-scene', bucket=self.bucket, keys=self.keys,
+ expectedBinarySha256=self.binding['candidateBinarySha256']) for node in fleet.nodes}
+ for node, scene in scenes.items():
+ require(scene.get('quiescent') is True and scene.get('readOnly') is True
+ and scene.get('runId') == self.binding['runId'] and scene.get('bucket') == self.bucket
+ and scene.get('binarySha256') == self.binding['candidateBinarySha256']
+ and scene.get('keys') == self.keys and scene.get('originalPhysicalDrives') == 4
+ and scene.get('payloadIntegrityClaimed') is False,
+ 'DL MPU scene is not the original stopped candidate/key scope')
+ value = {name: scene[name] for name in ('bucket', 'keys', 'scopes', 'directories', 'files', 'bytesHashed')}
+ require(json_hash(value) == scene['sceneSha256'] and len(scene['scopes']) == 32
+ and {(row['disk'], row['key']) for row in scene['scopes']}
+ == {(disk, key) for disk in range(4) for key in self.keys}, 'DL MPU census is incomplete or changed')
+ key_hashes = {key: hashlib.sha256((self.bucket + '/' + key).encode()).hexdigest() for key in self.keys}
+ require(all(row['keySha256'] == key_hashes[row['key']] and type(row['present']) is bool
+ for row in scene['scopes']), 'DL MPU census hashes refer to another bucket/key')
+ for kind in ('files', 'directories'):
+ rows = scene[kind]
+ require(len({(row['disk'], row['path']) for row in rows}) == len(rows)
+ and all(type(row['disk']) is int and 0 <= row['disk'] < 4 and row['key'] in key_hashes
+ and '..' not in Path(row['path']).parts
+ and (row['path'] == '.rustfs.sys/multipart/' + key_hashes[row['key']]
+ or row['path'].startswith('.rustfs.sys/multipart/' + key_hashes[row['key']] + '/'))
+ for row in rows), 'DL MPU file/directory escaped its original exact key scope')
+ self.capture_count += 1
+ saved = {'stage': stage, 'bindingSha256': binding_digest(self.binding), 'nodes': scenes}
+ save(self.directory / f'mpu-{self.capture_count:02d}-{stage}.json', saved)
+ fleet.record('DL-stopped-MPU-' + stage, saved)
+ if stage == 'pre-reconcile': self.pre_read = json_hash(saved)
+ if stage == 'post-reconcile': self.post_mpu = saved
+ return saved
+
+ def _call(self, recorder, operation, params, expected):
+ recorder.call(operation, params, expect_http=expected)
+ row = read_receipts(recorder.output, self.binding, recorder.probe.endpoint_name)[-1]
+ observed = row['ack']['observation']
+ require(observed.get('clientCallInvoked') is True and observed.get('resultUnknown') is False
+ and row['ack']['operationCheck'] == 'PASS' and not observed.get('clientCleanupError'),
+ 'DL reconciliation call has no exact known invoked response')
+ return observed
+
+ def _uploads(self, recorders, label, folder):
+ result = {}
+ for endpoint, recorder in recorders.items():
+ observed = self._call(recorder, 'list_multipart_uploads', {
+ 'Bucket': self.bucket, 'Prefix': 'dl/', 'MaxUploads': 32}, 200)
+ response, uploads = observed['response'], []
+ require(response.get('IsTruncated') is False, 'DL pending upload listing is incomplete')
+ for row in response.get('Uploads', []):
+ require(row.get('Key') in self.keys and isinstance(row.get('UploadId'), str) and row['UploadId'],
+ 'DL pending upload escaped an original key or omitted its opaque ID')
+ uploads.append((row['Key'], row['UploadId']))
+ require(len(uploads) <= 32 and len(set(uploads)) == len(uploads), 'DL pending upload list duplicates/exceeds its cap')
+ result[endpoint] = sorted(uploads)
+ save(folder / (label + '.json'), result)
+ return result
+
+ def _inventories(self, folder, label):
+ observed = {}
+ # Inventory itself verifies full version GET payloads, tags and current
+ # LIST consistency. Save each peer before checking equality.
+ for endpoint in self.binding['endpoints']:
+ value = self.probe_factory(self.binding, endpoint).inventory(self.bucket)
+ save(folder / (endpoint + '-' + label + '.json'), value)
+ observed[endpoint] = value
+ for value in observed.values(): compare_inventories(self.baseline, value)
+ return observed
+
+ def reconcile(self):
+ """Retire pending uploads with distinct recovery ACKs, never retry PUT/Complete."""
+ with self.serial:
+ self.safe_to_cleanup = False
+ require(self.fixture is not None and self.baseline is not None and self.pre_read is not None
+ and self.phase in {'BASELINED', 'LOAD_FINISHED', 'RECONCILE_FAILED'}
+ and self.attempt < 2 and json_hash(self.fixture) == self.fixture_sha
+ and json_hash(self.baseline) == self.baseline_sha,
+ 'DL reconciliation lacks a complete frozen seed/baseline or original stopped MPU scene')
+ self.attempt += 1
+ self.phase = 'RECONCILING'
+ folder = self.directory / f'reconcile-{self.attempt:02d}'
+ folder.mkdir()
+ original = self.registered_receipts()
+ self.original_receipts_sha = json_hash(original)
+ save(folder / 'original-request-states.json', original)
+ recorders = {endpoint: Recorder(self.probe_factory(self.binding, endpoint), folder / endpoint)
+ for endpoint in self.binding['endpoints']}
+ try:
+ self._validate_seed(original['seed'])
+ for worker in original['workers']:
+ if worker['role'] == 'writer':
+ for row in worker['rows']:
+ validate_planned_request(row['request'], self.fixture, worker['endpoint'], readable=True)
+ self._inventories(folder, 'before-retirement')
+ writers = [row for worker in original['workers'] if worker['role'] == 'writer' for row in worker['rows']]
+ require(all(not (row['ackPresent'] and row['ack']['observation'].get('acknowledged') is True) for row in writers),
+ 'DL unexpected positive mutation ACK must be independently redeemed; retain the original failure')
+ known = {(upload['key'], upload['uploadId']) for side in self.fixture['sides'].values() for upload in side['uploads'].values()}
+ discovery_keys = {row['request']['params']['Key'] for row in writers if row['request']['operation'] == 'create_multipart_upload'}
+ pending = self._uploads(recorders, 'pending-before', folder)
+ discovered = {pair for rows in pending.values() for pair in rows} - known
+ require(all(key in discovery_keys for key, identity in discovered)
+ and len({key for key, identity in discovered}) == len(discovered),
+ 'DL observed an unbound or duplicate unknown CreateMultipartUpload effect')
+ registered = sorted(known | discovered)
+ recovery = recorders[self.binding['defaultEndpoint']]
+ retirements = []
+ for key, upload_id in registered:
+ params = {'Bucket': self.bucket, 'Key': key, 'UploadId': upload_id}
+ # A refused abort may have partially removed upload/parts.
+ # These observations precede a distinct, fully restored
+ # cluster Abort; neither is relabeled as the original ACK.
+ for recorder in recorders.values():
+ part = self._call(recorder, 'list_parts', {**params, 'MaxParts': 32}, (200, 404, 503))
+ status = part['response']['ResponseMetadata']['HTTPStatusCode']
+ require(status == 200 and part['response'].get('IsTruncated') is False
+ and part['response'].get('Key') == key and part['response'].get('UploadId') == upload_id
+ or status == 404 and part['response'].get('Error', {}).get('Code') == 'NoSuchUpload'
+ or status == 503 and part['response'].get('Error', {}).get('Code') in {'ServiceUnavailable', 'SlowDownRead'},
+ 'DL pre-retirement parts observation is foreign or unrecognized')
+ response = self._call(recovery, 'abort_multipart_upload', params, (204, 404))
+ status = response['response']['ResponseMetadata']['HTTPStatusCode']
+ require(status == 204 and response['acknowledged'] is True
+ or status == 404 and response['acknowledged'] is False
+ and response['response'].get('Error', {}).get('Code') == 'NoSuchUpload',
+ 'DL recovery Abort has neither its own ACK nor exact upload absence')
+ retirements.append({'key': key, 'uploadId': upload_id, 'newRecoveryAbortAck': status == 204,
+ 'observedNoSuchUpload': status == 404, 'originalAbortAckClaimed': False})
+ remaining = self._uploads(recorders, 'pending-after', folder)
+ require(all(not values for values in remaining.values()), 'DL pending upload survived original-intent retirement')
+ for key, upload_id in registered:
+ for recorder in recorders.values():
+ absent = self._call(recorder, 'list_parts', {
+ 'Bucket': self.bucket, 'Key': key, 'UploadId': upload_id, 'MaxParts': 32}, 404)
+ require(absent['response'].get('Error', {}).get('Code') == 'NoSuchUpload'
+ and absent['acknowledged'] is False, 'DL retired upload still has addressable parts')
+ self._inventories(folder, 'after-retirement')
+ result = {'allOriginalBusinessVersionsPreserved': True, 'allOriginalRequestsResolved': False,
+ 'allPeerPendingUploadsAbsent': True, 'retirements': retirements,
+ 'discoveredUploadIdsAreNotCreateAcks': sorted(discovered), 'originalRequestBytesRewritten': False,
+ 'originalUnknownResultsRelabeledAcknowledged': False, 'PUTOrCompleteRetryIssued': False,
+ 'preReadStoppedMpuEvidenceSha256': self.pre_read,
+ 'originalLoadComplete': self.load_result is not None and self.load_result['complete'],
+ 'canonicalCasePassClaimed': False, 'namespaceCleanupAllowed': False,
+ 'stoppedPostRetirementPhysicalCheckRequired': True}
+ save(folder / 'public-closure.json', result)
+ self.public_candidate, self.public_candidate_sha = result, json_hash(result)
+ self.phase = 'PUBLIC_RECONCILED'
+ return result
+ except Exception as exc:
+ self.phase = 'RECONCILE_FAILED'
+ save(folder / 'unresolved.json', {'type': type(exc).__name__, 'message': str(exc),
+ 'namespaceCleanupAllowed': False, 'canonicalCasePassClaimed': False})
+ raise
+
+ def finish_reconciliation(self, business_after, *, repair_targets):
+ with self.serial:
+ require(self.phase == 'PUBLIC_RECONCILED' and self.post_mpu is not None
+ and json_hash(self.public_candidate) == self.public_candidate_sha
+ and self.business_baseline is not None
+ and json_hash(self.business_baseline) == self.business_baseline_sha,
+ 'DL closure lacks its original public and stopped post-retirement evidence')
+ self._stopped_business_scope(business_after)
+ require(set(business_after) == set(self.business_baseline), 'DL business scene changed original nodes')
+ expected = {(row['key'], row['versionId']) for row in self.baseline['entries'] if not row['deleteMarker']}
+ require(isinstance(repair_targets, list) and all(target['bucket'] == self.bucket
+ and (target['key'], target['versionId']) in expected for target in repair_targets),
+ 'DL repair exceptions escaped original acknowledged business versions')
+ if repair_targets:
+ physical = validate_repaired_physical(self.business_baseline, business_after, repair_targets[0],
+ repair_targets=repair_targets)
+ else:
+ require(files(self.business_baseline) == files(business_after)
+ and all(value['formats'] == business_after[node]['formats']
+ for node, value in self.business_baseline.items()),
+ 'DL business scene changed files or formats without an independent repair')
+ physical = {'allOriginalPhysicalFilesAndFormatsExact': True}
+ save(self.directory / f'reconcile-{self.attempt:02d}' / 'final-business-physical.json', business_after)
+ require(all(not scene['files'] for scene in self.post_mpu['nodes'].values()),
+ 'DL unaccounted multipart files remain; retain the original namespace')
+ original = self.registered_receipts()
+ require(json_hash(original) == self.original_receipts_sha, 'DL original request journals changed during reconciliation')
+ effects = []
+ rows = [('seed', row) for row in original['seed'] if row['request']['operation'] in MUTATIONS]
+ rows.extend((worker['endpoint'], row) for worker in original['workers'] if worker['role'] == 'writer' for row in worker['rows'])
+ for actor, row in rows:
+ operation = row['request']['operation']
+ upload = operation in {'create_multipart_upload', 'upload_part', 'abort_multipart_upload'}
+ effects.append({'actor': actor, 'operation': operation, 'requestSha256': row['requestSha256'],
+ 'effect': 'NO_REMAINING_UPLOAD_AFTER_EXPLICIT_RECOVERY' if upload else
+ 'ORIGINAL_VERSION_PRESERVED' if actor == 'seed' or operation == 'delete_object' else 'NO_BUSINESS_COMMIT',
+ 'originalAckPresent': row['ackPresent'],
+ 'originalResultUnknown': not row['ackPresent'] or row['ack']['observation'].get('resultUnknown') is True,
+ 'originalAcknowledged': row['ackPresent'] and row['ack']['observation'].get('acknowledged') is True})
+ result = {**self.public_candidate, 'allOriginalRequestsResolved': True,
+ 'requestEffectResolutions': effects, 'stoppedPostRetirementPhysicalCheckRequired': False,
+ 'originalBusinessPhysicalSha256': self.business_baseline_sha,
+ 'postRetirementBusinessPhysicalSha256': json_hash(business_after), 'businessPhysical': physical,
+ 'postRetirementMpuEvidenceSha256': json_hash(self.post_mpu), 'namespaceCleanupAllowed': True}
+ save(self.directory / f'reconcile-{self.attempt:02d}' / 'closure.json', result)
+ self.safe_to_cleanup, self.phase = True, 'CLOSED'
+ return result
diff --git a/scripts/validation_heal/network_retention.py b/scripts/validation_heal/network_retention.py
new file mode 100644
index 00000000..6eef009e
--- /dev/null
+++ b/scripts/validation_heal/network_retention.py
@@ -0,0 +1,147 @@
+"""Preserve unresolved I03 business and MPU bytes without claiming restoration."""
+
+import copy
+import hashlib
+import json
+import re
+
+try:
+ from .heal_probe import require
+ from .healthy import physical_content
+ from .mpu_scene import keys_for
+except ImportError:
+ from heal_probe import require
+ from healthy import physical_content
+ from mpu_scene import keys_for
+
+
+def digest(value):
+ return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(',', ':')).encode()).hexdigest()
+
+
+def validate_plan(plan):
+ require(set(plan) == {'kind', 'runId', 'nodes', 'candidateCommit', 'harnessCommit', 'binarySha256',
+ 'bucket', 'owners', 'businessSceneSha256', 'mpuSceneSha256', 'mpuKeys'}
+ and plan['kind'] == 'unresolved-network-load'
+ and re.fullmatch(r'gh-[1-9][0-9]*-[1-9][0-9]*-h2', plan['runId'])
+ and len(plan['nodes']) == len(set(plan['nodes'])) == 4
+ and all(re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9_.-]*', node) for node in plan['nodes'])
+ and re.fullmatch(r'rv-heal-[0-9a-f]{16}', plan['bucket'])
+ and all(re.fullmatch(r'[0-9a-f]{40}', plan[field]) for field in ('candidateCommit', 'harnessCommit'))
+ and re.fullmatch(r'[0-9a-f]{64}', plan['binarySha256']), 'I03 retention plan identity or scope differs')
+ for field in ('owners', 'businessSceneSha256', 'mpuSceneSha256'):
+ require(set(plan[field]) == set(plan['nodes']), 'I03 retention omits an original peer')
+ endpoints = sorted({key.split('/')[1] for key in plan['mpuKeys']})
+ require(plan['mpuKeys'] == keys_for(endpoints), 'I03 retention MPU scope differs from its two original sides')
+ for node in plan['nodes']:
+ owner = plan['owners'][node]
+ require(owner.get('runId') == plan['runId'] and owner.get('bucket') == plan['bucket']
+ and owner.get('runtimeProfile') == 'lock-quorum' and owner.get('binarySha256') == plan['binarySha256']
+ and owner.get('unit') == 'rustfs-validation-heal-' + plan['runId'] + '.service'
+ and len(owner.get('mounts', [])) == 4
+ and all(re.fullmatch(r'[0-9a-f]{64}', plan[field][node]) for field in ('businessSceneSha256', 'mpuSceneSha256')),
+ 'I03 retention lacks the exact reserved candidate/runtime owner and stopped scene digests')
+ require(len({owner['hostname'] for owner in plan['owners'].values()}) == 4,
+ 'I03 retention aliases do not identify four distinct reserved hosts')
+
+
+def prepare(fleet, binding, keys, business, mpu_nodes):
+ """Freeze the actual stopped failure scene after gate release, before moving."""
+ require(fleet.run_id == binding['runId'] and not fleet.network_gates and not fleet.quorum_gates
+ and set(business) == set(mpu_nodes) == set(fleet.nodes) and len(fleet.nodes) == 4,
+ 'I03 retention requires all stopped peers and released owned network/write gates')
+ reservations = [json.loads(path.read_text()) for path in fleet.evidence.glob('*-reserved.json')]
+ require(len(reservations) == 4 and {row.get('node') for row in reservations} == set(fleet.nodes)
+ and all(row.get('reserved') is True for row in reservations), 'I03 retention lacks its original reservations')
+ plan = {'kind': 'unresolved-network-load', 'runId': fleet.run_id, 'nodes': list(fleet.nodes),
+ 'candidateCommit': binding['candidateCommit'], 'harnessCommit': binding['harnessCommit'],
+ 'binarySha256': binding['candidateBinarySha256'], 'bucket': binding['bucketPrefix'], 'mpuKeys': list(keys),
+ 'owners': {row['node']: row['owner'] for row in reservations},
+ 'businessSceneSha256': {node: digest(value) for node, value in physical_content(business).items()},
+ 'mpuSceneSha256': {node: value['sceneSha256'] for node, value in mpu_nodes.items()}}
+ validate_plan(plan)
+ for node in fleet.nodes:
+ scene, mpu = business[node], mpu_nodes[node]
+ require(scene.get('quiescent') is True and type(scene.get('pid')) is int and scene['pid'] == 0
+ and scene.get('unit') == plan['owners'][node]['unit']
+ and scene.get('binarySha256') == plan['binarySha256']
+ and len(scene['formats']) == 4 and mpu.get('keys') == keys
+ and mpu.get('bucket') == plan['bucket'] and mpu.get('runId') == fleet.run_id
+ and mpu.get('binarySha256') == plan['binarySha256'] and mpu.get('quiescent') is True
+ and mpu.get('readOnly') is True and mpu.get('originalPhysicalDrives') == 4
+ and digest({field: mpu[field] for field in ('bucket', 'keys', 'scopes', 'directories', 'files', 'bytesHashed')})
+ == mpu['sceneSha256'], 'I03 retention lacks its stopped original business/MPU capture')
+ return fleet.record('network-retention-intent', copy.deepcopy(plan))
+
+
+def isolate(fleet, plan):
+ """Retry the same archive intent; never recover a shard, Abort or delete data."""
+ validate_plan(plan)
+ require(fleet.run_id == plan['runId'] and list(fleet.nodes) == plan['nodes'], 'I03 retention controller changed its original fleet')
+ def params(node):
+ return {'expectedOwner': plan['owners'][node], 'expectedOriginalSceneSha256': plan['businessSceneSha256'][node],
+ 'expectedMpuSceneSha256': plan['mpuSceneSha256'][node], 'mpuKeys': plan['mpuKeys']}
+ audits = {}
+ for node in fleet.nodes:
+ audits[node] = fleet.record('network-retention-preflight', {'node': node,
+ **fleet.remote(node, 'retained-scene-audit', **params(node))})
+ require(audits[node].get('inspectionOnly') is True and audits[node].get('mutated') is False
+ and audits[node].get('credentialContentsRead') is False,
+ 'I03 retention preflight did not preserve read-only credential boundaries')
+ for node in fleet.nodes:
+ result = fleet.record('network-retention-isolation', {'node': node,
+ **fleet.remote(node, 'retained-scene-isolate', expectedBaseline=audits[node]['baseline'], **params(node))})
+ require(result.get('activeNamespaceIsolated') is True and result.get('baseline') == audits[node]['baseline']
+ and result.get('originalSceneRestored') is False and result.get('backupRestorationUsed') is False
+ and result.get('canonicalPassClaimed') is False and result.get('credentialContentsRead') is False,
+ 'I03 retained scene move did not preserve the exact original failure')
+ for node in fleet.nodes:
+ proof = fleet.record('network-retention-post-audit', {'node': node,
+ **fleet.remote(node, 'retained-scene-audit', **params(node))})
+ require(proof.get('inspectionOnly') is True and proof.get('mutated') is False
+ and proof.get('activeNamespaceIsolated') is True and proof.get('baseline') == audits[node]['baseline']
+ and len(proof.get('privateArchiveParents', [])) == 5
+ and all(row.get('exists') is True and row.get('uid') == 0 and row.get('mode') == 0o700
+ for row in proof['privateArchiveParents']), 'I03 retained scene post-audit drifted')
+ return {'runId': plan['runId'], 'cleanup': 'ISOLATED', 'activeNamespaceIsolated': True,
+ 'originalSceneRestored': False, 'originalEvidenceComplete': False, 'originalVerdictRewritten': False,
+ 'canonicalPassClaimed': False, 'namespaceDeletionPerformed': False, 'intentSha256': digest(plan)}
+
+
+def archived_plan(artifact, failed_run, candidate):
+ """Select one retained branch; an isolated namespace is never already-clean."""
+ case = 'heal.network-partition-and-lock-quorum'
+ results = list(artifact.glob(f'*/run/{case}/cases/{case}.json'))
+ require(type(failed_run) is int and failed_run > 0 and len(results) == 1, 'I03 isolation needs one exact failed run')
+ result = json.loads(results[0].read_text())
+ provenance = result.get('provenance') or {}
+ require(result.get('case_id') == case and result.get('verdict') == 'FAIL' and result.get('cleanup_status') == 'FAIL'
+ and provenance.get('source_commit') == candidate['source_sha']
+ and provenance.get('candidate_sha256') == candidate['package_sha256'],
+ 'I03 isolation failed-result candidate or cleanup differs')
+ evidence = results[0].parents[1] / 'evidence' / case
+ summary = json.loads((evidence / 'healthy-summary.json').read_text())
+ retained = [row for row in summary['results'] if row.get('cleanup') != 'PASS']
+ require(summary.get('cleanup') == 'FAIL' and len(retained) == 1
+ and retained[0].get('verdict') == 'FAIL' and retained[0].get('topology') == 'H2'
+ and retained[0].get('branch') in {'minority-partition', 'read-boundary', 'write-boundary', 'lock-boundary', 'rejoin'},
+ 'I03 isolation must preserve exactly one failed branch')
+ folder = evidence / 'H2' / retained[0]['branch']
+ def one(label):
+ paths = list(folder.glob('*-' + label + '.json'))
+ require(len(paths) == 1, 'I03 isolation lacks its original ' + label)
+ return json.loads(paths[0].read_text())
+ plan, binding = one('network-retention-intent'), one('binding')
+ validate_plan(plan)
+ require(re.fullmatch(rf'gh-{failed_run}-[1-9][0-9]*-h2', plan['runId'])
+ and plan['runId'] == binding.get('runId') and plan['bucket'] == binding.get('bucketPrefix')
+ and plan['candidateCommit'] == binding.get('candidateCommit') == candidate['source_sha']
+ and plan['harnessCommit'] == binding.get('harnessCommit') == provenance.get('testing_commit')
+ and plan['binarySha256'] == binding.get('candidateBinarySha256') == provenance.get('binary_sha256')
+ and binding.get('candidatePackageSha256') == candidate['package_sha256'],
+ 'I03 isolation original plan/binding/provenance differs')
+ reservations = [json.loads(path.read_text()) for path in folder.glob('*-reserved.json')]
+ require(len(reservations) == 4 and {row.get('node') for row in reservations} == set(plan['nodes'])
+ and all(row.get('reserved') is True and row.get('owner') == plan['owners'][row['node']] for row in reservations),
+ 'I03 isolation owner differs from its original reservation')
+ return [plan]
diff --git a/scripts/validation_heal/node_outage.py b/scripts/validation_heal/node_outage.py
new file mode 100644
index 00000000..d026390f
--- /dev/null
+++ b/scripts/validation_heal/node_outage.py
@@ -0,0 +1,316 @@
+#!/usr/bin/env python3
+"""B09: acknowledged degraded I/O and physical MRF repair on the current H2 fleet."""
+
+import argparse
+import copy
+from datetime import UTC, datetime, timedelta
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import secrets
+import signal
+import sys
+import time
+from urllib import request
+from urllib.error import URLError
+
+try:
+ from .heal_probe import Probe, compare_inventories, payload, require, save
+ from .heal_workload import Recorder, seed, verify_seed
+ from .healthy import Fleet, physical_content, validate_topology
+ from .inspector import verify as verify_inspector
+ from .matrix import select
+ from .shards import SIZES, files, map_target, run_task
+ from .versions import public_version_id, validate_version_receipts, verify_quorum_contract
+except ImportError:
+ from heal_probe import Probe, compare_inventories, payload, require, save
+ from heal_workload import Recorder, seed, verify_seed
+ from healthy import Fleet, physical_content, validate_topology
+ from inspector import verify as verify_inspector
+ from matrix import select
+ from shards import SIZES, files, map_target, run_task
+ from versions import public_version_id, validate_version_receipts, verify_quorum_contract
+
+
+BRANCHES = {"one-node-stop": "DB", "rejoin-with-new-versions": "DV"}
+SIZE = 1024**2 + 47
+
+
+def quorum_plan(target, offline_node, contract):
+ require(contract.get("recognized") is True and contract.get("writeQuorumRule") == "data-plus-one-if-equal-parity",
+ "node outage requires recognized candidate read/write quorum policies")
+ erasure = target["logical"]["erasure"]
+ data, parity = erasure["data_blocks"], erasure["parity_blocks"]
+ lost = [shard for shard in target["cohort"] if shard["node"] == offline_node]
+ remaining = data + parity - len(lost)
+ write = data + int(data == parity)
+ require(len(lost) == 4 and len(target["cohort"]) == 16 and data + parity == 16
+ and len({shard["node"] for shard in target["cohort"]}) == 4
+ and remaining >= max(data, write), "selected node outage cannot admit the degraded read/write workload")
+ return {"offlineNode": offline_node, "offlineDiskUUIDs": [shard["diskUUID"] for shard in lost],
+ "lostSameSetDrives": len(lost), "remainingSameSetDrives": remaining,
+ "readQuorum": data, "writeQuorum": write, "policy": contract["writeQuorumRule"],
+ "distributedLockAdmission": "each successful write ACK also proves its actual lock admission"}
+
+
+def put_ack(expected, key, body, response, fixture):
+ version = response.get("VersionId", "null")
+ require(fixture != "DV" or version not in {None, "null", ""}, "degraded versioned PUT omitted its version identity")
+ require(fixture != "DB" or version in {None, "null"}, "never-versioned PUT unexpectedly created a version")
+ version = public_version_id(version)
+ result = copy.deepcopy(expected)
+ if fixture == "DB": result = [entry for entry in result if entry["key"] != key]
+ else:
+ require(not any(entry["key"] == key and entry["versionId"] == version for entry in result),
+ "degraded PUT reused an acknowledged version")
+ for entry in result:
+ if entry["key"] == key: entry["isLatest"] = False
+ result.append({"key": key, "versionId": version, "deleteMarker": False, "isLatest": True,
+ "payload": {"size": len(body), "sha256": hashlib.sha256(body).hexdigest()}})
+ return result
+
+
+def delete_ack(expected, key, response, fixture, *, version=None):
+ result = copy.deepcopy(expected)
+ if version is not None:
+ require(response.get("VersionId") == version and response.get("DeleteMarker") in {None, False},
+ "explicit data-version DELETE did not acknowledge the exact identity")
+ require(any(entry["key"] == key and entry["versionId"] == version and not entry["isLatest"] for entry in result),
+ "historical delete would change the expected current identity")
+ return [entry for entry in result if (entry["key"], entry["versionId"]) != (key, version)]
+ if fixture == "DB":
+ require(response.get("DeleteMarker") in {None, False} and response.get("VersionId") in {None, "null"},
+ "never-versioned DELETE returned a marker or foreign version")
+ return [entry for entry in result if entry["key"] != key]
+ marker = response.get("VersionId")
+ require(response.get("DeleteMarker") is True and marker not in {None, "", "null"}
+ and not any(entry["key"] == key and entry["versionId"] == marker for entry in result),
+ "degraded marker DELETE omitted or reused its acknowledged identity")
+ for entry in result:
+ if entry["key"] == key: entry["isLatest"] = False
+ result.append({"key": key, "versionId": marker, "deleteMarker": True, "isLatest": True})
+ return result
+
+
+def workload(recorder, bucket, expected, fixture):
+ manifest = copy.deepcopy(expected)
+ for index in range(6):
+ key = f"offline/new-{index:02d}.bin"
+ body = payload(f"{bucket}/{key}/offline", SIZE + index)
+ response = recorder.call("put_object", {"Bucket": bucket, "Key": key,
+ "Metadata": {"heal-fixture": "offline-ack"}, "Tagging": "suite=heal&fixture=offline"}, body)
+ manifest = put_ack(manifest, key, body, response, fixture)
+ for index in (1, 3):
+ key = f"objects/{index:04d}.bin"
+ body = payload(f"{bucket}/{key}/offline-overwrite", SIZE + index)
+ response = recorder.call("put_object", {"Bucket": bucket, "Key": key,
+ "Metadata": {"heal-fixture": "offline-overwrite"}, "Tagging": "suite=heal&fixture=offline"}, body)
+ manifest = put_ack(manifest, key, body, response, fixture)
+ key = "objects/0002.bin" if fixture == "DV" else "objects/0006.bin"
+ response = recorder.call("delete_object", {"Bucket": bucket, "Key": key})
+ manifest = delete_ack(manifest, key, response, fixture)
+ if fixture == "DV":
+ old = next(entry for entry in expected if entry["key"] == "objects/0001.bin" and not entry["isLatest"])
+ response = recorder.call("delete_object", {"Bucket": bucket, "Key": old["key"], "VersionId": old["versionId"]})
+ manifest = delete_ack(manifest, old["key"], response, fixture, version=old["versionId"])
+ for entry in [entry for entry in manifest if not entry["deleteMarker"]][:6]:
+ observed = recorder.probe.s3("get_object", {"Bucket": bucket, "Key": entry["key"], "VersionId": entry["versionId"]})
+ # Archive failures as well as complete bodies' hashes before checking.
+ save(recorder.output / ("offline-get-" + hashlib.sha256((entry["key"] + entry["versionId"]).encode()).hexdigest()[:20] + ".json"), observed)
+ require(observed.get("acknowledged") and observed.get("payload") == entry["payload"],
+ "degraded historical GET differs from acknowledged bytes")
+ recorder.call("head_object", {"Bucket": bucket, "Key": entry["key"], "VersionId": entry["versionId"]})
+ return manifest
+
+
+def replica_census(snapshot, bucket, expected, offline_snapshot, *, topology="H2"):
+ validate_topology(snapshot, topology)
+ original = files(offline_snapshot)
+ wanted = {}
+ for entry in expected:
+ wanted.setdefault(entry["key"], set()).add((entry["versionId"], entry["deleteMarker"], entry["isLatest"]))
+ for node, value in snapshot.items():
+ for disk in range(4):
+ observed = {}
+ for item in value["files"]:
+ if item["disk"] != disk: continue
+ metadata = item.get("metadata")
+ if metadata:
+ require(metadata.get("bucket") == bucket, "metadata escaped the run bucket")
+ versions = {(public_version_id(version.get("versionId")), version["deleted"], version["isLatest"])
+ for version in metadata["versions"]}
+ if versions: observed[metadata["key"]] = versions
+ require(observed == wanted, "rejoined replica lacks or resurrected an acknowledged version/marker")
+ targets = []
+ for entry in expected:
+ if entry["deleteMarker"]: continue
+ target = map_target(snapshot, bucket, entry["key"], "data", 1, entry["versionId"], latest_required=False)
+ for shard in target["cohort"]:
+ previous = original.get((shard["node"], shard["disk"], shard["relativePath"]))
+ require(previous is None or (shard["sha256"] == previous["sha256"] and shard["size"] == previous["size"]),
+ "rejoin changed an authoritative surviving shard")
+ targets.append(target)
+ return targets
+
+
+def validate_deleted_read(observed, version):
+ response = observed.get("response") or {}
+ require(observed.get("acknowledged") is False and observed.get("resultUnknown") is False and "payload" not in observed
+ and response.get("ResponseMetadata", {}).get("HTTPStatusCode") == 404
+ and response.get("Error", {}).get("Code") == ("NoSuchKey" if version == "null" else "NoSuchVersion"),
+ "rejoined endpoint returned a deleted identity or an unrecognized refusal")
+
+
+def run_branch(args, branch, nodes):
+ fixture = BRANCHES[branch]
+ directory = args.evidence / branch
+ directory.mkdir()
+ run_id = f"gh-{os.environ['GITHUB_RUN_ID']}-{os.environ.get('GITHUB_RUN_ATTEMPT', '1')}-h2"
+ fleet = Fleet(nodes, directory, run_id, args.tool_directory, runtime_profile="offline-replay")
+ candidate = json.loads(args.candidate.read_text())
+ tool = fleet.record("inspector-build", verify_inspector(args.tool_directory, candidate))
+ contract = verify_quorum_contract(tool, candidate)
+ bucket = "rv-heal-" + hashlib.sha256(run_id.encode()).hexdigest()[:16]
+ access, secret = secrets.token_hex(16), secrets.token_hex(24)
+ os.environ.update(HEAL_ACCESS_KEY=access, HEAL_SECRET_KEY=secret)
+ binding = {"schemaVersion": 1, "runId": run_id, "jumpHostOnly": True, "exclusiveHealService": True,
+ "leaseExpiresAt": (datetime.now(UTC) + timedelta(minutes=30)).isoformat(), "bucketPrefix": bucket,
+ "endpoints": {f"node{index}": f"http://rustfs-node{index}:9000" for index in range(1, 5)},
+ "defaultEndpoint": "node1", "candidateCommit": candidate["source_sha"], "harnessCommit": args.testing_commit,
+ "candidatePackageSha256": candidate["package_sha256"], "candidateBinarySha256": args.binary_sha,
+ "rcBinarySha256": hashlib.sha256(args.rc_bin.read_bytes()).hexdigest(),
+ "maxInventoryObjects": 512, "maxInventoryBytes": 512 * 1024**2}
+ fleet.record("binding", binding)
+ probe, active = Probe(binding), []
+ result = {"topology": "H2", "branch": branch, "fixture": fixture, "verdict": "RUNNING", "cleanup": "PENDING"}
+ try:
+ require(contract.get("writeQuorumRule") == "data-plus-one-if-equal-parity",
+ "node outage needs a fresh recognized candidate write-quorum contract")
+ fleet.deploy(args.binary_sha, bucket, access, secret)
+ recorder = Recorder(probe, directory / "fixture")
+ expected = seed(recorder, bucket, fixture, 128 if fixture == "DB" else 4, [SIZE])
+ initial = fleet.record("s3-baseline", probe.inventory(bucket, unversioned_current_tags=fixture == "DB"))
+ fleet.record("fixture-manifest", {"expected": expected, **verify_seed(expected, initial)})
+ before = fleet.snapshot("physical-baseline")
+ offline_node = nodes[-1]
+ target = map_target(before, bucket, "objects/0001.bin", "data", 1,
+ next(entry["versionId"] for entry in expected if entry["key"] == "objects/0001.bin" and entry["isLatest"]))
+ fleet.record("quorum-fault-plan", quorum_plan(target, offline_node, contract))
+ stop = fleet.record("node-stopped", fleet.remote(offline_node, "stop"))
+ require(stop.get("quiescent") is True and stop.get("service", {}).get("MainPID") == "0", "node stop lacks process exit")
+ opener = request.build_opener(request.ProxyHandler({}))
+ try:
+ with opener.open(binding["endpoints"]["node4"] + "/health/ready", timeout=3):
+ raise ValueError("stopped node endpoint still accepts requests")
+ except URLError as exc:
+ require(isinstance(exc.reason, ConnectionRefusedError), "node outage was not a witnessed connection refusal")
+ fleet.record("node-endpoint-refused", {"endpoint": "node4", "connectionRefused": True})
+ frozen_offline = fleet.record("offline-node-baseline", fleet.remote(offline_node, "snapshot", quiescent=True))
+ expected = workload(recorder, bucket, expected, fixture)
+ acknowledged = fleet.record("s3-degraded-ack-manifest", probe.inventory(bucket, unversioned_current_tags=fixture == "DB"))
+ fleet.record("degraded-full-proof", {"expected": expected, **verify_seed(expected, acknowledged)})
+ previous = {(entry["key"], entry["versionId"]): entry for entry in initial["entries"]}
+ retained = {(entry["key"], entry["versionId"]) for entry in acknowledged["entries"]}
+ for entry in acknowledged["entries"]:
+ old = previous.get((entry["key"], entry["versionId"]))
+ if old and not entry["deleteMarker"] and old.get("payload") == entry.get("payload"):
+ require(old["properties"] == entry["properties"] and old["tags"] == entry["tags"],
+ "degraded I/O changed an untouched version's public metadata or tags")
+ still_offline = fleet.record("offline-node-after-acks", fleet.remote(offline_node, "snapshot", quiescent=True))
+ require(physical_content({offline_node: frozen_offline}) == physical_content({offline_node: still_offline}),
+ "offline node received writes before it rejoined")
+ authorities = fleet.record("physical-offline-authorities", {node: fleet.remote(node, "snapshot", quiescent=node == offline_node) for node in nodes})
+ fleet.record("node-rejoined", fleet.remote(offline_node, "start"))
+ fleet.wait_ready()
+ fleet.record("trigger", {"source": "MRF", "manualTasksBeforePhysicalRepair": False,
+ "targetS3ReadsBeforePhysicalRepair": False, "budgetSeconds": 180})
+ deadline, last_error = time.monotonic() + 180, None
+ while True:
+ scene = fleet.snapshot("physical-mrf-progress")
+ try:
+ targets = replica_census(scene, bucket, expected, authorities)
+ break
+ except ValueError as exc:
+ last_error = str(exc)
+ require(time.monotonic() < deadline, "MRF physical rejoin deadline exceeded: " + last_error)
+ time.sleep(2)
+ fleet.stop()
+ repaired = fleet.snapshot("physical-repaired-quiescent", quiescent=True)
+ targets = replica_census(repaired, bucket, expected, authorities)
+ proofs = []
+ for target in targets:
+ for shard in target["cohort"]:
+ observed = fleet.record("rejoined-streaming-bitrot", fleet.remote(shard["node"], "verify-part",
+ disk=shard["disk"], relativePath=shard["relativePath"], versionId=shard["versionId"], partNumber=1))
+ require(observed.get("physicalBitrotVerified") is True and observed["sha256"] == shard["sha256"]
+ and observed["physicalSize"] == shard["size"], "rejoined shard failed its exact committed checksum")
+ proofs.append({"node": shard["node"], "disk": shard["disk"], "key": target["key"], "versionId": shard["versionId"]})
+ fleet.start()
+ status = run_task(probe, fleet, {"bucket": bucket, "key": ""}, 2, active, option_overrides={"recursive": True})
+ responsibility = validate_version_receipts(status, bucket, expected, 2)
+ fleet.stop()
+ final = fleet.snapshot("physical-after-verifier", quiescent=True)
+ require(physical_content(repaired) == physical_content(final), "verification task unexpectedly repaired or changed the MRF result")
+ fleet.start()
+ for endpoint in binding["endpoints"]:
+ entry_probe = Probe(binding, endpoint)
+ current = fleet.record("s3-rejoined-" + endpoint, entry_probe.inventory(bucket, unversioned_current_tags=fixture == "DB"))
+ compare_inventories(acknowledged, current)
+ for key, version in set(previous) - retained:
+ params = {"Bucket": bucket, "Key": key}
+ if version != "null": params["VersionId"] = version
+ observed = fleet.record("deleted-identity-read-" + endpoint, entry_probe.s3("get_object", params))
+ validate_deleted_read(observed, version)
+ result.update(verdict="PASS", allNodeS3ManifestEqual=True, responsibility=responsibility,
+ physical={"allExpectedReplicaIdentities": True, "survivingShardsUnchanged": True,
+ "newTargetShardsVerified": True, "streamingShardProofs": len(proofs),
+ "backupRestorationUsed": False, "source": "MRF"})
+ except (Exception, KeyboardInterrupt) as exc:
+ result.update(verdict="FAIL", failure={"type": type(exc).__name__, "message": str(exc)})
+ if fleet.reserved:
+ try:
+ fleet.stop()
+ fleet.snapshot("physical-failure-scene", quiescent=True)
+ except Exception as capture:
+ fleet.record("failure-scene-error", {"message": str(capture)})
+ finally:
+ for path, token in active:
+ try: fleet.record("stop-unresolved", probe.admin(path, [("clientToken", token), ("forceStop", "true")], b""))
+ except Exception as exc: fleet.record("stop-error", {"message": str(exc)})
+ result["cleanup"] = "PASS" if fleet.cleanup() else "FAIL"
+ fleet.record("topology-result", result)
+ return result
+
+
+def main():
+ def interrupted(signum, frame): raise KeyboardInterrupt(f"node-outage workflow signal {signum}")
+ signal.signal(signal.SIGTERM, interrupted)
+ parser = argparse.ArgumentParser(description=__doc__)
+ for name in ("candidate", "rc-bin", "evidence", "tool-directory"): parser.add_argument("--" + name, type=Path, required=True)
+ parser.add_argument("--case-id", choices=("heal.node-outage-rejoin",), required=True)
+ parser.add_argument("--testing-commit", required=True)
+ parser.add_argument("--binary-sha", required=True)
+ args = parser.parse_args()
+ nodes = os.environ.get("RUSTFS_NODES", "vm000 vm001 vm002 vm003").split()
+ require(len(nodes) == 4 and len(set(nodes)) == 4 and all(re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", node) for node in nodes), "four existing Heal nodes required")
+ require(re.fullmatch(r"[0-9a-f]{64}", args.binary_sha) and re.fullmatch(r"[1-9][0-9]*", os.environ.get("GITHUB_RUN_ID", "")), "fixed workflow/candidate identities required")
+ require(list(BRANCHES) == select(args.case_id)[0]["variants"], "node-outage executor omitted a source branch")
+ results = []
+ try:
+ for branch in BRANCHES:
+ results.append(run_branch(args, branch, nodes))
+ if results[-1]["cleanup"] != "PASS": break
+ finally:
+ complete = len(results) == len(BRANCHES) and all(item["verdict"] == "PASS" and item["cleanup"] == "PASS" for item in results)
+ save(args.evidence / "healthy-summary.json", {"caseId": args.case_id, "results": results,
+ "cleanup": "PASS" if results and all(item["cleanup"] == "PASS" for item in results) else "FAIL", "complete": complete})
+ require(complete, "B09 requires both complete H2 branches; inspect topology-result evidence")
+
+
+if __name__ == "__main__":
+ try: main()
+ except Exception as exc:
+ print(f"Heal node outage validation failed: {exc}", file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_heal/nolock.py b/scripts/validation_heal/nolock.py
new file mode 100644
index 00000000..ce498b40
--- /dev/null
+++ b/scripts/validation_heal/nolock.py
@@ -0,0 +1,576 @@
+#!/usr/bin/env python3
+"""A09 normalized public races and separately bound native storage fences."""
+
+from __future__ import annotations
+
+import argparse
+import copy
+from concurrent.futures import ThreadPoolExecutor
+from datetime import datetime, timedelta, timezone
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import secrets
+import signal
+import sys
+import time
+from urllib.parse import quote
+import uuid
+
+try:
+ from .heal_probe import Probe, canonical, compare_inventories, payload, require, save
+ from .heal_workload import Recorder, verify_seed
+ from .healthy import BASE_OPTIONS, Fleet, physical_content, validate_topology
+ from .inspector import verify as verify_inspector
+ from .matrix import select
+ from .heal_fault import bounded_fault_id
+ from .shards import files, validate_repaired_physical
+ from .cursor import validate_missing_batch, verify_part_manifest
+ from .versions import public_version_id
+ from .race import COPY_CANDIDATE, COPY_REFERENCE, map_fixture as map_race_fixture
+ from .race_native_evidence import verify as verify_native
+ from .race_io import set_fleet as set_race_io_cap
+except ImportError:
+ from heal_probe import Probe, canonical, compare_inventories, payload, require, save
+ from heal_workload import Recorder, verify_seed
+ from healthy import BASE_OPTIONS, Fleet, physical_content, validate_topology
+ from inspector import verify as verify_inspector
+ from matrix import select
+ from heal_fault import bounded_fault_id
+ from shards import files, validate_repaired_physical
+ from cursor import validate_missing_batch, verify_part_manifest
+ from versions import public_version_id
+ from race import COPY_CANDIDATE, COPY_REFERENCE, map_fixture as map_race_fixture
+ from race_native_evidence import verify as verify_native
+ from race_io import set_fleet as set_race_io_cap
+
+
+CASE_ID = "heal.admin-nolock-safety"
+BRANCHES = ("overwrite-nolock", "delete-nolock", "native-lock-fence")
+BLACK_BOX = BRANCHES[:2]
+OBJECTS = 257
+SIZE = 257 * 1024 + 123
+TARGET_KEY = "objects/0256.bin"
+REFERENCE_KEY = "objects/0000.bin"
+REFERENCE = {
+ "candidateCommit": "072b57b37559fbe626aeda8abf61617fdcbdd926",
+ "sourceFiles": {
+ "rustfs/src/admin/handlers/heal.rs": "7e4007c29a8329ced8e48b88111400e79cffd845bd6ddc6243b79f3cd48e8199",
+ "crates/heal/src/heal/channel.rs": "7f669ca43b9559db9b97fba6b419a6c76a94d76acd8526db132d74b7c534d6b0"},
+ "requested": {**BASE_OPTIONS, "recursive": True, "nolock": True, "pool": None, "set": None},
+ "effective": {**BASE_OPTIONS, "recursive": True, "nolock": False, "pool": None, "set": None},
+ "nativeFence": "pinned release has no reviewed e2e-test-hooks artifact or exact Admin lock/commit barrier",
+}
+NATIVE_REFERENCE = {**REFERENCE, 'candidateCommit': COPY_CANDIDATE,
+ 'sourceFiles': dict(COPY_REFERENCE['sourceFiles']),
+ 'nativeFence': 'exact-source default Deep exclusive owner fences queued PUT and exact historical DELETE'}
+NATIVE_GROUP = ['default-heal-exclusive-owner']
+
+
+def source_contract(candidate):
+ for reference in (REFERENCE, NATIVE_REFERENCE):
+ if candidate.get('source_sha') == reference['candidateCommit']:
+ return reference
+ raise ValueError('A09 candidate contract is not reviewed')
+
+
+def native_preflight(candidate, testing_commit, directory):
+ reference = source_contract(candidate)
+ if reference is REFERENCE:
+ return None
+ require(bool(directory), 'A09 requires exact-source native proof before fleet mutation')
+ proof = verify_native(directory, candidate, testing_commit, CASE_ID)
+ require(proof['requiredRegressionLabels'] == NATIVE_GROUP and proof['requiredNativeSubsetPassed'] is True
+ and proof['wholeCasePassClaimed'] is False and proof['canonicalCasePassClaimed'] is False,
+ 'A09 exact default-owner native group is incomplete')
+ return proof
+
+
+class WindowNotObserved(ValueError):
+ """No observable client overlap; this is not a product success."""
+
+
+class RaceRecorder(Recorder):
+ """Keep unresolved mutations and exact SDK receipts after any assertion."""
+ def __init__(self, *args, versioned, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.versioned, self.pending, self.seen = versioned, None, set()
+
+ def call(self, operation, params, body=None, **kwargs):
+ self.pending = {"operation": operation, "params": copy.deepcopy(params)}
+ result = super().call(operation, params, body, **kwargs)
+ version = result.get("VersionId")
+ if operation == "put_object":
+ require(isinstance(result.get("ETag"), str) and result["ETag"]
+ and result.get("DeleteMarker") in {None, False}, "A09 PUT ACK lacks ETag or changed kind")
+ if self.versioned:
+ require(isinstance(version, str) and str(uuid.UUID(version)) == version and uuid.UUID(version).int
+ and (params["Key"], version) not in self.seen, "A09 PUT ACK has an invalid/reused VersionId")
+ self.seen.add((params["Key"], version))
+ else:
+ require(version in {None, "null"}, "A09 never-versioned PUT created a real version")
+ elif operation == "delete_object":
+ require(self.versioned and params.get("VersionId") in {
+ value for key, value in self.seen if key == params["Key"]}
+ and version == params["VersionId"] and result.get("DeleteMarker") in {None, False},
+ "A09 DELETE ACK changed the exact known data-version identity")
+ self.pending = None
+ return result
+
+
+def settings(observed, reference=REFERENCE):
+ require(observed.get("httpStatus") == 200 and isinstance(observed.get("response"), dict),
+ "A09 status has an unknown or failed HTTP response")
+ value = observed["response"]
+ require(value.get("summary") in {"running", "finished"}, "A09 original task disappeared, stopped or failed")
+ # Canonical JSON distinguishes bool/int and preserves raw null selectors.
+ require(canonical(value.get("settings")) == canonical(reference["effective"]),
+ "A09 Admin nolock was not safely normalized or changed unrelated options")
+ return value["settings"]
+
+
+def sample_overlap(ack, observed, path, token, reference=REFERENCE):
+ foreground = ack.get("observation") or {}
+ settings(observed, reference)
+ value = observed["response"]
+ a, z = foreground.get("callStartedMonotonicNs"), foreground.get("callFinishedMonotonicNs")
+ b, c = observed.get("callStartedMonotonicNs"), observed.get("callFinishedMonotonicNs")
+ processed = (value.get("outcome") or {}).get("counters", {}).get("processed")
+ return (foreground.get("acknowledged") is True and foreground.get("clientCallInvoked") is True
+ and foreground.get("clockDomain") and foreground["clockDomain"] == observed.get("clockDomain")
+ and observed.get("path") == path and [tuple(row) for row in observed.get("query", [])] == [("clientToken", token)]
+ and all(type(tick) is int for tick in (a, z, b, c)) and 0 < a < b < c < z
+ and value["summary"] == "running" and type(processed) is int and processed > 0)
+
+
+def seed_fixture(recorder, bucket, branch, *, count=OBJECTS):
+ require(branch in BLACK_BOX and type(count) is int and 3 <= count <= OBJECTS, "A09 unregistered fixture")
+ recorder.call("create_bucket", {"Bucket": bucket})
+ if branch == "delete-nolock":
+ recorder.call("put_bucket_versioning", {"Bucket": bucket, "VersioningConfiguration": {"Status": "Enabled"}})
+ entries = []
+ keys = [f"objects/{index:04d}.bin" for index in range(count - 1)] + [TARGET_KEY]
+ for key in keys:
+ versions = 3 if key == TARGET_KEY and branch == "delete-nolock" else 1
+ for generation in range(versions):
+ nonce = f"{bucket}/{key}/{generation}"
+ body = payload(nonce, SIZE)
+ response = recorder.call("put_object", {"Bucket": bucket, "Key": key,
+ "ContentType": "application/octet-stream", "Metadata": {"heal-fixture": "A09", "nonce": nonce},
+ "Tagging": "suite=heal&fixture=A09"}, body)
+ entries.append({"key": key, "versionId": public_version_id(response.get("VersionId")),
+ "isLatest": generation == versions - 1, "deleteMarker": False,
+ "payload": {"size": len(body), "sha256": hashlib.sha256(body).hexdigest()}})
+ return entries
+
+
+def operation_plan(bucket, branch, expected):
+ require(branch in BLACK_BOX, "A09 unregistered foreground branch")
+ if branch == "overwrite-nolock":
+ # Three distinct serial commits; the final independent reference was
+ # written before injection, allowing a coding-index byte comparison.
+ return [{"operation": "put_object", "params": {"Bucket": bucket, "Key": TARGET_KEY,
+ "ContentType": "application/octet-stream", "Metadata": {"nonce": str(index)},
+ "Tagging": "race=overwrite"},
+ "nonce": f"{bucket}/overwrite/{index}" if index < 2 else f"{bucket}/{REFERENCE_KEY}/0"}
+ for index in range(3)]
+ target = [row for row in expected if row["key"] == TARGET_KEY]
+ require(len(target) == 3 and len({row["versionId"] for row in target}) == 3
+ and target[-1]["isLatest"], "A09 DV lacks three ACK-ordered versions")
+ return [{"operation": "delete_object", "params": {"Bucket": bucket, "Key": TARGET_KEY,
+ "VersionId": target[0]["versionId"]}}]
+
+
+def final_inventory(baseline, branch, plan, responses):
+ require(len(plan) == len(responses) == (3 if branch == "overwrite-nolock" else 1),
+ "A09 foreground plan/ACK sequence is incomplete")
+ result = copy.deepcopy(baseline)
+ if branch == "overwrite-nolock":
+ entry = next(row for row in result["entries"] if row["key"] == TARGET_KEY)
+ step, ack = plan[-1], responses[-1]
+ body = payload(step["nonce"], SIZE)
+ require(isinstance(ack.get("ETag"), str) and ack["ETag"]
+ and ack.get("VersionId") in {None, "null"}, "A09 last PUT ACK lost its identity")
+ entry.update(payload={"size": len(body), "sha256": hashlib.sha256(body).hexdigest()},
+ tags=[{"Key": "race", "Value": "overwrite"}])
+ entry["properties"].update(ETag=ack["ETag"], ContentLength=len(body), ContentType="application/octet-stream",
+ Metadata=step["params"]["Metadata"])
+ else:
+ version = plan[0]["params"]["VersionId"]
+ require(responses[0].get("VersionId") == version, "A09 DELETE response changed the selected VersionId")
+ result["entries"] = [row for row in result["entries"]
+ if (row["key"], row["versionId"]) != (TARGET_KEY, version)]
+ require(len(result["entries"]) + 1 == len(baseline["entries"]), "A09 deleted a foreign/missing version")
+ return result
+
+
+def object_scene(snapshot, bucket, key):
+ return {node: {**value, "files": [row for row in value["files"]
+ if row["path"].startswith(f"{bucket}/{key}/")]}
+ for node, value in snapshot.items()}
+
+
+def map_fixture(snapshot, bucket, expected):
+ mapped = map_race_fixture(snapshot, bucket, expected)
+ require(len(mapped) == len(expected), "A09 physical map omitted/duplicated an ACK")
+ return mapped
+
+
+def physical_transition(before, after, bucket, mapped, final, branch):
+ """Prove current ACK authority before allowing any GET or backup rescue."""
+ require(all(before[node]["formats"] == after[node]["formats"] for node in before),
+ "A09 race changed an original disk format")
+ known = {row["key"] for row in final["entries"]}
+ require(all(any(row["path"].startswith(f"{bucket}/{key}/") for key in known)
+ for value in after.values() for row in value["files"]), "A09 created an unacknowledged physical key")
+ for (key, version), target in mapped.items():
+ if key == TARGET_KEY: continue
+ validate_repaired_physical(object_scene(before, bucket, key), object_scene(after, bucket, key), target)
+ survivors = map_fixture(after, bucket, final["entries"])
+ original_target = [value for (key, _), value in mapped.items() if key == TARGET_KEY]
+ current_target = [value for (key, _), value in survivors.items() if key == TARGET_KEY]
+ if branch == "overwrite-nolock":
+ require(len(original_target) == len(current_target) == 1
+ and original_target[0]["dataDir"] != current_target[0]["dataDir"],
+ "A09 overwrite retained the old physical generation")
+ reference = next(target for (key, _), target in mapped.items() if key == REFERENCE_KEY)
+ require({part["index"]: (part["sha256"], part["size"]) for part in reference["cohort"]}
+ == {part["index"]: (part["sha256"], part["size"]) for part in current_target[0]["cohort"]},
+ "A09 last ACK shard bytes/coding indices differ from the independent payload reference")
+ else:
+ remaining = {(row["key"], row["versionId"]) for row in final["entries"]}
+ removed = [value for pair, value in mapped.items() if pair not in remaining]
+ require(len(removed) == 1, "A09 delete transition lacks one exact deleted version")
+ for target in current_target:
+ original = mapped[TARGET_KEY, target["versionId"]]
+ require(target["logical"] == original["logical"]
+ and [(row["index"], row["sha256"], row["size"]) for row in target["cohort"]]
+ == [(row["index"], row["sha256"], row["size"]) for row in original["cohort"]],
+ "A09 delete changed another retained ACK generation")
+ current_dirs = {target["dataDir"] for target in current_target}
+ for value in after.values():
+ for row in value["files"]:
+ prefix = f"{bucket}/{TARGET_KEY}/"
+ if not row["path"].startswith(prefix): continue
+ suffix = row["path"][len(prefix):]
+ require(suffix == "xl.meta" or suffix.split("/")[0] in current_dirs,
+ "A09 resurrected an old/deleted data directory or left an unbound target leaf")
+ return survivors, {"allControlShardsRestored": True, "lastACKGenerationVerified": True,
+ "physicalProofBeforeGETOrRescue": True, "oldVersionResurrected": False}
+
+
+def terminal_proof(status, bucket, before, final):
+ outcome = status.get("outcome") or {}
+ require(status.get("summary") == "finished" and outcome.get("execution", {}).get("state") == "completed"
+ and outcome.get("coverage") == "complete", "A09 task lacks complete successful terminal coverage")
+ old = {(row["key"], row["versionId"]) for row in before["entries"]}
+ new = {(row["key"], row["versionId"]) for row in final["entries"]}
+ counters = outcome.get("counters") or {}
+ require(all(type(counters.get(key)) is int and counters[key] >= 0
+ for key in ("processed", "healed", "unchanged", "failed", "unknown", "skipped", "attemptFailures"))
+ and all(counters[key] == 0 for key in ("failed", "unknown", "skipped", "attemptFailures"))
+ and counters.get("overflowed") is False and counters["processed"] == counters["healed"] + counters["unchanged"]
+ and len(old & new) <= counters["processed"] <= len(old | new),
+ "A09 terminal responsibility exceeds ACK bounds or hides unknown/failed work")
+ receipts = outcome.get("objects")
+ require(isinstance(receipts, list) and 0 < len(receipts) <= 128
+ and outcome.get("objectsTruncated") is (counters["processed"] > 128),
+ "A09 confused truncated diagnostics with its independent complete ACK manifest")
+ seen, incarnation = set(), None
+ for receipt in receipts:
+ identity = receipt.get("identity") or {}
+ pair = identity.get("object"), public_version_id(identity.get("versionId"))
+ require(identity.get("bucket") == bucket and pair in old | new and pair not in seen
+ and type(identity.get("poolIndex")) is int and identity["poolIndex"] == 0
+ and type(identity.get("setIndex")) is int and identity["setIndex"] == 0
+ and isinstance(identity.get("bucketIncarnationId"), str) and identity["bucketIncarnationId"]
+ and receipt.get("disposition", {}).get("state") in {"repaired", "verified_healthy"},
+ "A09 diagnostics changed version/incarnation/scope or claimed unknown/false health")
+ incarnation = incarnation or identity["bucketIncarnationId"]
+ require(incarnation == identity["bucketIncarnationId"], "A09 diagnostics mix incarnations")
+ seen.add(pair)
+ return {"minimumACKResponsibilities": len(old & new), "maximumACKResponsibilities": len(old | new),
+ "counters": counters, "exactRaceEnumerationClaimed": False}
+
+
+def observe(probe, fleet, path, token, reference=REFERENCE):
+ value = fleet.record("original-token-status", probe.admin(path, [("clientToken", token)], b""))
+ settings(value, reference)
+ return value
+
+
+def wait_running(probe, fleet, path, token, total, reference=REFERENCE):
+ deadline = time.monotonic() + 30
+ while True:
+ observed = observe(probe, fleet, path, token, reference)
+ value = observed["response"]
+ processed = (value.get("outcome") or {}).get("counters", {}).get("processed")
+ if value["summary"] == "finished":
+ raise WindowNotObserved("A09 task finished before its partial-progress race window")
+ if type(processed) is int and 0 < processed < total: return observed
+ if time.monotonic() >= deadline:
+ raise WindowNotObserved("A09 nonzero partial progress was not observed within 30 seconds")
+ time.sleep(.05)
+
+
+def wait_terminal(probe, fleet, path, token, reference=REFERENCE):
+ deadline = time.monotonic() + 240
+ while True:
+ current = observe(probe, fleet, path, token, reference)
+ if current["response"]["summary"] == "finished": return current["response"]
+ require(time.monotonic() < deadline, "A09 original task exceeded its terminal budget")
+ time.sleep(.2)
+
+
+def foreground(recorder, plan):
+ responses = []
+ for step in plan:
+ body = payload(step["nonce"], SIZE) if step["operation"] == "put_object" else None
+ responses.append(recorder.call(step["operation"], step["params"], body))
+ return responses
+
+
+def race(recorder, plan, probe, fleet, path, token, reference=REFERENCE):
+ samples = []
+ # Only the worker mutates Recorder state. All Fleet.record writes run on
+ # the controller, so evidence sequence allocation never races.
+ with ThreadPoolExecutor(max_workers=1) as pool:
+ worker = pool.submit(foreground, recorder, plan)
+ deadline = time.monotonic() + 120
+ try:
+ while not worker.done() and time.monotonic() < deadline:
+ samples.append(observe(probe, fleet, path, token, reference))
+ time.sleep(.01)
+ except BaseException:
+ recorder.mutations.poison("status observation failed; drain in-flight mutations only")
+ raise
+ if not worker.done(): recorder.mutations.poison("foreground deadline; drain only")
+ responses = worker.result(timeout=60)
+ acks = [json.loads(path.read_text()) for path in sorted(recorder.output.glob("*-ack.json"))]
+ require(len(acks) == len(plan) and all(row.get("operationCheck") == "PASS" for row in acks),
+ "A09 lacks every durable foreground ACK")
+ overlaps = [any(sample_overlap(ack, sample, path, token, reference) for sample in samples) for ack in acks]
+ fleet.record("public-call-overlap-proof", {"everyMutationOverlapped": all(overlaps), "mutations": overlaps,
+ "nativeFenceProved": False, "serverCommitOverlapProved": False})
+ return responses, all(overlaps)
+
+
+def run_branch(args, topology, nodes, branch):
+ directory = args.evidence / topology / branch
+ directory.mkdir(parents=True, exist_ok=False)
+ run_id = f"gh-{os.environ['GITHUB_RUN_ID']}-{os.environ.get('GITHUB_RUN_ATTEMPT', '1')}-{topology.lower()}"
+ fleet = Fleet(nodes, directory, run_id, args.tool_directory, runtime_profile="manual")
+ candidate = json.loads(args.candidate.read_text())
+ reference = source_contract(candidate)
+ fleet.record("inspector-build", verify_inspector(args.tool_directory, candidate))
+ bucket = "rv-heal-" + hashlib.sha256(run_id.encode()).hexdigest()[:16]
+ access, secret = secrets.token_hex(16), secrets.token_hex(24)
+ os.environ.update(HEAL_ACCESS_KEY=access, HEAL_SECRET_KEY=secret)
+ binding = {"schemaVersion": 1, "runId": run_id, "jumpHostOnly": True, "exclusiveHealService": True,
+ "leaseExpiresAt": (datetime.now(timezone.utc) + timedelta(minutes=45)).isoformat(), "bucketPrefix": bucket,
+ "endpoints": {f"node{index}": f"http://rustfs-node{index}:9000" for index in range(1, len(nodes) + 1)},
+ "defaultEndpoint": "node1", "candidateCommit": candidate["source_sha"], "harnessCommit": args.testing_commit,
+ "candidatePackageSha256": candidate["package_sha256"], "candidateBinarySha256": args.binary_sha,
+ "rcBinarySha256": hashlib.sha256(args.rc_bin.read_bytes()).hexdigest(),
+ "maxInventoryObjects": 512, "maxInventoryBytes": 512 * 1024**2}
+ fleet.record("binding", binding)
+ fleet.record("source-contract", {**reference, "branch": branch, "objectCount": OBJECTS, "objectSize": SIZE,
+ "requiredPUTs": 3 if branch == BLACK_BOX[0] else 0, "serverCommitOverlapProved": False})
+ probe, active, recorder, writer = Probe(binding), [], None, None
+ result = {"topology": topology, "branch": branch, "fixture": "DL" if branch == BLACK_BOX[0] else "DV",
+ "verdict": "RUNNING", "cleanup": "PENDING", "canonicalCasePassClaimed": False}
+ changed_target_registered, target_authority = None, False
+ try:
+ fleet.deploy(args.binary_sha, bucket, access, secret)
+ fleet.record("topology-proof", validate_topology(fleet.snapshot("deployed-topology"), topology))
+ recorder = RaceRecorder(probe, directory / "seed-ACKs", versioned=branch == BLACK_BOX[1])
+ expected = seed_fixture(recorder, bucket, branch)
+ baseline = fleet.record("complete-baseline-S3", probe.inventory(bucket, unversioned_current_tags=branch == BLACK_BOX[0]))
+ verify_seed(expected, baseline)
+ plan = operation_plan(bucket, branch, expected)
+ fleet.record("frozen-foreground-plan", plan)
+ fleet.stop()
+ before = fleet.snapshot("complete-baseline-physical", quiescent=True)
+ mapped = map_fixture(before, bucket, expected)
+ verify_part_manifest(fleet, list(mapped.values()), "baseline-streaming", count=len(mapped))
+ earliest_target = next(entry for entry in expected if entry["key"] == TARGET_KEY)
+ targets = [mapped[row["key"], row["versionId"]] for row in expected
+ if row["key"] != TARGET_KEY or row["versionId"] == earliest_target["versionId"]]
+ require(len(targets) == OBJECTS, "A09 fault plan omitted a key or changed its source dimension")
+ receipts = []
+ for index, target in enumerate(targets):
+ params = {"disk": target["disk"], "faultId": bounded_fault_id("A09", branch, index)}
+ fleet.faults.append((target["node"], params))
+ if target["key"] == TARGET_KEY: changed_target_registered = target["node"], params
+ receipts.append(fleet.record("exact-missing-part", {"node": target["node"], **fleet.remote(
+ target["node"], "apply-fault", **params, relativePath=target["relativePath"],
+ expectedSha256=target["sha256"], kind="missing")}))
+ faulted = fleet.snapshot("complete-fault-physical", quiescent=True)
+ fleet.record("fault-batch-proof", validate_missing_batch(before, faulted, targets, receipts))
+ fleet.start()
+ fault_hosts = sorted({target['node'] for target in targets})
+ set_race_io_cap(fleet, CASE_ID, branch, True, nodes=fault_hosts)
+ require(physical_content(faulted) == physical_content(fleet.snapshot("attribution-before-START")),
+ "NOT_OBSERVED: A09 missing shards changed before manual admission")
+ path = "/rustfs/admin/v3/heal/" + quote(bucket, safe="")
+ fleet.record("prepared-Admin-request", {"path": path, "query": [], "options": reference["requested"]})
+ start = fleet.record("original-Admin-start", probe.admin(path, [], json.dumps(reference["requested"]).encode()))
+ token = start.get("response", {}).get("clientToken") if isinstance(start.get("response"), dict) else None
+ require(start.get("httpStatus") == 200 and isinstance(token, str) and token, "A09 admission lacks an original token")
+ active.append((path, token))
+ wait_running(probe, fleet, path, token, len(expected), reference)
+ witness = fleet.snapshot("target-missing-before-foreground")
+ target = next(target for target in targets if target["key"] == TARGET_KEY)
+ if (target["node"], target["disk"], target["relativePath"]) in files(witness):
+ raise WindowNotObserved("A09 old target already repaired before foreground mutations")
+ writer = RaceRecorder(probe, directory / "foreground-ACKs", versioned=branch == BLACK_BOX[1])
+ writer.seen.update((row["key"], row["versionId"]) for row in expected)
+ responses, overlapped = race(writer, plan, probe, fleet, path, token, reference)
+ set_race_io_cap(fleet, CASE_ID, branch, False, nodes=fault_hosts)
+ final = fleet.record("ACK-derived-final-manifest", final_inventory(baseline, branch, plan, responses))
+ terminal = wait_terminal(probe, fleet, path, token, reference)
+ active.remove((path, token))
+ for _ in range(2):
+ repeated = observe(probe, fleet, path, token, reference)["response"]
+ require(canonical(repeated.get("outcome")) == canonical(terminal.get("outcome"))
+ and repeated["summary"] == terminal["summary"], "A09 terminal responsibility changed on repetition")
+ # Collect physical evidence even if terminal counters fail. Old target
+ # bytes are never rescued over a successful overwrite or exact delete.
+ fleet.stop()
+ after = fleet.snapshot("product-physical-before-GET-or-rescue", quiescent=True)
+ survivors, physical = physical_transition(before, after, bucket, mapped, final, branch)
+ fleet.record("last-ACK-physical-proof", physical)
+ verify_part_manifest(fleet, list(survivors.values()), "final-streaming", count=len(survivors))
+ target_authority = True
+ # Every missing-part registration is now superseded by product output;
+ # leaving it in Fleet.faults would make cleanup attempt a blind backup
+ # rescue over the new ACK generation.
+ fleet.faults.clear()
+ fleet.record("obsolete-target-fault-retired", {"fault": changed_target_registered,
+ "reason": "complete last-ACK physical authority proves the original generation obsolete",
+ "oldBytesRestored": False, "backupRetainedUntilCleanup": True})
+ fleet.start()
+ for endpoint in binding["endpoints"]:
+ current = fleet.record("all-peer-final-S3", {"endpoint": endpoint, **Probe(binding, endpoint).inventory(
+ bucket, unversioned_current_tags=branch == BLACK_BOX[0])})
+ compare_inventories(final, current)
+ if branch == BLACK_BOX[1]:
+ negative = fleet.record("all-peer-deleted-version-GET", Probe(binding, endpoint).s3("get_object", plan[0]["params"]))
+ response = negative.get("response") or {}
+ require(negative.get("acknowledged") is False and negative.get("resultUnknown") is False
+ and response.get("ResponseMetadata", {}).get("HTTPStatusCode") == 404
+ and response.get("Error", {}).get("Code") == "NoSuchVersion",
+ "A09 deleted exact VersionId revived or failed for another reason")
+ fleet.stop()
+ require(physical_content(after) == physical_content(fleet.snapshot("physical-after-S3", quiescent=True)),
+ "A09 final GET changed the already-proven physical result")
+ result["terminal"] = terminal_proof(terminal, bucket, baseline, final)
+ if not overlapped: raise WindowNotObserved("A09 did not observe every required SDK call overlap")
+ result.update(verdict="PASS", physical=physical, publicCallOverlapObserved=True,
+ nativeFenceProved=False, serverCommitOverlapProved=False)
+ except (Exception, KeyboardInterrupt) as exc:
+ result.update(verdict="NOT_OBSERVED" if isinstance(exc, WindowNotObserved) else "FAIL",
+ failure={"type": type(exc).__name__, "message": str(exc)})
+ try:
+ fleet.stop()
+ fleet.snapshot("retained-failure-scene", quiescent=True)
+ except Exception as capture: fleet.record("failure-scene-error", {"error": str(capture)})
+ finally:
+ for path, token in active:
+ try: fleet.record("cancel-original-token", probe.admin(path, [("clientToken", token), ("forceStop", "true")], b""))
+ except Exception as exc: fleet.record("cancel-error", {"error": str(exc)})
+ unresolved = any(item is not None and item.pending is not None for item in (recorder, writer))
+ mutated = writer is not None and writer.sequence > 0
+ if unresolved or mutated and not target_authority:
+ try: fleet.stop()
+ except Exception as exc: fleet.record("retain-stop-error", {"error": str(exc)})
+ fleet.record("last-ACK-rescue-required", {"state": "DIRTY", "namespacesRetained": True,
+ "unresolvedMutation": unresolved, "lastACKPhysicalAuthorityVerified": target_authority,
+ "originalGenerationBlindRestorationProhibited": True})
+ result["cleanup"] = "FAIL"
+ else:
+ result["cleanup"] = "PASS" if fleet.cleanup() else "FAIL"
+ fleet.record("branch-result", result)
+ return result
+
+
+def native_blocked(candidate, topology):
+ require(topology in {"H1", "H2"}, "A09 native prerequisite lost its source topology")
+ return {"topology": topology, "branch": BRANCHES[-1], "verdict": "BLOCKED", "cleanup": "PASS",
+ "candidateCommit": candidate["source_sha"], "canonicalCasePassClaimed": False,
+ "reason": REFERENCE["nativeFence"], "nativeFenceProved": False, "fleetExecutionClaimed": False}
+
+
+def complete_results(results):
+ wanted = {(topology, branch) for topology in ("H1", "H2") for branch in BRANCHES}
+ actual = {(row["topology"], row["branch"]) for row in results}
+ require(len(actual) == len(results) and actual <= wanted, "A09 duplicate or foreign topology/branch")
+ # No blocked, omitted or best-effort native branch can complete A09.
+ return actual == wanted and all(row["verdict"] == row["cleanup"] == "PASS" for row in results) and all(
+ row.get("nativeFenceProved") is True for row in results if row["branch"] == BRANCHES[-1])
+
+
+def main():
+ if sys.argv[1:2] == ['preflight']:
+ parser = argparse.ArgumentParser(description='Read-only A09 native admission before bootstrap')
+ parser.add_argument('--candidate', type=Path, required=True)
+ parser.add_argument('--case-id', choices=[CASE_ID], required=True)
+ parser.add_argument('--testing-commit', required=True)
+ args = parser.parse_args(sys.argv[2:])
+ proof = native_preflight(json.loads(args.candidate.read_text()), args.testing_commit,
+ os.environ.get('VALIDATION_HEAL_NATIVE_DIRECTORY'))
+ print(json.dumps({'nativeProof':proof,'fleetMutationRequested':False,'canonicalCasePassClaimed':False}))
+ return
+ def interrupted(signum, frame): raise KeyboardInterrupt(f"A09 interrupted by signal {signum}")
+ signal.signal(signal.SIGTERM, interrupted)
+ parser = argparse.ArgumentParser(description=__doc__)
+ for name in ("candidate", "rc-bin", "evidence", "tool-directory"):
+ parser.add_argument("--" + name, type=Path, required=True)
+ parser.add_argument("--case-id", choices=[CASE_ID], required=True)
+ parser.add_argument("--testing-commit", required=True)
+ parser.add_argument("--binary-sha", required=True)
+ args = parser.parse_args()
+ require(select(CASE_ID)[0]["variants"] == list(BRANCHES), "A09 executor omitted a source branch")
+ require(re.fullmatch(r"[0-9a-f]{64}", args.binary_sha) and re.fullmatch(r"[0-9a-f]{40}", args.testing_commit)
+ and re.fullmatch(r"[1-9][0-9]*", os.environ.get("GITHUB_RUN_ID", "")), "A09 immutable identities required")
+ nodes = os.environ.get("RUSTFS_NODES", "vm000 vm001 vm002 vm003").split()
+ require(len(nodes) >= 4 and len(nodes) == len(set(nodes))
+ and all(re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", node) for node in nodes),
+ "A09 requires four distinct current Heal nodes")
+ candidate = json.loads(args.candidate.read_text())
+ native = native_preflight(candidate, args.testing_commit, os.environ.get('VALIDATION_HEAL_NATIVE_DIRECTORY'))
+ if native is None:
+ results = [native_blocked(candidate, topology) for topology in ("H1", "H2")]
+ else:
+ save(args.evidence/'independent-native-nolock-subset.json', native)
+ results = [{'caseId':CASE_ID,'topology':topology,'branch':BRANCHES[-1],'verdict':'PASS','cleanup':'PASS',
+ 'candidateCommit':candidate['source_sha'],'nativeFenceProved':True,'nativeProof':native,
+ 'nativePhysicalShapes':['4/2','16/4'],'fleetExecutionClaimed':False,
+ 'distributedFleetExecutionClaimed':False,'canonicalCasePassClaimed':False}
+ for topology in ('H1','H2')]
+ try:
+ stop = False
+ for topology in ("H1", "H2"):
+ for branch in BLACK_BOX:
+ row = run_branch(args, topology, nodes[:1] if topology == "H1" else nodes[:4], branch)
+ results.append(row)
+ if row["cleanup"] != "PASS":
+ stop = True
+ break
+ if stop:
+ break
+ finally:
+ complete = complete_results(results)
+ save(args.evidence / "healthy-summary.json", {"caseId": CASE_ID, "results": results, "complete": complete,
+ "cleanup": "PASS" if all(row["cleanup"] == "PASS" for row in results) else "FAIL",
+ "canonicalCasePassClaimed": False, "nativePrerequisitesBlocked": native is None})
+ require(complete, "A09 full race/native coverage incomplete; every H1/H2 branch stays in the denominator")
+
+
+if __name__ == "__main__":
+ try: main()
+ except Exception as exc:
+ print(f"A09 validation failed: {exc}", file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_heal/object_lock.py b/scripts/validation_heal/object_lock.py
new file mode 100644
index 00000000..0dab7bab
--- /dev/null
+++ b/scripts/validation_heal/object_lock.py
@@ -0,0 +1,191 @@
+"""D09 protection proofs and S3 finalization after real Compliance expiry."""
+
+from datetime import UTC, timedelta
+from email.utils import parsedate_to_datetime
+import hashlib
+import secrets
+import time
+
+try:
+ from .authorization import iam_exchange
+ from .heal_probe import Probe, payload, require
+except ImportError:
+ from authorization import iam_exchange
+ from heal_probe import Probe, payload, require
+
+
+BRANCHES = ("governance-retention", "compliance-retention", "legal-hold", "historical-protected-version")
+KEYS = ("locked/governance.bin", "locked/compliance.bin", "locked/hold.bin", "locked/historical.bin")
+SIZE = 5 * 1024**2 + 123
+RETENTION_MINUTES = 10
+CLEANUP_SECONDS = (RETENTION_MINUTES + 2) * 60
+
+
+def seed_locked(recorder, fleet, bucket, context):
+ probe = recorder.probe
+ suffix = hashlib.sha256(probe.binding["runId"].encode()).hexdigest()[:16]
+ user, policy, secret = "rvheal" + suffix + "locked", "rvheal" + suffix + "lockpolicy", secrets.token_hex(24)
+ context.update(user=user, policy=policy, principal=Probe(probe.binding, credential_pair=(user, secret)),
+ policyCreated=False, attached=False, bucketAttempted=False)
+ fleet.secret_values.update({user, secret, *probe.credentials()})
+ iam_exchange(probe, fleet, "add-user", user, secret=secret)
+ context["policyCreated"] = True
+ document = {"Version": "2012-10-17", "Statement": [
+ {"Effect": "Allow", "Action": ["s3:*"], "Resource": [f"arn:aws:s3:::{bucket}", f"arn:aws:s3:::{bucket}/*"]},
+ {"Effect": "Deny", "Action": ["s3:BypassGovernanceRetention"], "Resource": [f"arn:aws:s3:::{bucket}/*"]}]}
+ iam_exchange(probe, fleet, "add-policy", user, policy=policy, document=document)
+ context["attached"] = True
+ iam_exchange(probe, fleet, "attach", user, policy=policy)
+ context["bucketAttempted"] = True
+ recorder.call("create_bucket", {"Bucket": bucket, "ObjectLockEnabledForBucket": True})
+ mode = recorder.call("get_bucket_versioning", {"Bucket": bucket})
+ require(mode.get("Status") == "Enabled", "Object Lock bucket did not enable real versioning")
+ lock = recorder.call("get_object_lock_configuration", {"Bucket": bucket})
+ require(lock.get("ObjectLockConfiguration", {}).get("ObjectLockEnabled") == "Enabled", "Object Lock is not active")
+ server_date = lock.get("ResponseMetadata", {}).get("HTTPHeaders", {}).get("date")
+ require(isinstance(server_date, str), "protected fixture needs a server-time retention anchor")
+ server_time = parsedate_to_datetime(server_date)
+ require(server_time.tzinfo is not None, "retention server time is not timezone-aware")
+ until = server_time.astimezone(UTC).replace(microsecond=0) + timedelta(minutes=RETENTION_MINUTES)
+ context["retainUntil"] = until
+ expected, protections = [], []
+ for index, key in enumerate(KEYS):
+ protection = {"ObjectLockLegalHoldStatus": "ON"} if index == 2 else {
+ "ObjectLockMode": "COMPLIANCE" if index == 1 else "GOVERNANCE", "ObjectLockRetainUntilDate": until}
+ for nonce in range(3):
+ body = payload(f"{bucket}/{key}/{nonce}", SIZE)
+ response = recorder.call("put_object", {"Bucket": bucket, "Key": key,
+ "Metadata": {"heal-fixture": "object-lock", "generation": str(nonce)},
+ "Tagging": f"suite=heal&generation=v{nonce}", **protection}, body)
+ version = response.get("VersionId")
+ require(version not in {None, "", "null"}, "protected PUT omitted its acknowledged version")
+ expected.append({"key": key, "versionId": version, "deleteMarker": False, "isLatest": nonce == 2,
+ "payload": {"size": SIZE, "sha256": hashlib.sha256(body).hexdigest()}})
+ protections.append({"key": key, "versionId": version, **protection})
+ context["protections"] = protections
+ fleet.record("lock-fixture", {"protections": protections, "complianceUsesRealExpiry": True,
+ "principalBypassGovernanceDenied": True})
+ return expected
+
+
+def validate_protection(observed, spec):
+ response = observed.get("response") or {}
+ require(observed.get("acknowledged") is True, "protected metadata request failed")
+ if "ObjectLockLegalHoldStatus" in spec:
+ require(response.get("LegalHold", {}).get("Status") == spec["ObjectLockLegalHoldStatus"], "legal hold changed or disappeared")
+ else:
+ retention = response.get("Retention", {})
+ require(retention.get("Mode") == spec["ObjectLockMode"] and retention.get("RetainUntilDate") == spec["ObjectLockRetainUntilDate"],
+ "retention mode or exact UTC expiry changed")
+ server_date = response.get("ResponseMetadata", {}).get("HTTPHeaders", {}).get("date")
+ require(isinstance(server_date, str) and parsedate_to_datetime(server_date) < spec["ObjectLockRetainUntilDate"],
+ "retention proof did not occur during the real server-time protection window")
+
+
+def validate_protected_delete(observed):
+ response = observed.get("response") or {}
+ require(observed.get("acknowledged") is False and observed.get("resultUnknown") is False and "payload" not in observed
+ and response.get("ResponseMetadata", {}).get("HTTPStatusCode") == 403
+ and response.get("Error", {}).get("Code") == "AccessDenied", "protected DELETE was acknowledged or did not return exact AccessDenied")
+
+
+def prove_locked(probe, fleet, bucket, context, *, key=None, label):
+ selected = [spec for spec in context["protections"] if key is None or spec["key"] == key]
+ require(selected, "protected branch has no acknowledged versions")
+ for spec in selected:
+ params = {"Bucket": bucket, "Key": spec["key"], "VersionId": spec["versionId"]}
+ operation = "get_object_legal_hold" if "ObjectLockLegalHoldStatus" in spec else "get_object_retention"
+ observed = fleet.record(label + "-protection", probe.s3(operation, params))
+ validate_protection(observed, spec)
+ # Read succeeds under the same non-bypass principal, so an IAM-wide
+ # access denial cannot masquerade as enforced object protection.
+ readable = fleet.record(label + "-authorized-head", context["principal"].s3("head_object", params))
+ require(readable.get("acknowledged") is True and readable["response"].get("VersionId") == spec["versionId"],
+ "non-bypass principal cannot read the protected version")
+ denied = fleet.record(label + "-delete-denied", context["principal"].s3("delete_object", params))
+ validate_protected_delete(denied)
+ if spec.get("ObjectLockMode") == "GOVERNANCE":
+ denied = fleet.record(label + "-bypass-denied", context["principal"].s3("delete_object",
+ {**params, "BypassGovernanceRetention": True}, allow_governance_bypass=True))
+ validate_protected_delete(denied)
+ return {"protectedVersions": len(selected), "exactProtectionMetadata": True, "nonBypassDeleteDenied": True,
+ "serverTimeBeforeExpiry": True}
+
+
+def finalize_locked(probe, fleet, bucket, context):
+ """Remove protection through APIs; retain the namespace on incomplete cleanup."""
+ errors = []
+ try:
+ if fleet.reserved: fleet.stop()
+ if fleet.faults:
+ for registration in list(fleet.faults):
+ node, params = registration
+ fleet.record("locked-fault-rescue", fleet.remote(node, "recover-fault", **params))
+ fleet.faults.remove(registration)
+ if fleet.reserved: fleet.start()
+ if context.get("bucketAttempted"):
+ listing = fleet.record("locked-final-list", probe.s3("list_object_versions", {"Bucket": bucket}))
+ response = listing.get("response") or {}
+ absent = not listing.get("acknowledged") and response.get("Error", {}).get("Code") == "NoSuchBucket"
+ require(absent or listing.get("acknowledged") and not response.get("IsTruncated"), "protected finalization inventory is incomplete")
+ versions = [] if absent else [{"Key": entry["Key"], "VersionId": entry["VersionId"], "marker": marker}
+ for field, marker in (("Versions", False), ("DeleteMarkers", True)) for entry in response.get(field, [])]
+ require(len(versions) <= 32, "protected cleanup exceeded the owned fixture budget")
+ deadline = time.monotonic() + CLEANUP_SECONDS
+ for version in versions:
+ params = {"Bucket": bucket, "Key": version["Key"], "VersionId": version["VersionId"]}
+ require(version["Key"] in KEYS, "protected finalization found an unowned key")
+ if not version["marker"]:
+ hold = fleet.record("locked-final-hold", probe.s3("get_object_legal_hold", params))
+ value = (hold.get("response") or {}).get("LegalHold", {})
+ if hold.get("acknowledged") and value.get("Status") == "ON":
+ changed = fleet.record("locked-final-hold-off", probe.s3("put_object_legal_hold", {**params, "LegalHold": {"Status": "OFF"}}))
+ require(changed.get("acknowledged") is True, "legal hold could not be released by the owner")
+ retention = fleet.record("locked-final-retention", probe.s3("get_object_retention", params))
+ if (retention.get("response") or {}).get("Retention", {}).get("Mode") == "GOVERNANCE":
+ params["BypassGovernanceRetention"] = True
+ while True:
+ deleted = fleet.record("locked-final-delete", probe.s3("delete_object", params,
+ **({"allow_governance_bypass": True} if params.get("BypassGovernanceRetention") else {})))
+ if deleted.get("acknowledged"):
+ require(deleted["response"].get("ResponseMetadata", {}).get("HTTPStatusCode") in {200, 204}, "protected final DELETE had an unexpected status")
+ break
+ # A Compliance version stays on disk until the server
+ # accepts its deletion after actual expiry. No clock edits,
+ # shortened retention or internal-file removal are used.
+ validate_protected_delete(deleted)
+ require(time.monotonic() < deadline, "protected data is still retained past the cleanup budget")
+ time.sleep(2)
+ if not absent:
+ empty = fleet.record("locked-final-empty", probe.s3("list_object_versions", {"Bucket": bucket}))
+ require(empty.get("acknowledged") and not any(empty["response"].get(field) for field in ("Versions", "DeleteMarkers", "IsTruncated")),
+ "protected versions or markers remain after API finalization")
+ removed = fleet.record("locked-final-bucket-delete", probe.s3("delete_bucket", {"Bucket": bucket}))
+ require(removed.get("acknowledged") is True, "protected bucket finalization failed")
+ absent = fleet.record("locked-final-bucket-absence", probe.s3("head_bucket", {"Bucket": bucket}))
+ require(absent.get("acknowledged") is False and absent.get("resultUnknown") is False
+ and (absent.get("response") or {}).get("ResponseMetadata", {}).get("HTTPStatusCode") == 404,
+ "protected bucket absence is unproven")
+ except Exception as exc:
+ errors.append(str(exc))
+ user, policy = context.get("user"), context.get("policy")
+ for operation, enabled in (("detach", context.get("attached")), ("remove-user", user is not None), ("remove-policy", context.get("policyCreated"))):
+ if enabled:
+ try: iam_exchange(probe, fleet, operation, user, policy=policy if operation != "remove-user" else None)
+ except Exception as exc: errors.append(str(exc))
+ if errors:
+ try:
+ fleet.stop()
+ for node in fleet.reserved: fleet.record("locked-retained-logs", {"node": node, **fleet.remote(node, "logs")})
+ fleet.snapshot("locked-retained-scene", quiescent=True)
+ except Exception as exc: errors.append(str(exc))
+ fleet.record("locked-finalization", {"cleanup": "FAIL" if errors else "PASS", "errors": errors,
+ "state": "RETAINED" if errors else "API_FINALIZED", "namespaceDeletionAllowed": not errors,
+ "complianceExpiryShortened": False, "internalProtectedFileDeletionUsed": False})
+ return not errors
+
+
+def cleanup_locked_fleet(probe, fleet, bucket, context):
+ # Physical namespace deletion is permitted only after protected versions
+ # and temporary IAM resources have been finalized through their APIs.
+ return finalize_locked(probe, fleet, bucket, context) and fleet.cleanup()
diff --git a/scripts/validation_heal/options.py b/scripts/validation_heal/options.py
new file mode 100644
index 00000000..2fc09ddc
--- /dev/null
+++ b/scripts/validation_heal/options.py
@@ -0,0 +1,641 @@
+#!/usr/bin/env python3
+"""A10: source option probes on fresh DB/DV and owned C10 orphan archives."""
+
+import argparse
+import copy
+from datetime import datetime, timedelta, timezone
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import secrets
+import signal
+import sys
+import time
+from urllib.parse import quote
+import uuid
+
+try:
+ from .heal_probe import Probe, compare_inventories, payload, require, save
+ from .heal_workload import Recorder, verify_seed
+ from .healthy import BASE_OPTIONS, Fleet, physical_content, validate_rejection, validate_topology
+ from .inspector import verify as verify_inspector
+ from .matrix import select
+ from .heal_fault import bounded_fault_id
+ from .shards import files, map_target, validate_fault, validate_repaired_physical, peer_status_equal, validate_absent_request
+ from .versions import public_version_id
+ from .scopes import verify_parts
+ from .stop import idle_fleet
+ from . import orphan
+except ImportError:
+ from heal_probe import Probe, compare_inventories, payload, require, save
+ from heal_workload import Recorder, verify_seed
+ from healthy import BASE_OPTIONS, Fleet, physical_content, validate_rejection, validate_topology
+ from inspector import verify as verify_inspector
+ from matrix import select
+ from heal_fault import bounded_fault_id
+ from shards import files, map_target, validate_fault, validate_repaired_physical, peer_status_equal, validate_absent_request
+ from versions import public_version_id
+ from scopes import verify_parts
+ from stop import idle_fleet
+ import orphan
+
+
+CASE_ID = 'heal.admin-option-contract'
+BRANCHES = ('recursive-false', 'recursive-true', 'recreate', 'remove-false', 'remove-true',
+ 'update-parity', 'read-repair', 'selector-valid', 'selector-invalid')
+FIXTURES = ('DB', 'DV')
+SIZE = 1024**2 + 123
+KEYS = ('scope/target.bin', 'scope/child/target.bin', 'scope-extra/control.bin', 'other/control.bin',
+ *tuple(f'set-control/{number:02d}.bin' for number in range(12)))
+REFERENCE = {'candidateCommit': orphan.CANDIDATE,
+ 'sourceFiles': {'rustfs/src/admin/handlers/heal.rs': '7e4007c29a8329ced8e48b88111400e79cffd845bd6ddc6243b79f3cd48e8199',
+ 'crates/heal/src/heal/channel.rs': '7f669ca43b9559db9b97fba6b419a6c76a94d76acd8526db132d74b7c534d6b0'},
+ 'support': {'recursive': 'supported; false is exact object, true traverses child keys',
+ 'recreate': 'supported; request and retained settings must agree',
+ 'remove': 'supported; restricted to this run C10-proven unreferenced orphan',
+ 'updateParity': 'supported; deep parity corruption must be physically repaired',
+ 'readRepair': 'unsupported for Admin; HTTP 400 InvalidArgument before execution',
+ 'pool/set': 'supported paired selector; exact independently measured physical set'},
+ 'dangerousEffects': {'remove': 'may delete unreferenced persisted bytes; never applied to a live fixture',
+ 'recreate': 'may reconstruct missing data; original ACK identities and complete controls stay fixed'},
+ 'cliCoverageClaimed': False,
+ 'validationSource': {'commit': '524eb5b32c3236497fdd8a9271df0494fa3d9974', 'sourceFiles': {
+ 'scripts/heal_h1_options_batch.py': '918d7b6d7a9bacb37095210b5a72a10b024b21f7a9cf46501eeae6272dd625d2',
+ 'scripts/heal_h1_options_node.py': 'b78e7a65d071e08be3ee9d615091c69f801d5a48badb85f246acf9340fd829bc'}},
+ 'removeTrueProcedure': 'one ACK version, exact DELETE authority, one complete original below-quorum object tree, recursive bucket sweep, whole object removal',
+ 'removeTrueBaseline': 'option reference only, as in the frozen source; no synthetic remove=false orphan task'}
+
+
+def paired_options(branch):
+ require(branch in BRANCHES, 'unknown A10 option branch')
+ baseline = {**BASE_OPTIONS, 'pool': None, 'set': None}
+ selected = dict(baseline)
+ if branch == 'recursive-false': baseline['recursive'] = True
+ elif branch == 'recursive-true': selected['recursive'] = True
+ elif branch == 'recreate': selected['recreate'] = True
+ elif branch == 'remove-true':
+ baseline['recursive'] = selected['recursive'] = True
+ selected['remove'] = True
+ elif branch == 'update-parity': selected['updateParity'] = True
+ elif branch == 'read-repair': selected['readRepair'] = True
+ elif branch.startswith('selector-'):
+ baseline['recursive'] = selected['recursive'] = True
+ if branch == 'selector-valid': selected.update(pool=0, set=0)
+ else:
+ baseline.update(pool=0, set=0)
+ selected.update(pool=99, set=99)
+ changed = {name for name in baseline if baseline[name] != selected[name]}
+ expected = {'recursive-false': {'recursive'}, 'recursive-true': {'recursive'}, 'recreate': {'recreate'},
+ 'remove-false': set(), 'remove-true': {'remove'}, 'update-parity': {'updateParity'},
+ 'read-repair': {'readRepair'}, 'selector-valid': {'pool', 'set'}, 'selector-invalid': {'pool', 'set'}}[branch]
+ require(changed == expected, 'A10 paired request changed unrelated defaults')
+ return baseline, selected
+
+
+def source_phases(branch):
+ require(branch in BRANCHES, 'unknown A10 source branch')
+ # The frozen source runs the proven orphan only with remove=true. Its
+ # remove=false branch independently heals a repairable acknowledged object.
+ return ('selected',) if branch == 'remove-true' else ('baseline', 'selected')
+
+
+class FixtureRecorder(Recorder):
+ """Retain the namespace unless all mutation receipts and identities resolve."""
+ def __init__(self, *args, fixture='DB', **kwargs):
+ super().__init__(*args, **kwargs)
+ require(fixture in FIXTURES, 'unknown A10 ACK versioning mode')
+ self.fixture = fixture
+ self.versions = set()
+ self.pending = None
+
+ def call(self, operation, params, body=None, **kwargs):
+ self.pending = {'operation': operation, 'params': copy.deepcopy(params)}
+ value = super().call(operation, params, body, **kwargs)
+ # A successful HTTP status alone is insufficient for a durable identity.
+ if operation in {'put_object', 'delete_object'}:
+ version = value.get('VersionId')
+ require(version in {None, 'null'} or isinstance(version, str) and str(uuid.UUID(version)) == version
+ and uuid.UUID(version).int != 0,
+ 'A10 mutation receipt has an invalid version identity')
+ if operation == 'put_object':
+ require(isinstance(value.get('ETag'), str) and value['ETag'], 'A10 PUT ACK omitted ETag')
+ require((self.fixture == 'DB' and version in {None, 'null'})
+ or (self.fixture == 'DV' and version not in {None, 'null'}), 'A10 PUT ACK changed its DB/DV mode')
+ identity = params['Key'], public_version_id(version)
+ require(identity not in self.versions, 'A10 PUT ACK reused an exact version identity')
+ self.versions.add(identity)
+ elif operation == 'delete_object':
+ if 'VersionId' in params:
+ require(version == params['VersionId'] and value.get('DeleteMarker') in {None, False},
+ 'A10 exact data-version DELETE ACK changed identity or kind')
+ elif self.fixture == 'DV':
+ identity = params['Key'], public_version_id(version)
+ require(value.get('DeleteMarker') is True and version not in {None, 'null'}
+ and identity not in self.versions, 'A10 DELETE ACK omitted its independent marker identity')
+ self.versions.add(identity)
+ else:
+ require(version in {None, 'null'} and value.get('DeleteMarker') in {None, False},
+ 'A10 DB DELETE ACK created a version or marker')
+ self.pending = None
+ return value
+
+
+def seed_fixture(recorder, bucket, fixture, keys=KEYS, *, single_version_key=None):
+ require(fixture in FIXTURES and len(keys) == len(set(keys)) and keys
+ and (single_version_key is None or single_version_key in keys[:-1]), 'invalid A10 fixture')
+ recorder.call('create_bucket', {'Bucket': bucket})
+ if fixture == 'DV':
+ recorder.call('put_bucket_versioning', {'Bucket': bucket, 'VersioningConfiguration': {'Status': 'Enabled'}})
+ expected = []
+ for key in keys:
+ generations = 3 if fixture == 'DV' and key != single_version_key else 1
+ for number in range(generations):
+ body = payload(f'A10/{fixture}/{key}/{number}', SIZE)
+ response = recorder.call('put_object', {'Bucket': bucket, 'Key': key,
+ 'Metadata': {'heal-fixture': 'A10', 'generation': str(number)}, 'Tagging': 'suite=heal&fixture=' + fixture}, body)
+ recorder.pending = {'operation': 'put_object', 'bucket': bucket, 'key': key, 'reason': 'fixture identity unresolved'}
+ version = public_version_id(response.get('VersionId'))
+ require((fixture == 'DB' and version == 'null') or (fixture == 'DV' and version != 'null'),
+ 'A10 candidate ACK disagrees with DB/DV versioning')
+ require((key, version) not in {(entry['key'], entry['versionId']) for entry in expected},
+ 'A10 PUT ACK reused a version')
+ expected.append({'key': key, 'versionId': version, 'isLatest': number == generations - 1,
+ 'deleteMarker': False, 'payload': {'size': len(body), 'sha256': hashlib.sha256(body).hexdigest()}})
+ recorder.pending = None
+ if fixture == 'DV':
+ key = keys[-1]
+ response = recorder.call('delete_object', {'Bucket': bucket, 'Key': key})
+ recorder.pending = {'operation': 'delete_object', 'bucket': bucket, 'key': key, 'reason': 'marker identity unresolved'}
+ version = public_version_id(response.get('VersionId'))
+ require(response.get('DeleteMarker') is True and version != 'null'
+ and version not in {entry['versionId'] for entry in expected}, 'A10 marker ACK is incomplete or duplicate')
+ next(entry for entry in expected if entry['key'] == key and entry['isLatest'])['isLatest'] = False
+ expected.append({'key': key, 'versionId': version, 'isLatest': True, 'deleteMarker': True})
+ recorder.pending = None
+ return expected
+
+
+def map_fixture(snapshot, bucket, expected, topology, *, multi_set=True):
+ validate_topology(snapshot, topology, expected_sets=2 if topology == 'H2' and multi_set else 1)
+ indexed, mapped = files(snapshot), {}
+ for entry in expected:
+ scopes = set()
+ for node, value in snapshot.items():
+ for row in value['formats']:
+ if (node, row['disk'], f"{bucket}/{entry['key']}/xl.meta") in indexed:
+ layout = row['value']['xl']
+ scopes.update(number for number, members in enumerate(layout['sets']) if layout['this'] in members)
+ require(len(scopes) == 1, 'A10 ACK has an ambiguous physical set')
+ set_index = next(iter(scopes))
+ identity = (bucket, entry['key'], entry['versionId'])
+ if entry['deleteMarker']:
+ mapped[identity] = {'bucket': bucket, 'key': entry['key'], 'versionId': entry['versionId'],
+ 'poolIndex': 0, 'setIndex': set_index, 'deleteMarker': True}
+ else:
+ mapped[identity] = {**map_target(snapshot, bucket, entry['key'], 'data', 1, entry['versionId'],
+ latest_required=entry['isLatest'], multi_set=topology == 'H2' and multi_set, set_index=set_index), 'deleteMarker': False}
+ mapped[identity]['isLatest'] = entry['isLatest']
+ if topology == 'H2' and multi_set:
+ require({target['setIndex'] for target in mapped.values()} == {0, 1}, 'A10 fixture did not populate both actual sets')
+ require(len(mapped) == len(expected), 'A10 physical mapping omitted an ACK')
+ return mapped
+
+
+def selection(branch, options, mapped):
+ if branch.startswith('recursive-'):
+ return {identity for identity in mapped if identity[1].startswith('scope/')} if options['recursive'] else set()
+ if branch.startswith('selector-'):
+ return {identity for identity, target in mapped.items() if options['pool'] is None
+ or (target['poolIndex'], target['setIndex']) == (options['pool'], options['set'])}
+ # An exact object request has no versionId and resolves the current ACK.
+ current = {identity for identity, target in mapped.items()
+ if identity[1] == KEYS[0] and target.get('isLatest') is True}
+ require(len(current) == 1, 'A10 exact object request lacks one unambiguous current ACK')
+ return current
+
+
+def request_path(branch, bucket):
+ if branch.startswith('selector-'): return '/rustfs/admin/v3/heal/'
+ if branch == 'remove-true': return '/rustfs/admin/v3/heal/' + quote(bucket, safe='')
+ key = 'scope/' if branch.startswith('recursive-') else KEYS[0]
+ return '/rustfs/admin/v3/heal/' + quote(bucket, safe='') + '/' + quote(key, safe='/')
+
+
+def run_request(probe, fleet, path, options, active, *, rejected=False):
+ raw = fleet.record('A10-admission', probe.admin(path, [], json.dumps(options).encode()))
+ if raw.get('httpStatus') == 200 and isinstance(raw.get('response'), dict) and raw['response'].get('clientToken'):
+ active.append((path, raw['response']['clientToken']))
+ if rejected:
+ validate_rejection(raw, 'InvalidArgument')
+ return raw
+ require(raw.get('httpStatus') == 200 and isinstance(raw.get('response'), dict)
+ and active and active[-1][0] == path, 'A10 admission omitted its exact token')
+ token = active[-1][1]
+ deadline = time.monotonic() + 180
+ while True:
+ raw = fleet.record('A10-status', probe.admin(path, [('clientToken', token)], b''))
+ require(raw.get('httpStatus') == 200 and isinstance(raw.get('response'), dict), 'A10 original status is unavailable')
+ status = raw['response']
+ if status.get('summary') != 'running': break
+ require(time.monotonic() < deadline, 'A10 task exceeded its frozen budget')
+ time.sleep(.2)
+ require(status.get('settings') == options, 'A10 candidate silently changed or ignored request fields')
+ for endpoint in probe.binding['endpoints']:
+ peer = Probe(probe.binding, endpoint)
+ convergence = time.monotonic() + 30
+ while True:
+ repeat = fleet.record('A10-retained-status', peer.admin(path, [('clientToken', token)], b''))
+ require(repeat.get('httpStatus') == 200 and isinstance(repeat.get('response'), dict), 'A10 retained task status is missing')
+ if (repeat['response'].get('settings') == options and peer_status_equal(status, repeat['response'],
+ task=(path, token), peer_task=(path, token))): break
+ require(time.monotonic() < convergence, 'A10 original responsibility did not converge on every peer')
+ time.sleep(.2)
+ active.remove((path, token))
+ return status
+
+
+def validate_outcome(status, mapped, selected, repaired, options, *, absent=frozenset()):
+ outcome = status.get('outcome') or {}
+ require(absent <= selected and not (repaired & absent)
+ and (not absent or options.get('remove') is True and options.get('recursive') is True),
+ 'A10 absence receipts require the exact destructive source scope')
+ if not selected:
+ buckets = {identity[0] for identity in mapped}
+ require(not repaired and len(buckets) == 1, 'A10 absent prefix changed its physical scope or claimed a repair')
+ proof = validate_absent_request(status, next(iter(buckets)), 'scope/', options)
+ return {**proof, 'exactPrefixNotFound': True, 'selectedACKs': 0, 'repairedACKs': 0}
+ require(status.get('summary') == 'finished' and status.get('settings') == options
+ and status.get('truncated', False) is False and outcome.get('coverage') == 'complete'
+ and outcome.get('objectsTruncated') is False, 'A10 lacks complete original task coverage/settings')
+ counters = outcome.get('counters') or {}
+ require(repaired <= selected <= set(mapped) and outcome.get('execution', {}).get('state') == 'completed',
+ 'A10 responsibility escaped the physical scope or failed')
+ objects, seen, incarnations = outcome.get('objects'), set(), {}
+ require(isinstance(objects, list) and len(objects) == len(selected), 'A10 diagnostics omitted a version/marker or added a control')
+ for receipt in objects:
+ item = receipt.get('identity') or {}
+ identity = (item.get('bucket'), item.get('object'), public_version_id(item.get('versionId')))
+ require(identity in selected and identity not in seen and item.get('kind') == 'object', 'A10 wrong or duplicate ACK identity')
+ target, scope = mapped[identity], (item.get('poolIndex'), item.get('setIndex'))
+ if (options['pool'], options['set']) != (None, None):
+ require(all(type(value) is int for value in scope) and scope == (options['pool'], options['set'])
+ == (target['poolIndex'], target['setIndex']), 'A10 explicit selector identity was lost or changed')
+ elif scope != (None, None):
+ require(all(type(value) is int for value in scope) and scope == (target['poolIndex'], target['setIndex']),
+ 'A10 unrestricted receipt contradicts its physical set')
+ incarnation = item.get('bucketIncarnationId')
+ require(isinstance(incarnation, str) and incarnation and incarnations.setdefault(identity[0], incarnation) == incarnation,
+ 'A10 lost or mixed a bucket incarnation')
+ state = ('authoritatively_absent' if identity in absent else 'repaired' if identity in repaired
+ else 'metadata_healthy' if target['deleteMarker'] else 'verified_healthy')
+ require(receipt.get('disposition', {}).get('state') == state, 'A10 false repair, unknown or incomplete integrity')
+ seen.add(identity)
+ expected = {'processed': len(selected), 'healed': len(repaired), 'unchanged': len(selected) - len(repaired),
+ 'failed': 0, 'unknown': 0, 'skipped': 0, 'attemptFailures': 0, 'overflowed': False}
+ require(all(type(counters.get(name)) is type(value) and counters[name] == value for name, value in expected.items()),
+ 'A10 counters disagree with the complete independent ACK/physical manifest')
+ return {'selectedACKs': len(selected), 'repairedACKs': len(repaired),
+ 'authoritativelyAbsentACKs': len(absent), 'completeVersionsAndMarkers': True}
+
+
+def physical_selection(original, after, targets, selected):
+ controls = [target for target in targets if (target['bucket'], target['key'], target['versionId']) not in selected]
+ repaired = [target for target in targets if target not in controls]
+ expected = copy.deepcopy(original)
+ actual = files(after)
+ for target in controls:
+ identity = target['node'], target['disk'], target['relativePath']
+ # Bitflipped control bytes are witnessed separately below. The product
+ # physical oracle takes the exact fault scene as its control authority.
+ if target['faultKind'] == 'missing':
+ require(identity not in actual, 'A10 unselected missing control was silently repaired')
+ else:
+ require(identity in actual and {name: actual[identity][name] for name in ('sha256', 'size')} == target['faultBytes'],
+ 'A10 unselected corrupted control was silently repaired or changed')
+ for entry in expected[target['node']]['files']:
+ if (target['disk'], target['relativePath']) == (entry['disk'], entry['path']):
+ if target['faultKind'] == 'missing': expected[target['node']]['files'].remove(entry)
+ else: entry.update(sha256=target['faultBytes']['sha256'], size=target['faultBytes']['size'])
+ break
+ if repaired:
+ proof = validate_repaired_physical(expected, after, repaired[0], repair_targets=repaired)
+ else:
+ require(physical_content(expected) == physical_content(after), 'A10 no-repair/rejected request changed persisted bytes')
+ proof = {'allPersistedBytesUnchanged': True}
+ return {**proof, 'unselectedFaultsPreserved': len(controls), 'selectedPhysicalRepairs': len(repaired),
+ 'beforeAnyTargetGETOrBackupRescue': True}
+
+
+def verify_public(fleet, binding, bucket, expected, baseline, fixture, label):
+ for endpoint in binding['endpoints']:
+ observed = fleet.record(label, Probe(binding, endpoint).inventory(bucket, unversioned_current_tags=fixture == 'DB'))
+ verify_seed(expected, observed)
+ compare_inventories(baseline, observed)
+
+
+def scope_probe(fleet, probe, binding, recorder, bucket, topology, fixture, branch, options, active):
+ expected = seed_fixture(recorder, bucket, fixture)
+ baseline = fleet.record('A10-full-ACK-baseline', probe.inventory(bucket, unversioned_current_tags=fixture == 'DB'))
+ verify_seed(expected, baseline)
+ fleet.stop()
+ original = fleet.snapshot('A10-original-physical', quiescent=True)
+ mapped = map_fixture(original, bucket, expected, topology)
+ data = {identity: target for identity, target in mapped.items() if not target['deleteMarker']}
+ verify_parts(fleet, data, 'A10-all-original-streaming')
+ selected = selection(branch, options, mapped)
+ targets = []
+ current_object_request = not branch.startswith(('recursive-', 'selector-'))
+ for index, key in enumerate(KEYS[:4]):
+ identity = next(identity for identity in data if identity[1] == key
+ and (not current_object_request or key != KEYS[0] or identity in selected))
+ target = data[identity]
+ role = 'parity' if index % 2 or branch == 'update-parity' and index == 0 else 'data'
+ member = next(row for row in target['cohort'] if row['role'] == role)
+ targets.append({**target, **member, 'faultKind': 'bitflip' if role == 'parity' else 'missing'})
+ if topology == 'H2':
+ for number in (0, 1):
+ if any(target['setIndex'] == number for target in targets): continue
+ target = next(target for identity, target in data.items() if target['setIndex'] == number)
+ targets.append({**target, 'faultKind': 'missing'})
+ prior, registrations = original, []
+ for index, target in enumerate(targets):
+ params = {'disk': target['disk'], 'faultId': bounded_fault_id('A10', branch, index)}
+ fleet.faults.append((target['node'], params))
+ registrations.append((target['node'], params, target))
+ raw = fleet.record('A10-fault', fleet.remote(target['node'], 'apply-fault', **params,
+ relativePath=target['relativePath'], expectedSha256=target['sha256'], kind=target['faultKind'],
+ **({'offset': target['size'] // 2} if target['faultKind'] == 'bitflip' else {})))
+ scene = fleet.snapshot('A10-witness-fault', quiescent=True)
+ validate_fault(prior, scene, target, raw)
+ target['faultBytes'] = raw.get('fault')
+ prior = scene
+ fleet.start()
+ require(physical_content(prior) == physical_content(fleet.snapshot('A10-fault-immediately-before-START')),
+ 'NOT_OBSERVED: A10 faults changed before original task admission')
+ rejected = options['readRepair'] or options['pool'] == 99
+ status = run_request(probe, fleet, request_path(branch, bucket), options, active, rejected=rejected)
+ idle_fleet(fleet, binding)
+ fleet.stop()
+ after = fleet.snapshot('A10-product-before-GET-or-rescue', quiescent=True)
+ if rejected:
+ require(physical_content(prior) == physical_content(after), 'A10 structured rejection mutated physical controls')
+ selected = set()
+ terminal = {'httpStatus': status['httpStatus'], 'structuredInvalidArgument': True, 'noActiveResponsibility': True}
+ else:
+ repaired = {(target['bucket'], target['key'], target['versionId']) for target in targets} & selected
+ terminal = validate_outcome(status, mapped, selected, repaired, options)
+ if not selected:
+ require('Object not found: ' + bucket + '/scope/' in status.get('detail', ''), 'A10 exact prefix failure names another key')
+ physical = physical_selection(original, after, targets, selected)
+ verify_parts(fleet, {identity: target for identity, target in data.items() if identity in selected},
+ 'A10-selected-physical-streaming') if selected else None
+ # Restore only excluded controls after the product result has been frozen.
+ for node, params, target in registrations:
+ if (target['bucket'], target['key'], target['versionId']) not in selected:
+ receipt = fleet.record('A10-excluded-control-rescue', fleet.remote(node, 'recover-fault', **params))
+ require(receipt.get('physicalRepairPassAllowed') is False, 'A10 backup rescue claimed product repair')
+ fleet.faults.remove((node, params))
+ restored = fleet.snapshot('A10-all-physical-after-control-rescue', quiescent=True)
+ validate_repaired_physical(original, restored, targets[0], repair_targets=targets)
+ verify_parts(fleet, data, 'A10-all-final-physical-streaming')
+ fleet.start()
+ verify_public(fleet, binding, bucket, expected, baseline, fixture, 'A10-all-peer-final-S3')
+ fleet.stop()
+ require(physical_content(restored) == physical_content(fleet.snapshot('A10-physical-after-S3', quiescent=True)),
+ 'A10 GET changed the already-proven physical result')
+ return {'terminal': terminal, 'physical': physical, 'fixture': fixture, 'originalACKs': len(expected)}
+
+
+def validate_orphan_replay(original, before, authority, bucket, target):
+ prefix = bucket + '/' + orphan.KEY + '/'
+ source, actual, deleted = files(original), files(before), files(authority)
+ selected = target['node'], target['disk']
+ coding = target['logical']['erasure']
+ require(type(coding.get('data_blocks')) is int and 1 < coding['data_blocks']
+ and type(coding.get('parity_blocks')) is int and coding['parity_blocks'] > 0
+ and coding['data_blocks'] + coding['parity_blocks'] == len(orphan.members(original)),
+ 'A10 orphan replay is not below the independently decoded read quorum')
+ expected = {k: r for k, r in source.items() if k[:2] == selected and k[2].startswith(prefix)}
+ require(expected and (selected[0], selected[1], prefix + 'xl.meta') in expected
+ and any(k[2].startswith(prefix + target['dataDir'] + '/') for k in expected),
+ 'A10 source orphan lacks its original metadata or payload')
+ require(not any(k[2].startswith(prefix) for k in deleted)
+ and {k: r for k, r in actual.items() if k[2].startswith(prefix)} == expected,
+ 'A10 replay must preserve the complete original object on exactly one member')
+ require({k: r for k, r in actual.items() if not k[2].startswith(prefix)} == deleted
+ == {k: r for k, r in source.items() if not k[2].startswith(prefix)}
+ and {n: d['formats'] for n, d in original.items()}
+ == {n: d['formats'] for n, d in before.items()} == {n: d['formats'] for n, d in authority.items()}
+ and orphan.members(before) == orphan.members(authority) == orphan.members(original),
+ 'A10 orphan replay changed post-DELETE controls or physical membership')
+ return {'completeOriginalObjectTree': True, 'replayedMembers': 1,
+ 'independentlyDecodedReadQuorum': coding['data_blocks'], 'postDeleteAuthorityRetained': True}
+
+
+def orphan_probe(fleet, probe, binding, recorder, bucket, topology, fixture, options, active):
+ require(options['recursive'] is True and options['remove'] is True and options['dryRun'] is False,
+ 'A10 source orphan requires a recursive destructive sweep')
+ # C10 archives preserve the full old tree and a distinct post-DELETE rescue.
+ keys = (orphan.KEY, 'control.bin', 'marker-control.bin')
+ expected = seed_fixture(recorder, bucket, fixture, keys, single_version_key=orphan.KEY)
+ doomed = next(entry for entry in expected if entry['key'] == orphan.KEY and entry['isLatest'])
+ if fixture == 'DV':
+ # Remove every orphan-key generation, with separate exact durable ACKs.
+ doomed_entries = [entry for entry in expected if entry['key'] == orphan.KEY]
+ else: doomed_entries = [doomed]
+ fleet.record('A10-orphan-original-S3', verify_seed(expected, probe.inventory(bucket, unversioned_current_tags=fixture == 'DB')))
+ fleet.stop()
+ original = fleet.snapshot('A10-orphan-original-all-members', quiescent=True)
+ mapped = map_fixture(original, bucket, expected, topology, multi_set=False)
+ target = mapped[(bucket, orphan.KEY, doomed['versionId'])]
+ require(len(doomed_entries) == 1, 'A10 source orphan requires one acknowledged generation')
+ verify_parts(fleet, {identity: item for identity, item in mapped.items() if not item['deleteMarker']},
+ 'A10-all-original-orphan-and-control-streaming')
+ orphan.capture(fleet, original, bucket, 'old')
+ fleet.start()
+ for entry in doomed_entries:
+ params = {'Bucket': bucket, 'Key': orphan.KEY}
+ if fixture == 'DV': params['VersionId'] = entry['versionId']
+ response = recorder.call('delete_object', params)
+ recorder.pending = {'operation': 'delete_object', 'params': params, 'reason': 'ACK identity not yet resolved'}
+ require(response.get('ResponseMetadata', {}).get('HTTPStatusCode') == 204
+ and response.get('DeleteMarker') in {None, False}
+ and public_version_id(response.get('VersionId')) == entry['versionId'], 'A10 orphan DELETE ACK changed exact identity')
+ recorder.pending = None
+ expected.remove(entry)
+ baseline = fleet.record('A10-last-DELETE-S3-authority', probe.inventory(bucket, unversioned_current_tags=fixture == 'DB'))
+ verify_seed(expected, baseline)
+ fleet.stop()
+ authority = fleet.snapshot('A10-last-DELETE-all-physical-authority', quiescent=True)
+ require(all(not values for values in orphan.references(authority, bucket, orphan.KEY).values())
+ and not any(identity[2].startswith(bucket + '/' + orphan.KEY + '/') for identity in files(authority)),
+ 'A10 remove target has a surviving version/marker/data reference')
+ fleet.a10_orphan_authority = authority
+ fleet.a10_orphan_restored = False
+ orphan.arm_all(fleet, authority, bucket, 'authority')
+ try:
+ fleet.record('A10-owned-complete-original-object-replay', fleet.remote(target['node'], 'orphan-install',
+ disk=target['disk'], bucket=bucket, key=orphan.KEY, dataDir=target['dataDir'], mode='object'))
+ before = fleet.snapshot('A10-orphan-fault-before-GET', quiescent=True)
+ fleet.record('A10-source-orphan-replay-proof', validate_orphan_replay(original, before, authority, bucket, target))
+ fleet.start()
+ require(physical_content(before) == physical_content(fleet.snapshot('A10-orphan-immediately-before-START')),
+ 'NOT_OBSERVED: A10 orphan changed before admission')
+ status = run_request(probe, fleet, request_path('remove-true', bucket), options, active)
+ idle_fleet(fleet, binding)
+ fleet.stop()
+ after = fleet.snapshot('A10-orphan-product-before-GET-or-rescue', quiescent=True)
+ require(not any(identity[2].startswith(bucket + '/' + orphan.KEY + '/') for identity in files(after))
+ and physical_content(after) == physical_content(authority),
+ 'A10 source sweep did not remove the whole orphan while preserving every post-DELETE control')
+ physical = {'wholeOrphanObjectRemoved': True, 'allPostDeleteControlsAndFormatsUnchanged': True,
+ 'beforeAnyTargetGETOrBackupRescue': True}
+ removed = {(bucket, orphan.KEY, doomed['versionId'])}
+ absent = {(r.get('identity', {}).get('bucket'), r.get('identity', {}).get('object'),
+ public_version_id(r.get('identity', {}).get('versionId')))
+ for r in status.get('outcome', {}).get('objects', [])
+ if r.get('disposition', {}).get('state') == 'authoritatively_absent'}
+ require(absent <= removed, 'A10 sweep claimed a live control was absent')
+ terminal = validate_outcome(status, mapped, set(mapped), removed - absent, options, absent=absent)
+ fleet.start()
+ verify_public(fleet, binding, bucket, expected, baseline, fixture, 'A10-orphan-product-before-rescue-S3')
+ for endpoint in binding['endpoints']:
+ peer = Probe(binding, endpoint)
+ for entry in doomed_entries:
+ params = {'Bucket': bucket, 'Key': orphan.KEY}
+ if fixture == 'DV': params['VersionId'] = entry['versionId']
+ observed = fleet.record('A10-deleted-orphan-not-resurrected', peer.s3('get_object', params))
+ response = observed.get('response') or {}
+ require(not observed['acknowledged'] and not observed['resultUnknown'] and 'payload' not in observed
+ and response.get('ResponseMetadata', {}).get('HTTPStatusCode') == 404
+ and response.get('Error', {}).get('Code') in {'NoSuchKey', 'NoSuchVersion'}, 'A10 deleted ACK resurrected')
+ finally:
+ # These archives are the last DELETE authority, never the old PUT tree.
+ fleet.stop()
+ orphan.recover_all(fleet)
+ restored = fleet.snapshot('A10-orphan-last-DELETE-restored', quiescent=True)
+ require(physical_content(authority) == physical_content(restored), 'A10 orphan rescue changed last DELETE authority')
+ fleet.a10_orphan_restored = True
+ mapping = map_fixture(restored, bucket, expected, topology, multi_set=False)
+ verify_parts(fleet, {identity: target for identity, target in mapping.items() if not target['deleteMarker']},
+ 'A10-orphan-all-live-controls-streaming')
+ fleet.start()
+ verify_public(fleet, binding, bucket, expected, baseline, fixture, 'A10-orphan-all-peer-final-S3')
+ return {'fixture': fixture, 'terminal': terminal, 'physical': physical, 'allOriginalPostDeleteAuthoritiesRestored': True,
+ 'sourceCompleteBelowQuorumOrphanProof': True, 'deletedACKs': len(doomed_entries), 'rescueIsProductRepair': False}
+
+
+def run_instance(args, topology, nodes, branch, fixture, phase):
+ require(phase in source_phases(branch), 'A10 phase is not part of its source procedure')
+ directory = args.evidence / topology / branch / fixture / phase
+ directory.mkdir(parents=True)
+ candidate = json.loads(args.candidate.read_text())
+ require(candidate['source_sha'] == REFERENCE['candidateCommit'], 'A10 option contract is not reviewed for this candidate')
+ run_id = f"gh-{os.environ['GITHUB_RUN_ID']}-{os.environ.get('GITHUB_RUN_ATTEMPT', '1')}-{topology.lower()}"
+ is_orphan = branch == 'remove-true'
+ runtime = 'orphan-preserve' if is_orphan else 'scope-isolation' if topology == 'H2' else 'manual'
+ fleet = Fleet(nodes, directory, run_id, args.tool_directory, runtime_profile=runtime)
+ bucket = 'rv-heal-' + hashlib.sha256(run_id.encode()).hexdigest()[:16]
+ access, secret = secrets.token_hex(16), secrets.token_hex(24)
+ os.environ.update(HEAL_ACCESS_KEY=access, HEAL_SECRET_KEY=secret)
+ binding = {'schemaVersion': 1, 'runId': run_id, 'jumpHostOnly': True, 'exclusiveHealService': True,
+ 'leaseExpiresAt': (datetime.now(timezone.utc) + timedelta(minutes=30)).isoformat(), 'bucketPrefix': bucket,
+ 'endpoints': {f'node{index}': f'http://rustfs-node{index}:9000' for index in range(1, len(nodes) + 1)},
+ 'defaultEndpoint': 'node1', 'candidateCommit': candidate['source_sha'], 'harnessCommit': args.testing_commit,
+ 'candidatePackageSha256': candidate['package_sha256'], 'candidateBinarySha256': args.binary_sha,
+ 'rcBinarySha256': hashlib.sha256(args.rc_bin.read_bytes()).hexdigest(), 'maxInventoryObjects': 128,
+ 'maxInventoryBytes': 512 * 1024**2}
+ baseline_options, selected_options = paired_options(branch)
+ options = baseline_options if phase == 'baseline' else selected_options
+ fleet.record('A10-source-bound-contract', {**REFERENCE, 'branch': branch, 'fixture': fixture, 'phase': phase,
+ 'baselineOptions': baseline_options, 'selectedOptions': selected_options,
+ 'singleLogicalFieldDelta': 'pool/set selector pair' if branch.startswith('selector-') else branch,
+ 'registeredExecutionPhases': source_phases(branch),
+ 'baselineExecutionRequired': branch != 'remove-true'})
+ fleet.record('binding', binding)
+ fleet.record('inspector-build', verify_inspector(args.tool_directory, candidate))
+ probe, active, recorder = Probe(binding), [], None
+ result = {'topology': topology, 'branch': branch, 'fixture': fixture, 'phase': phase, 'options': options,
+ 'verdict': 'RUNNING', 'cleanup': 'PENDING'}
+ seed_complete = False
+ try:
+ fleet.deploy(args.binary_sha, bucket, access, secret)
+ recorder = FixtureRecorder(probe, directory / 'fixture-ACKs', fixture=fixture)
+ # Each phase owns a fresh namespace; failed/incomplete seeding cannot
+ # be hidden by a later default repair or by final namespace removal.
+ if is_orphan:
+ proof = orphan_probe(fleet, probe, binding, recorder, bucket, topology, fixture, options, active)
+ else:
+ proof = scope_probe(fleet, probe, binding, recorder, bucket, topology, fixture, branch, options, active)
+ seed_complete = True
+ result.update(verdict='PASS', proof=proof)
+ except (Exception, KeyboardInterrupt) as exc:
+ result.update(verdict='FAIL', failure={'type': type(exc).__name__, 'message': str(exc)})
+ finally:
+ for path, token in active:
+ try: fleet.record('A10-stop-unresolved-original-token', probe.admin(path, [('clientToken', token), ('forceStop', 'true')], b''))
+ except Exception as exc: fleet.record('A10-stop-error', {'error': str(exc)})
+ unresolved = recorder is not None and recorder.pending is not None
+ incomplete_restore = hasattr(fleet, 'a10_orphan_authority') and not fleet.a10_orphan_restored
+ if unresolved or incomplete_restore:
+ try: fleet.stop()
+ except Exception as exc: fleet.record('A10-retain-stop-error', {'error': str(exc)})
+ fleet.record('A10-unresolved-authority-retained', {'state': 'DIRTY',
+ 'request': recorder.pending if recorder is not None else None, 'namespacesRetained': True,
+ 'originalPostDeleteRestorationIncomplete': incomplete_restore})
+ clean = False
+ else:
+ clean = fleet.cleanup()
+ result.update(cleanup='PASS' if clean else 'FAIL', fullProbeCompleted=seed_complete)
+ fleet.record('instance-result', result)
+ return result
+
+
+def complete_results(results):
+ wanted = {(topology, branch, fixture, phase) for topology in ('H1', 'H2') for branch in BRANCHES
+ for fixture in FIXTURES for phase in source_phases(branch)}
+ actual = {(row['topology'], row['branch'], row['fixture'], row['phase']) for row in results}
+ require(len(actual) == len(results) and actual <= wanted, 'A10 duplicate or foreign parameter instance')
+ return actual == wanted and all(row['verdict'] == row['cleanup'] == 'PASS' and row.get('fullProbeCompleted') is True
+ for row in results)
+
+
+def main():
+ def interrupted(signum, frame): raise KeyboardInterrupt(f'A10 interrupted by signal {signum}')
+ signal.signal(signal.SIGTERM, interrupted)
+ parser = argparse.ArgumentParser(description=__doc__)
+ for name in ('candidate', 'rc-bin', 'evidence', 'tool-directory'):
+ parser.add_argument('--' + name, type=Path, required=True)
+ parser.add_argument('--case-id', choices=[CASE_ID], required=True)
+ parser.add_argument('--testing-commit', required=True)
+ parser.add_argument('--binary-sha', required=True)
+ args = parser.parse_args()
+ require(re.fullmatch(r'[0-9a-f]{64}', args.binary_sha) and re.fullmatch(r'[1-9][0-9]*', os.environ.get('GITHUB_RUN_ID', '')),
+ 'A10 requires immutable workflow/binary identities')
+ require(select(CASE_ID)[0]['variants'] == list(BRANCHES), 'A10 omitted a source option')
+ nodes = os.environ.get('RUSTFS_NODES', 'vm000 vm001 vm002 vm003').split()
+ require(len(nodes) >= 4 and len(nodes) == len(set(nodes)) and all(re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9_.-]*', node) for node in nodes),
+ 'A10 requires four distinct current Heal nodes')
+ results = []
+ try:
+ for topology in ('H1', 'H2'):
+ for branch in BRANCHES:
+ for fixture in FIXTURES:
+ for phase in source_phases(branch):
+ row = run_instance(args, topology, nodes[:1] if topology == 'H1' else nodes[:4], branch, fixture, phase)
+ results.append(row)
+ if row['cleanup'] != 'PASS': return
+ finally:
+ complete = complete_results(results)
+ save(args.evidence / 'healthy-summary.json', {'caseId': CASE_ID, 'results': results, 'complete': complete,
+ 'cleanup': 'PASS' if results and all(row['cleanup'] == 'PASS' for row in results) else 'FAIL'})
+ require(complete, 'A10 requires every registered H1/H2 DB/DV source option instance and complete cleanup')
+
+
+if __name__ == '__main__':
+ try: main()
+ except Exception as exc:
+ print(f'A10 option validation failed: {exc}', file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_heal/orphan.py b/scripts/validation_heal/orphan.py
new file mode 100644
index 00000000..4c5f19ae
--- /dev/null
+++ b/scripts/validation_heal/orphan.py
@@ -0,0 +1,514 @@
+#!/usr/bin/env python3
+"""C10: actual grace, exact shared references and pending-MPU preservation."""
+
+import argparse
+from datetime import datetime, timedelta, timezone
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import secrets
+import signal
+import subprocess
+import sys
+import time
+import uuid
+
+try:
+ from .authority import object_files
+ from .heal_probe import Probe, canonical, compare_inventories, payload, require, save
+ from .heal_workload import Recorder, verify_seed
+ from .healthy import Fleet, physical_content, validate_topology
+ from .inspector import verify as verify_inspector
+ from .matrix import select
+ from .native_regressions import CANDIDATE
+ from .shared_fixture import verify as verify_shared
+ from .shards import files, map_target, run_task, validate_absent_request
+ from .scopes import verify_parts
+ from .stop import idle_fleet
+except ImportError:
+ from authority import object_files
+ from heal_probe import Probe, canonical, compare_inventories, payload, require, save
+ from heal_workload import Recorder, verify_seed
+ from healthy import Fleet, physical_content, validate_topology
+ from inspector import verify as verify_inspector
+ from matrix import select
+ from native_regressions import CANDIDATE
+ from shared_fixture import verify as verify_shared
+ from shards import files, map_target, run_task, validate_absent_request
+ from scopes import verify_parts
+ from stop import idle_fleet
+
+
+CASE_ID = 'heal.dangling-and-orphan-safety'
+BRANCHES = ('remove-false', 'remove-true-before-grace', 'remove-true-after-grace', 'shared-version-reference', 'pending-mpu')
+SIZE = 2 * 1024**2 + 123
+KEY = 'orphan.bin'
+
+
+def members(snapshot):
+ return {(node, row['disk']) for node, value in snapshot.items() for row in value['formats']}
+
+
+def references(snapshot, bucket, key):
+ indexed = files(snapshot)
+ result = {}
+ for node, disk in members(snapshot):
+ metadata = indexed.get((node, disk, f'{bucket}/{key}/xl.meta'), {}).get('metadata', {})
+ require(not metadata or metadata.get('bucket') == bucket and metadata.get('key') == key,
+ 'C10 physical metadata changed bucket/key identity')
+ values = metadata.get('versions', [])
+ require(len({value['versionId'] for value in values}) == len(values), 'C10 duplicate physical version identity')
+ result[(node, disk)] = {value['versionId']: value for value in values}
+ return result
+
+
+def require_shared(snapshot, bucket, versions):
+ require(len(versions) == 2 and len(set(versions)) == 2, 'C10 shared fixture needs two independent ACK versions')
+ for values in references(snapshot, bucket, KEY).values():
+ live = [values.get(version) for version in versions]
+ require(all(value and value.get('deleted') is False and value.get('inline') is None for value in live)
+ and live[0].get('dataDir') and live[0]['dataDir'] == live[1].get('dataDir'),
+ 'C10 lost a shared live version reference')
+
+
+def physical_safety(before, after, authority, bucket, expected, branch, doomed, directory, target):
+ require(members(before) == members(after) == members(authority), 'C10 lost an original physical member')
+ require({node: value['formats'] for node, value in before.items()} ==
+ {node: value['formats'] for node, value in after.items()}, 'C10 changed original format/UUID/set identity')
+ left, right = files(before), files(after)
+ prefix = bucket + '/' + KEY + '/'
+ controls = lambda values: {identity: value for identity, value in values.items() if not identity[2].startswith(prefix)}
+ require(controls(left) == controls(right), 'C10 changed control bytes or decoded metadata')
+ original_refs, final_refs = references(authority, bucket, KEY), references(after, bucket, KEY)
+ selected = [entry for entry in expected if entry['key'] == KEY]
+ wanted = {entry['versionId']: entry for entry in selected}
+ for member, values in final_refs.items():
+ allowed = set(wanted)
+ if branch == 'remove-true-before-grace' and member == target: allowed.add(doomed)
+ require(set(values) == allowed, 'C10 lost, invented or resurrected a version/marker reference')
+ for version, entry in wanted.items():
+ old, new = original_refs[member].get(version), values.get(version)
+ require(old and new and new.get('deleted') is entry['deleteMarker']
+ and new.get('dataDir') == old.get('dataDir'), 'C10 live version/marker or dataDir changed')
+ if entry['deleteMarker']: continue
+ data_prefix = prefix + old['dataDir'] + '/'
+ original = {identity: value for identity, value in left.items() if identity[:2] == member and identity[2].startswith(data_prefix)}
+ actual = {identity: value for identity, value in right.items() if identity[:2] == member and identity[2].startswith(data_prefix)}
+ require(original and actual == original, 'C10 changed referenced payloads or integrity sidecars')
+ if branch == 'pending-mpu':
+ require(physical_content(before) == physical_content(after), 'C10 Heal changed an unfaulted pending-MPU control object')
+ elif branch == 'remove-false':
+ require(physical_content(before) == physical_content(after), 'C10 remove=false changed protected orphan evidence')
+ elif branch == 'remove-true-before-grace':
+ old = {identity: value for identity, value in left.items() if identity[:2] == target and identity[2].startswith(prefix + directory + '/')}
+ new = {identity: value for identity, value in right.items() if identity[:2] == target and identity[2].startswith(prefix + directory + '/')}
+ require(old and old == new and final_refs[target][doomed].get('dataDir') == directory,
+ 'C10 pre-grace deleted protected stale metadata or shards')
+ else:
+ require(not any(identity[2].startswith(prefix + directory + '/') for identity in right),
+ 'C10 expired or unreferenced replay was not actually removed')
+ if branch == 'shared-version-reference':
+ require_shared(after, bucket, [entry['versionId'] for entry in selected if not entry['deleteMarker']])
+ return {'allOriginalMembersChecked': len(members(after)), 'liveReferencesAndPartsPreserved': True,
+ 'healthyControlsAndFormatsPreserved': True, 'backupRescueIsProductRepair': False}
+
+
+def terminal_safety(status, branch, bucket, options):
+ if branch == 'remove-false':
+ return validate_absent_request(status, bucket, KEY, options)
+ require(status.get('summary') == 'finished' and status.get('truncated', False) is False
+ and status.get('settings') == options, 'C10 terminal receipt is absent/truncated or changed its request')
+ outcome = status.get('outcome') or {}
+ counters = outcome.get('counters') or {}
+ require(outcome.get('coverage') == 'complete' and outcome.get('objectsTruncated', False) is False
+ and all(type(counters.get(name)) is int and counters[name] >= 0 for name in ('processed', 'healed', 'failed', 'unknown'))
+ and counters['unknown'] == 0, 'C10 responsibility is missing, partial or unknown')
+ if branch == 'remove-true-before-grace':
+ deferred = [value for value in outcome.get('objects', [])
+ if value.get('identity', {}).get('bucket') == bucket and value.get('identity', {}).get('object') == KEY
+ and value.get('disposition', {}).get('state') == 'deferred'
+ and value['disposition'].get('details', {}).get('reason') == 'dangling_delete_grace']
+ require(deferred and counters['failed'] == 0 and outcome.get('execution', {}).get('state') == 'completed',
+ 'C10 pre-grace preservation lacks an exact typed deferred responsibility')
+ else:
+ require(counters['failed'] == 0 and outcome.get('execution', {}).get('state') == 'completed',
+ 'C10 physical safety has no successful terminal responsibility')
+ return {'canonicalCounters': counters, 'fullOriginalResponsibility': True}
+
+
+def capture(fleet, snapshot, bucket, label):
+ result = {}
+ for (node, disk), expected in sorted(object_files(snapshot, bucket, KEY).items()):
+ params = {'disk': disk, 'bucket': bucket, 'key': KEY}
+ observed = fleet.record('C10-stopped-object-presence', {'node': node, **fleet.remote(node, 'orphan-observe', **params)})
+ require(observed['files'] == expected and abs(observed['nodeEpoch'] - time.time()) <= 3,
+ 'C10 stopped archive or real host clock differs from the physical witness')
+ result[(node, disk)] = fleet.record('C10-archive-' + label, {'node': node, **fleet.remote(node, 'orphan-capture',
+ **params, generation=label, expectedFiles=expected, expectedPresent=observed['present'])})
+ return result
+
+
+def arm_all(fleet, snapshot, bucket, label):
+ capture(fleet, snapshot, bucket, label)
+ for node, disk in sorted(members(snapshot)):
+ params = {'disk': disk, 'bucket': bucket, 'key': KEY, 'generation': label}
+ # Register before SSH: a lost arm receipt leaves the whole namespace DIRTY.
+ fleet.orphan_faults.append((node, params))
+ fleet.record('C10-arm-original-member', {'node': node, **fleet.remote(node, 'orphan-arm', **params)})
+
+
+def recover_all(fleet):
+ for node, params in tuple(fleet.orphan_faults):
+ receipt = fleet.record('C10-recover-original-member', {'node': node, **fleet.remote(node, 'orphan-recover', **params)})
+ require(receipt.get('state') == 'CLEAR' and receipt.get('lastACKAuthorityRestored') is True,
+ 'C10 original member is not completely recovered')
+ fleet.orphan_faults.remove((node, params))
+
+
+def pending_snapshot(fleet):
+ return {(node, disk): fleet.record('C10-read-only-MPU-census', {'node': node, 'disk': disk,
+ **fleet.remote(node, 'orphan-multipart', disk=disk)})['files'] for node in fleet.nodes for disk in range(4)}
+
+
+def verify_live_parts(fleet, mapping, label):
+ require(mapping, 'C10 streaming manifest is empty')
+ total, identities = 0, set()
+ for identity, target in mapping.items():
+ require(identity == (target['bucket'], target['key'], target['versionId']),
+ 'C10 streaming manifest lost its exact version-addressed authority')
+ # Two independently ACKed shared versions deliberately reference the
+ # same path. Each version must validate its own metadata commitment;
+ # the general selector verifier still rejects aliases within a cohort.
+ proof = verify_parts(fleet, {identity: target}, label)
+ total += proof['exactStreamingParts']
+ identities.update((member['node'], member['disk'], member['relativePath']) for member in target['cohort'])
+ return {'versionAddressedStreamingParts': total, 'distinctPhysicalParts': len(identities),
+ 'exactACKVersions': len(mapping), 'sharedPathsCountedAsIndependentDisks': False}
+
+
+def verify_public(fleet, binding, bucket, expected, deleted, label):
+ for endpoint in binding['endpoints']:
+ peer = Probe(binding, endpoint)
+ inventory = fleet.record(label + '-full-inventory', peer.inventory(bucket))
+ fleet.record(label + '-ACK-comparison', verify_seed(expected, inventory))
+ for version in deleted:
+ observed = fleet.record(label + '-deleted-version', peer.s3('get_object', {'Bucket': bucket, 'Key': KEY, 'VersionId': version}))
+ response = observed.get('response') or {}
+ require(observed['acknowledged'] is False and observed['resultUnknown'] is False and 'payload' not in observed
+ and response.get('ResponseMetadata', {}).get('HTTPStatusCode') == 404
+ and response.get('Error', {}).get('Code') in {'NoSuchVersion', 'NoSuchKey'}, 'C10 deleted version resurrected')
+ current = next((entry for entry in expected if entry['key'] == KEY and entry['isLatest']), None)
+ if current is None or current['deleteMarker']:
+ observed = fleet.record(label + '-current-absence', peer.s3('get_object', {'Bucket': bucket, 'Key': KEY}))
+ response = observed.get('response') or {}
+ headers = response.get('ResponseMetadata', {}).get('HTTPHeaders', {})
+ require(not observed['acknowledged'] and not observed['resultUnknown'] and 'payload' not in observed
+ and response.get('ResponseMetadata', {}).get('HTTPStatusCode') == 404
+ and response.get('Error', {}).get('Code') == 'NoSuchKey', 'C10 absent current/marker resurrected')
+ if current:
+ require(headers.get('x-amz-delete-marker') == 'true'
+ and headers.get('x-amz-version-id') in {None, current['versionId']}, 'C10 current marker header/identity changed')
+
+
+def run_branch(args, topology, nodes, branch):
+ directory = args.evidence / topology / branch
+ directory.mkdir(parents=True)
+ candidate = json.loads(args.candidate.read_text())
+ require(candidate['source_sha'] == CANDIDATE, 'C10 requires its reviewed candidate codec/grace policy')
+ result = {'topology': topology, 'branch': branch, 'verdict': 'RUNNING', 'cleanup': 'PENDING'}
+ run_id = f"gh-{os.environ['GITHUB_RUN_ID']}-{os.environ.get('GITHUB_RUN_ATTEMPT', '1')}-{topology.lower()}"
+ runtime = 'orphan-before' if branch == BRANCHES[1] else 'orphan-after' if branch == BRANCHES[2] else 'orphan-preserve'
+ grace = 300 if runtime == 'orphan-before' else 30 if runtime == 'orphan-after' else 0
+ fleet = Fleet(nodes, directory, run_id, args.tool_directory, runtime_profile=runtime)
+ bucket = 'rv-heal-' + hashlib.sha256(run_id.encode()).hexdigest()[:16]
+ access, secret = secrets.token_hex(16), secrets.token_hex(24)
+ os.environ.update(HEAL_ACCESS_KEY=access, HEAL_SECRET_KEY=secret)
+ binding = {'schemaVersion': 1, 'runId': run_id, 'jumpHostOnly': True, 'exclusiveHealService': True,
+ 'leaseExpiresAt': (datetime.now(timezone.utc) + timedelta(minutes=30)).isoformat(), 'bucketPrefix': bucket,
+ 'endpoints': {f'node{index}': f'http://rustfs-node{index}:9000' for index in range(1, len(nodes) + 1)},
+ 'defaultEndpoint': 'node1', 'candidateCommit': candidate['source_sha'], 'harnessCommit': args.testing_commit,
+ 'candidatePackageSha256': candidate['package_sha256'], 'candidateBinarySha256': args.binary_sha,
+ 'rcBinarySha256': hashlib.sha256(args.rc_bin.read_bytes()).hexdigest(), 'maxInventoryObjects': 128,
+ 'maxInventoryBytes': 512 * 1024**2}
+ fleet.record('binding', binding)
+ fleet.record('inspector-build', verify_inspector(args.tool_directory, candidate))
+ shared = fleet.record('shared-encoder-build', verify_shared(args.shared_directory, candidate))
+ probe, active, pending_mutation = Probe(binding), [], None
+ expected, deleted, recovery_snapshot = [], [], None
+ try:
+ def install_tool():
+ if branch != 'shared-version-reference': return
+ raw = (args.shared_directory / 'heal-shared-fixture').read_bytes()
+ for node in nodes:
+ path = '/var/lib/rustfs-auto-testing/heal/' + run_id + '/heal-shared-fixture'
+ receipt = subprocess.run([*fleet.ssh(node), 'sudo -n dd of=' + path + ' conv=excl,fsync oflag=nofollow bs=1M status=none'],
+ input=raw, capture_output=True, timeout=120)
+ require(receipt.returncode == 0, 'C10 exclusive private encoder upload failed')
+ fleet.record('C10-install-owned-encoder', {'node': node, **fleet.remote(node, 'orphan-install-tool', sha256=shared['binarySha256'])})
+ fleet.deploy(args.binary_sha, bucket, access, secret, before_start=install_tool)
+ recorder = Recorder(probe, directory / 'fixture')
+ def call(operation, params, body=None, check=None):
+ nonlocal pending_mutation
+ pending_mutation = {'operation': operation, 'params': params}
+ response = recorder.call(operation, params, body)
+ if check is not None: check(response)
+ pending_mutation = None
+ return response
+ def unique_version(response):
+ version = response.get('VersionId')
+ require(isinstance(version, str) and str(uuid.UUID(version)) == version and uuid.UUID(version).int != 0
+ and version not in {entry['versionId'] for entry in expected} | set(deleted),
+ 'C10 ACK lost its distinct exact non-null version identity')
+ def put(key, label, raw=None):
+ body = payload('C10/' + label, SIZE) if raw is None else raw
+ response = call('put_object', {'Bucket': bucket, 'Key': key, 'ContentType': 'application/octet-stream',
+ 'Metadata': {'heal-fixture': 'C10', 'ack': label}}, body, unique_version)
+ for entry in expected:
+ if entry['key'] == key: entry['isLatest'] = False
+ entry = {'key': key, 'versionId': response['VersionId'], 'isLatest': True, 'deleteMarker': False,
+ 'payload': {'size': len(body), 'sha256': hashlib.sha256(body).hexdigest()}}
+ expected.append(entry)
+ head = fleet.record('C10-server-generation-age', probe.s3('head_object', {'Bucket': bucket, 'Key': key, 'VersionId': entry['versionId']}))
+ require(head['acknowledged'], 'C10 generation age has no successful server HEAD')
+ info = head['response']
+ require(info.get('ContentLength') == len(body) and info.get('ETag') == response.get('ETag')
+ and info.get('ContentType') == 'application/octet-stream'
+ and info.get('Metadata') == {'heal-fixture': 'C10', 'ack': label}, 'C10 HEAD differs from original PUT ACK/properties')
+ entry.update(properties={name: info[name] for name in (
+ 'ETag', 'ContentLength', 'ContentType', 'ContentEncoding', 'ContentDisposition', 'CacheControl', 'Metadata',
+ 'StorageClass', 'ServerSideEncryption', 'SSEKMSKeyId', 'SSECustomerAlgorithm', 'SSECustomerKeyMD5',
+ 'ObjectLockMode', 'ObjectLockRetainUntilDate', 'ObjectLockLegalHoldStatus') if name in info}, tags=[])
+ value = head['response']['LastModified']
+ epoch = value.timestamp() if isinstance(value, datetime) else datetime.fromisoformat(value).timestamp()
+ require(abs(epoch - time.time()) <= 5, 'C10 fixture server clock/age precondition failed')
+ return entry, epoch
+ call('create_bucket', {'Bucket': bucket})
+ call('put_bucket_versioning', {'Bucket': bucket, 'VersioningConfiguration': {'Status': 'Enabled'}})
+ put('control.bin', 'control')
+ # A separate acknowledged marker control exercises DM preservation in every branch.
+ marker_data, _ = put('marker-control.bin', 'marker-control')
+ def marker_ack(response):
+ unique_version(response)
+ require(response.get('DeleteMarker') is True and response.get('ResponseMetadata', {}).get('HTTPStatusCode') == 204,
+ 'C10 marker DELETE lacks its exact successful ACK')
+ marker = call('delete_object', {'Bucket': bucket, 'Key': 'marker-control.bin'}, check=marker_ack)
+ marker_data['isLatest'] = False
+ expected.append({'key': 'marker-control.bin', 'versionId': marker['VersionId'], 'deleteMarker': True, 'isLatest': True})
+ raw = payload('C10/shared-first', SIZE)
+ first, first_time = put(KEY, 'first', raw)
+ if branch == 'shared-version-reference':
+ second, _ = put(KEY, 'second', raw)
+ baseline = fleet.record('C10-independent-original-ACK-S3', probe.inventory(bucket))
+ verify_seed(expected, baseline)
+ fleet.stop()
+ seed = fleet.snapshot('C10-stopped-shared-seed', quiescent=True)
+ validate_topology(seed, topology)
+ source_map = map_target(seed, bucket, KEY, 'data', 1, first['versionId'], latest_required=False)
+ dest_map = map_target(seed, bucket, KEY, 'data', 1, second['versionId'])
+ recovery_snapshot = seed
+ arm_all(fleet, seed, bucket, 'seed')
+ for left, right in zip(source_map['cohort'], dest_map['cohort'], strict=True):
+ require((left['node'], left['disk']) == (right['node'], right['disk']), 'C10 shared member ordering changed')
+ fleet.record('C10-exact-codec-shared-conversion', {'node': left['node'], **fleet.remote(left['node'], 'orphan-convert',
+ disk=left['disk'], bucket=bucket, key=KEY, sourceVersion=first['versionId'], destinationVersion=second['versionId'],
+ sourceDataDir=left['dataDir'], destinationDataDir=right['dataDir'])})
+ converted = fleet.snapshot('C10-shared-all-original-physical', quiescent=True)
+ require_shared(converted, bucket, [first['versionId'], second['versionId']])
+ fleet.start()
+ for endpoint in binding['endpoints']:
+ compare_inventories(baseline, fleet.record('C10-shared-all-peer-S3', Probe(binding, endpoint).inventory(bucket)))
+ fleet.stop()
+ confirmed = fleet.snapshot('C10-shared-before-later-ACK', quiescent=True)
+ require(physical_content(converted) == physical_content(confirmed), 'C10 shared representation drifted after GET')
+ for node, params in tuple(fleet.orphan_faults):
+ receipt = fleet.record('C10-retire-seed-rescue-before-later-ACK', {'node': node, **fleet.remote(node, 'orphan-disarm',
+ **params, expectedFiles=object_files(confirmed, bucket, KEY)[(node, params['disk'])])})
+ require(receipt.get('state') == 'RETIRED' and receipt.get('laterACKMayProceed') is True, 'C10 seed rescue was not retired')
+ fleet.orphan_faults.remove((node, params))
+ recovery_snapshot = None
+ fleet.start()
+ doomed, doomed_time = put(KEY, 'doomed-third')
+ else:
+ doomed, doomed_time = first, first_time
+ if branch.startswith('remove-true-'): put(KEY, 'second-retained')
+ fleet.record('C10-full-baseline-S3', verify_seed(expected, probe.inventory(bucket)))
+ fleet.stop()
+ original = fleet.snapshot('C10-all-original-generations', quiescent=True)
+ validate_topology(original, topology)
+ doomed_map = map_target(original, bucket, KEY, 'data', 1, doomed['versionId'], latest_required=not branch.startswith('remove-true-'))
+ target = doomed_map['node'], doomed_map['disk']
+ data_dir = doomed_map['dataDir']
+ if branch == 'pending-mpu':
+ fleet.start()
+ upload = call('create_multipart_upload', {'Bucket': bucket, 'Key': KEY}, check=lambda response:
+ require(isinstance(response.get('UploadId'), str) and response['UploadId'], 'C10 MPU create ACK omitted UploadId'))['UploadId']
+ pending_body = payload('C10/pending-part', SIZE)
+ part = call('upload_part', {'Bucket': bucket, 'Key': KEY, 'UploadId': upload, 'PartNumber': 1}, pending_body,
+ lambda response: require(isinstance(response.get('ETag'), str) and response['ETag'], 'C10 part ACK omitted ETag'))
+ fleet.stop()
+ before = authority = fleet.snapshot('C10-pending-MPU-object-before', quiescent=True)
+ pending_before = pending_snapshot(fleet)
+ require(len(pending_before) == (4 if topology == 'H1' else 16) and all(pending_before.values()),
+ 'C10 did not witness actual pending-MPU files on every original root')
+ fleet.start()
+ else:
+ capture(fleet, original, bucket, 'old')
+ fleet.start()
+ def delete_ack(response):
+ require(response.get('VersionId') == doomed['versionId'] and response.get('DeleteMarker') in {None, False}
+ and response.get('ResponseMetadata', {}).get('HTTPStatusCode') == 204, 'C10 exact-version DELETE ACK mismatch')
+ call('delete_object', {'Bucket': bucket, 'Key': KEY, 'VersionId': doomed['versionId']}, check=delete_ack)
+ deleted.append(doomed['versionId']); expected.remove(doomed)
+ if branch == 'shared-version-reference':
+ marker = call('delete_object', {'Bucket': bucket, 'Key': KEY}, check=marker_ack)
+ for entry in expected:
+ if entry['key'] == KEY: entry['isLatest'] = False
+ expected.append({'key': KEY, 'versionId': marker['VersionId'], 'deleteMarker': True, 'isLatest': True})
+ fleet.record('C10-post-delete-all-S3-authority', verify_seed(expected, probe.inventory(bucket)))
+ fleet.stop()
+ authority = fleet.snapshot('C10-post-delete-all-physical-authority', quiescent=True)
+ require(all(doomed['versionId'] not in values for values in references(authority, bucket, KEY).values())
+ and not any(identity[2].startswith(bucket + '/' + KEY + '/' + data_dir + '/') for identity in files(authority)),
+ 'C10 DELETE did not remove the exact stale reference/payload before controlled replay')
+ recovery_snapshot = authority
+ arm_all(fleet, authority, bucket, 'authority')
+ fleet.record('C10-controlled-single-root-replay', {'node': target[0], **fleet.remote(target[0], 'orphan-install',
+ disk=target[1], bucket=bucket, key=KEY, dataDir=data_dir,
+ mode='object' if branch.startswith('remove-true-') else 'data')})
+ before = fleet.snapshot('C10-fault-before-GET', quiescent=True)
+ present = {identity[:2] for identity in files(before) if identity[2].startswith(bucket + '/' + KEY + '/' + data_dir + '/')}
+ require(present == {target}, 'C10 replay is not confined to one exact original member')
+ fleet.start()
+ if branch == 'remove-true-after-grace':
+ while time.time() < doomed_time + grace + 4: time.sleep(min(1, max(0, doomed_time + grace + 4 - time.time())))
+ age = time.time() - doomed_time
+ require(branch != 'remove-true-before-grace' or 0 <= age < grace - 5, 'C10 missed the real pre-grace window')
+ require(branch != 'remove-true-after-grace' or age >= grace + 3, 'C10 real grace period has not elapsed')
+ fleet.record('C10-real-age-trigger', {'ageSeconds': age, 'serverLastModifiedEpoch': doomed_time, 'graceSeconds': grace,
+ 'globalClockChanged': False, 'remove': branch != 'remove-false'})
+ from_options = {'recursive': branch != 'remove-false', 'remove': branch != 'remove-false'}
+ try: from .healthy import BASE_OPTIONS
+ except ImportError: from healthy import BASE_OPTIONS
+ options = {**BASE_OPTIONS, 'scanMode': 2, **from_options}
+ status = run_task(probe, fleet, {'bucket': bucket, 'key': KEY if branch == 'remove-false' else ''}, 2, active,
+ status_endpoints=tuple(binding['endpoints'])[1:], option_overrides=from_options)
+ idle_fleet(fleet, binding)
+ fleet.stop()
+ after = fleet.snapshot('C10-product-physical-before-target-GET', quiescent=True)
+ result['physical'] = physical_safety(before, after, authority, bucket, expected, branch,
+ doomed['versionId'], data_dir, target)
+ result['terminal'] = terminal_safety(status, branch, bucket, options)
+ # Product S3 behavior must be observed before fixture rescue can hide
+ # lost references, resurrected versions or changed current semantics.
+ fleet.start()
+ verify_public(fleet, binding, bucket, expected, deleted, 'C10-product-before-rescue')
+ fleet.stop()
+ if branch == 'pending-mpu':
+ require(pending_before == pending_snapshot(fleet), 'C10 Heal mutated pending-MPU bytes')
+ fleet.start()
+ for endpoint in binding['endpoints']:
+ observed = fleet.record('C10-all-peer-list-parts', Probe(binding, endpoint).s3('list_parts',
+ {'Bucket': bucket, 'Key': KEY, 'UploadId': upload}))
+ response = observed.get('response') or {}
+ require(observed['acknowledged'] and response.get('IsTruncated', False) is False
+ and [{'number': row['PartNumber'], 'size': row['Size'], 'etag': row['ETag']} for row in response.get('Parts', [])]
+ == [{'number': 1, 'size': SIZE, 'etag': part['ETag']}], 'C10 pending UploadId/part ACK changed')
+ completed = call('complete_multipart_upload', {'Bucket': bucket, 'Key': KEY, 'UploadId': upload,
+ 'MultipartUpload': {'Parts': [{'PartNumber': 1, 'ETag': part['ETag']}]}}, check=unique_version)
+ for entry in expected:
+ if entry['key'] == KEY: entry['isLatest'] = False
+ expected.append({'key': KEY, 'versionId': completed['VersionId'], 'isLatest': True, 'deleteMarker': False,
+ 'payload': {'size': SIZE, 'sha256': hashlib.sha256(pending_body).hexdigest()}})
+ result['pendingMPUPhysicalAndListPartsPreserved'] = True
+ else:
+ # Preserve the product result above, then undo only the controlled
+ # replay using all original post-DELETE authorities before GET.
+ recover_all(fleet)
+ recovered = fleet.snapshot('C10-restored-last-DELETE-before-GET', quiescent=True)
+ require(physical_content(recovered) == physical_content(authority), 'C10 restoration changed last DELETE authority')
+ recovery_snapshot = None
+ fleet.start()
+ fleet.stop()
+ restored = fleet.snapshot('C10-final-current-ACK-physical', quiescent=True)
+ mapping = {(bucket, entry['key'], entry['versionId']): map_target(restored, bucket, entry['key'], 'data', 1,
+ entry['versionId'], latest_required=entry['isLatest']) for entry in expected if not entry['deleteMarker']}
+ result['streaming'] = verify_live_parts(fleet, mapping, 'C10-all-live-ACK-streaming')
+ fleet.start()
+ verify_public(fleet, binding, bucket, expected, deleted, 'C10-all-peer-final-S3')
+ result.update(verdict='PASS', postDeleteAuthorityRestored=True, backupRestorationIsProductRepair=False)
+ except (Exception, KeyboardInterrupt) as exc:
+ result.update(verdict='FAIL', failure={'type': type(exc).__name__, 'message': str(exc)})
+ finally:
+ for path, token in active:
+ try: fleet.record('C10-stop-unresolved-task', probe.admin(path, [('clientToken', token), ('forceStop', 'true')], b''))
+ except Exception as exc: fleet.record('C10-stop-error', {'error': str(exc)})
+ if pending_mutation is not None:
+ try: fleet.stop()
+ except Exception as exc: fleet.record('C10-retain-stop-error', {'error': str(exc)})
+ fleet.record('C10-uncertain-ACK-retained', {'state': 'DIRTY', 'request': pending_mutation,
+ 'namespacesRetained': True, 'oldAuthorityRestorationProhibited': True})
+ clean = False
+ else:
+ recovery_ok = True
+ if fleet.orphan_faults:
+ try:
+ fleet.stop()
+ for node in fleet.nodes:
+ fleet.record('C10-before-rescue-original-logs', {'node': node, **fleet.remote(node, 'logs')})
+ recover_all(fleet)
+ restored = fleet.snapshot('C10-failure-restored-authority', quiescent=True)
+ require(recovery_snapshot is not None and physical_content(restored) == physical_content(recovery_snapshot),
+ 'C10 failed-case restoration differs from the last known ACK authority')
+ fleet.start()
+ verify_public(fleet, binding, bucket, expected, deleted, 'C10-failure-rescued-S3')
+ except Exception as exc:
+ fleet.record('C10-recovery-DIRTY', {'state': 'DIRTY', 'namespacesRetained': True, 'error': str(exc)})
+ try: fleet.stop()
+ except Exception as stop_error: fleet.record('C10-recovery-stop-error', {'error': str(stop_error)})
+ recovery_ok = False
+ clean = fleet.cleanup() if recovery_ok else False
+ result['cleanup'] = 'PASS' if clean else 'FAIL'
+ fleet.record('branch-result', result)
+ return result
+
+
+def main():
+ def interrupted(signum, frame): raise KeyboardInterrupt(f'C10 interrupted by signal {signum}')
+ signal.signal(signal.SIGTERM, interrupted)
+ parser = argparse.ArgumentParser(description=__doc__)
+ for name in ('candidate', 'rc-bin', 'evidence', 'tool-directory'):
+ parser.add_argument('--' + name, type=Path, required=True)
+ parser.add_argument('--shared-directory', type=Path, default=os.environ.get('VALIDATION_HEAL_SHARED_DIRECTORY'))
+ parser.add_argument('--case-id', choices=[CASE_ID], required=True)
+ parser.add_argument('--testing-commit', required=True)
+ parser.add_argument('--binary-sha', required=True)
+ args = parser.parse_args()
+ require(re.fullmatch(r'[0-9a-f]{64}', args.binary_sha) and re.fullmatch(r'[1-9][0-9]*', os.environ.get('GITHUB_RUN_ID', '')),
+ 'C10 requires immutable workflow/binary identities')
+ require(select(CASE_ID)[0]['variants'] == list(BRANCHES), 'C10 omitted a source branch')
+ require(args.shared_directory is not None, 'C10 requires its successful exact-candidate encoder before deployment')
+ save(args.evidence / 'shared-preflight.json', verify_shared(args.shared_directory, json.loads(args.candidate.read_text())))
+ nodes = os.environ.get('RUSTFS_NODES', 'vm000 vm001 vm002 vm003').split()
+ require(len(nodes) >= 4 and len(nodes) == len(set(nodes)) and all(re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9_.-]*', node) for node in nodes),
+ 'C10 requires four distinct current Heal nodes')
+ results = []
+ try:
+ for topology in ('H1', 'H2'):
+ for branch in BRANCHES:
+ results.append(run_branch(args, topology, nodes[:1] if topology == 'H1' else nodes[:4], branch))
+ if results[-1]['cleanup'] != 'PASS': break
+ if results[-1]['cleanup'] != 'PASS': break
+ finally:
+ complete = len(results) == 10 and all(row['verdict'] == row['cleanup'] == 'PASS' for row in results)
+ save(args.evidence / 'healthy-summary.json', {'caseId': CASE_ID, 'results': results, 'complete': complete,
+ 'cleanup': 'PASS' if results and all(row['cleanup'] == 'PASS' for row in results) else 'FAIL'})
+ require(complete, 'C10 requires all five actual H1/H2 branches and complete original-member cleanup')
+
+
+if __name__ == '__main__':
+ try: main()
+ except Exception as exc:
+ print(f'C10 orphan validation failed: {exc}', file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_heal/orphan_fault.py b/scripts/validation_heal/orphan_fault.py
new file mode 100644
index 00000000..da6d02a4
--- /dev/null
+++ b/scripts/validation_heal/orphan_fault.py
@@ -0,0 +1,237 @@
+"""C10 stopped descriptor archives, shared conversion and post-delete recovery."""
+
+import json
+import os
+import re
+import stat
+import subprocess
+import uuid
+
+
+def require(value, message):
+ if not value:
+ raise ValueError(message)
+
+
+def scope(owner, request):
+ require(owner.get('runtimeProfile') in {'orphan-preserve', 'orphan-before', 'orphan-after', 'outcome-grace'}
+ and request.get('bucket') == owner['bucket'] and request.get('key') == 'orphan.bin',
+ 'C10 escaped its registered runtime or exact owned object')
+ require(type(request.get('disk')) is int and 0 <= request['disk'] < 4, 'C10 original disk absent')
+ return [owner['bucket'], 'orphan.bin']
+
+
+def scene(root, owner, request, fault, generation):
+ parts = scope(owner, request)
+ with fault.parent(root, ['data', parts[0]]) as bucket:
+ if parts[1] not in os.listdir(bucket):
+ return False, {}, []
+ with fault.parent(bucket, [parts[1]]) as obj:
+ values, files = generation.tree(obj, fault, allow_sparse=True)
+ return True, values, files
+
+
+def identity(root, owner, request, fault):
+ return {'schemaVersion': 1, 'runId': owner['runId'], 'disk': request['disk'],
+ 'bucket': owner['bucket'], 'key': 'orphan.bin', 'mountId': fault.mount_id(root),
+ 'device': os.fstat(root).st_dev, 'binarySha256': owner['binarySha256']}
+
+
+def capture(root, owner, request, fault, generation):
+ label = request['generation']
+ require(label in {'seed', 'old', 'authority'}, 'unregistered C10 archive')
+ present, values, files = scene(root, owner, request, fault, generation)
+ require(files == request['expectedFiles'] and present == request['expectedPresent'],
+ 'C10 archive differs from its complete stopped physical witness')
+ record = {**identity(root, owner, request, fault), 'generation': label, 'present': present,
+ 'files': files, 'treeSha256': generation.tree_sha(files, fault), 'productRepairClaimed': False}
+ with fault.parent(root, ['quarantine']) as quarantine:
+ os.mkdir('C10-' + label, 0o700, dir_fd=quarantine)
+ os.fsync(quarantine)
+ with fault.parent(quarantine, ['C10-' + label]) as saved:
+ fault.new_file(saved, 'manifest.json', fault.encode(record))
+ os.mkdir('object', 0o700, dir_fd=saved)
+ with fault.parent(saved, ['object']) as obj:
+ generation.copy_into(obj, values, fault)
+ require(generation.tree(obj, fault, allow_sparse=True)[1] == files, 'C10 archive incomplete')
+ fault.new_file(saved, 'complete.json', fault.encode(record))
+ return record
+
+
+def captured(root, owner, request, label, fault, generation):
+ scope(owner, request)
+ require(label in {'seed', 'old', 'authority'}, 'unregistered C10 saved generation')
+ with fault.parent(root, ['quarantine', 'C10-' + label]) as saved:
+ record = json.loads(fault.read_file(saved, 'complete.json', 65536))
+ require(record == json.loads(fault.read_file(saved, 'manifest.json', 65536))
+ and all(record.get(key) == value for key, value in identity(root, owner, request, fault).items())
+ and record['generation'] == label and type(record['present']) is bool,
+ 'C10 archive candidate, ownership, device or mount changed')
+ with fault.parent(saved, ['object']) as obj:
+ values, files = generation.tree(obj, fault, allow_sparse=True)
+ require(files == record['files'] and generation.tree_sha(files, fault) == record['treeSha256']
+ and (record['present'] or not files), 'C10 archived bytes differ from authority')
+ return values, record
+
+
+def arm(root, owner, request, fault, generation):
+ label = request['generation']
+ require(label in {'seed', 'authority'}, 'C10 recovery cannot restore a deleted old generation')
+ _, record = captured(root, owner, request, label, fault, generation)
+ present, _, files = scene(root, owner, request, fault, generation)
+ require(present == record['present'] and files == record['files'], 'C10 authority drifted before arming')
+ intent = {**identity(root, owner, request, fault), 'restoreGeneration': label,
+ 'originalTreeSha256': record['treeSha256'], 'originalPresent': record['present'], 'productRepairClaimed': False}
+ with fault.parent(root, ['quarantine']) as quarantine:
+ fault.new_file(quarantine, 'C10-' + label + '-intent.json', fault.encode(intent))
+ return {**intent, 'state': 'ARMED'}
+
+
+def intent(root, owner, request, label, fault, generation):
+ values, record = captured(root, owner, request, label, fault, generation)
+ with fault.parent(root, ['quarantine']) as quarantine:
+ value = json.loads(fault.read_file(quarantine, 'C10-' + label + '-intent.json', 65536))
+ require(all(value.get(key) == item for key, item in identity(root, owner, request, fault).items())
+ and value['restoreGeneration'] == label and value['originalTreeSha256'] == record['treeSha256']
+ and value['originalPresent'] == record['present']
+ and 'C10-' + label + '-disarmed.json' not in os.listdir(quarantine),
+ 'C10 recovery authority is absent, foreign or already retired by later ACKs')
+ return values, record, value
+
+
+def install(root, owner, request, fault, generation):
+ parts = scope(owner, request)
+ _, authority, _ = intent(root, owner, request, 'authority', fault, generation)
+ old, _ = captured(root, owner, request, 'old', fault, generation)
+ present, current, files = scene(root, owner, request, fault, generation)
+ require(present == authority['present'] and files == authority['files'], 'C10 last DELETE authority drifted')
+ directory = request['dataDir']
+ require(str(uuid.UUID(directory)) == directory and uuid.UUID(directory).int != 0,
+ 'C10 stale payload has no exact canonical data directory')
+ stale = {name: raw for name, raw in old.items() if name.startswith(directory + '/')}
+ require(stale and not any(name.startswith(directory + '/') for name in current),
+ 'C10 replay would overwrite a live physical reference')
+ mode = request['mode']
+ require(mode in {'object', 'data'}, 'unregistered C10 replay mode')
+ values = old if mode == 'object' else {**current, **stale}
+ with fault.parent(root, ['quarantine']) as quarantine, fault.parent(root, ['data', parts[0]]) as bucket:
+ fault.new_file(quarantine, 'C10-replay-intent.json', fault.encode({'mode': mode, 'dataDir': directory,
+ 'restoreGeneration': 'authority', 'postDeleteTreeSha256': authority['treeSha256']}))
+ require('C10-pre-replay' not in os.listdir(quarantine), 'C10 retained scene already exists')
+ if present:
+ os.rename(parts[1], 'C10-pre-replay', src_dir_fd=bucket, dst_dir_fd=quarantine)
+ else:
+ os.mkdir('C10-pre-replay', 0o700, dir_fd=quarantine)
+ os.fsync(quarantine); os.fsync(bucket)
+ os.mkdir(parts[1], 0o700, dir_fd=bucket)
+ with fault.parent(bucket, [parts[1]]) as obj:
+ generation.copy_into(obj, values, fault)
+ actual, manifest = generation.tree(obj, fault, allow_sparse=True)
+ require(actual == values, 'C10 replay did not preserve all original sidecars and parts')
+ fault.new_file(quarantine, 'C10-replay-applied.json', fault.encode({'mode': mode, 'dataDir': directory,
+ 'files': manifest, 'state': 'APPLIED', 'postDeleteAuthorityRetained': True}))
+ return {'state': 'APPLIED', 'mode': mode, 'dataDir': directory, 'files': manifest}
+
+
+def recover(root, owner, request, fault, generation):
+ parts = scope(owner, request)
+ label = request['generation']
+ values, authority, original_intent = intent(root, owner, request, label, fault, generation)
+ name = 'C10-' + label + '-failed-current'
+ with fault.parent(root, ['quarantine']) as quarantine, fault.parent(root, ['data', parts[0]]) as bucket:
+ if name in os.listdir(quarantine):
+ present, _, files = scene(root, owner, request, fault, generation)
+ require(present == authority['present'] and files == authority['files'],
+ 'C10 interrupted recovery remains DIRTY; never discard a partial restore')
+ else:
+ if parts[1] in os.listdir(bucket):
+ with fault.parent(bucket, [parts[1]]) as obj:
+ require(os.fstat(obj).st_uid == 0, 'C10 live scene no longer root owned')
+ os.rename(parts[1], name, src_dir_fd=bucket, dst_dir_fd=quarantine)
+ else:
+ os.mkdir(name, 0o700, dir_fd=quarantine)
+ os.fsync(bucket); os.fsync(quarantine)
+ if authority['present']:
+ os.mkdir(parts[1], 0o700, dir_fd=bucket)
+ with fault.parent(bucket, [parts[1]]) as obj:
+ generation.copy_into(obj, values, fault)
+ os.fsync(bucket)
+ present, _, files = scene(root, owner, request, fault, generation)
+ require(present == authority['present'] and files == authority['files'], 'C10 authority restoration incomplete')
+ receipt = {**original_intent, 'state': 'CLEAR', 'lastACKAuthorityRestored': True, 'failedScenePreserved': True}
+ receipt_name = 'C10-' + label + '-recovered.json'
+ if receipt_name not in os.listdir(quarantine): fault.new_file(quarantine, receipt_name, fault.encode(receipt))
+ return receipt
+
+
+def disarm(root, owner, request, fault, generation):
+ label = request['generation']
+ require(label == 'seed', 'only a verified shared seed representation may retire recovery before a later ACK')
+ _, _, original = intent(root, owner, request, label, fault, generation)
+ present, _, files = scene(root, owner, request, fault, generation)
+ require(present and files == request['expectedFiles'], 'C10 shared fixture drifted before retiring seed rescue')
+ with fault.parent(root, ['quarantine']) as quarantine:
+ fault.new_file(quarantine, 'C10-seed-disarmed.json', fault.encode({**original,
+ 'state': 'RETIRED', 'verifiedSharedFiles': files, 'laterACKMayProceed': True}))
+ return {'state': 'RETIRED', 'laterACKMayProceed': True}
+
+
+def convert(root, owner, request, fault, generation, tool):
+ scope(owner, request)
+ _, saved, _ = intent(root, owner, request, 'seed', fault, generation)
+ present, values, files = scene(root, owner, request, fault, generation)
+ require(present and files == saved['files'], 'C10 shared seed changed before codec conversion')
+ source, destination = request['sourceDataDir'], request['destinationDataDir']
+ for directory in (source, destination):
+ require(str(uuid.UUID(directory)) == directory and uuid.UUID(directory).int != 0,
+ 'C10 shared directory is not canonical')
+ left = {name.removeprefix(source + '/'): raw for name, raw in values.items()
+ if name.startswith(source + '/') and re.fullmatch(r'part\.[1-9][0-9]*', name.split('/')[-1])}
+ right = {name.removeprefix(destination + '/'): raw for name, raw in values.items()
+ if name.startswith(destination + '/') and re.fullmatch(r'part\.[1-9][0-9]*', name.split('/')[-1])}
+ require(source != destination and left and left == right,
+ 'C10 independently ACKed generations do not contain identical physical payload parts')
+ result = subprocess.run([str(tool), owner['bucket'], 'orphan.bin', request['sourceVersion'], request['destinationVersion']],
+ input=values['xl.meta'], capture_output=True, timeout=60)
+ require(result.returncode == 0 and 0 < len(result.stdout) <= fault.LIMIT,
+ 'candidate codec refused the exact shared fixture')
+ receipt = json.loads(result.stderr)
+ require(receipt.get('sourceVersion') == request['sourceVersion'] and receipt.get('destinationVersion') == request['destinationVersion']
+ and receipt.get('sharedDataDir') == source and receipt.get('retiredDataDir') == destination
+ and receipt.get('filesystemMutationPerformed') is False and receipt.get('physicalByteEqualityClaimed') is False,
+ 'C10 codec receipt lost its original ACK identity')
+ with fault.parent(root, ['quarantine']) as quarantine, fault.parent(root, ['data', owner['bucket'], 'orphan.bin']) as obj:
+ fault.new_file(quarantine, 'C10-shared-intent.json', fault.encode(receipt))
+ fault.new_file(obj, 'C10-converted-meta', result.stdout)
+ os.rename(destination, 'C10-retired-shared-data', src_dir_fd=obj, dst_dir_fd=quarantine)
+ os.rename('xl.meta', 'C10-original-shared-meta', src_dir_fd=obj, dst_dir_fd=quarantine)
+ os.rename('C10-converted-meta', 'xl.meta', src_dir_fd=obj, dst_dir_fd=obj)
+ os.fsync(obj); os.fsync(quarantine)
+ expected = {name: raw for name, raw in values.items() if not name.startswith(destination + '/')}
+ expected['xl.meta'] = result.stdout
+ require(generation.tree(obj, fault, allow_sparse=True)[0] == expected,
+ 'C10 shared conversion changed unrelated bytes or lost source commitments')
+ fault.new_file(quarantine, 'C10-shared-applied.json', fault.encode(receipt))
+ return {**receipt, 'state': 'APPLIED', 'physicalByteEqualityVerified': True, 'retiredBytesPreserved': True}
+
+
+def multipart(root, fault):
+ """Read the entire isolated run's MPU tree without following aliases."""
+ files, total = [], 0
+ def visit(fd, prefix):
+ nonlocal total
+ require(len(prefix) <= 8, 'C10 MPU nesting exceeds its fixture budget')
+ for name in sorted(os.listdir(fd)):
+ require(re.fullmatch(r'[A-Za-z0-9_.-]{1,256}', name) and name not in {'.', '..'}, 'C10 MPU leaf is unreviewed')
+ info = os.stat(name, dir_fd=fd, follow_symlinks=False)
+ if stat.S_ISDIR(info.st_mode):
+ with fault.parent(fd, [name]) as child: visit(child, prefix + [name])
+ else:
+ raw = fault.read_file(fd, name)
+ total += len(raw)
+ require(len(files) < 512 and total <= 32 * 1024**2, 'C10 MPU census exceeds complete evidence budget')
+ files.append({'path': '/'.join(prefix + [name]), 'size': len(raw), 'sha256': fault.digest(raw)})
+ with fault.parent(root, ['data', '.rustfs.sys']) as system:
+ if 'multipart' in os.listdir(system):
+ with fault.parent(system, ['multipart']) as pending: visit(pending, [])
+ return {'readOnly': True, 'files': files, 'totalBytes': total}
diff --git a/scripts/validation_heal/outcome.py b/scripts/validation_heal/outcome.py
new file mode 100644
index 00000000..74dab961
--- /dev/null
+++ b/scripts/validation_heal/outcome.py
@@ -0,0 +1,663 @@
+#!/usr/bin/env python3
+"""J02: reconcile original mixed-task receipts with ACKs and physical faults."""
+
+import argparse
+import errno
+from datetime import datetime, timedelta, timezone
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import secrets
+import signal
+import sys
+
+try:
+ from .heal_probe import Probe, canonical, compare_inventories, require, save
+ from .heal_workload import Recorder, payload, seed, verify_seed
+ from .healthy import BASE_OPTIONS, Fleet, physical_content, validate_topology
+ from .heal_fault import bounded_fault_id
+ from .inspector import verify as verify_inspector
+ from .manager_fixture import verify as verify_manager
+ from .matrix import select
+ from .native_regressions import CANDIDATE
+ from .outcome_grace import run as original_grace_probe
+ from .shards import files, map_target, run_task, validate_fault, validate_repaired_physical, validate_repaired_status
+ from .stop import idle_fleet
+ from .versions import (metadata_target, public_version_id, validate_unrecoverable_get,
+ validate_version_receipts, verify_proof, verify_quorum_contract, version_identity)
+except ImportError:
+ from heal_probe import Probe, canonical, compare_inventories, require, save
+ from heal_workload import Recorder, payload, seed, verify_seed
+ from healthy import BASE_OPTIONS, Fleet, physical_content, validate_topology
+ from heal_fault import bounded_fault_id
+ from inspector import verify as verify_inspector
+ from manager_fixture import verify as verify_manager
+ from matrix import select
+ from native_regressions import CANDIDATE
+ from outcome_grace import run as original_grace_probe
+ from shards import files, map_target, run_task, validate_fault, validate_repaired_physical, validate_repaired_status
+ from stop import idle_fleet
+ from versions import (metadata_target, public_version_id, validate_unrecoverable_get,
+ validate_version_receipts, verify_proof, verify_quorum_contract, version_identity)
+
+
+CASE_ID = "heal.outcome-counters-and-error-truth"
+REVIEWED_CANDIDATES = frozenset({CANDIDATE, "1c7fc818dd858247c45b39542abd8291b1e6e31b",
+ "7951bb629821246868ee3761b321dd07c06db45c"})
+BRANCHES = ("mixed-healthy-repaired-failed", "dry-run", "deferred", "readonly-terminal-failure", "unknown-negative")
+SIZE = 5 * 1024**2 + 123
+# The reviewed immutable candidate makes one attempt and three bucket-object retries.
+FAILED_ATTEMPTS = 4
+SOURCE_ASSERTION_GAPS = ()
+
+
+def reconcile(status, bucket, expected, options, incarnation, *, attempt_failures, legacy_unknown=()):
+ """The manifest describes responsibility; legacy objectsHealed is not evidence."""
+ wanted = {version_identity(entry): entry for entry in expected}
+ require(wanted and len(wanted) == len(expected), "J02 manifest is empty or duplicates ACK identities")
+ require(type(attempt_failures) is int and attempt_failures >= 0, "J02 invalid attempt-failure oracle")
+ legacy_unknown = set(legacy_unknown)
+ require(legacy_unknown <= set(wanted) and legacy_unknown == {pair for pair, entry in wanted.items() if entry["state"] == "unknown"},
+ "J02 unknown responsibility lacks its independently frozen legacy manifest")
+ require(not legacy_unknown or any(entry["state"] in {"verified_healthy", "metadata_healthy"} for entry in expected),
+ "J02 legacy negative requires a separate protected control")
+ failed = sum(entry["state"] == "failed" for entry in expected)
+ require(status.get("summary") == ("stopped" if failed else "finished")
+ and status.get("settings") == options and status.get("truncated", False) is False,
+ "J02 task has a wrong terminal, options or incomplete public result")
+ outcome = status.get("outcome") or {}
+ require(outcome.get("execution", {}).get("state") == ("completed_with_errors" if failed else "completed")
+ and outcome.get("coverage") == "complete" and outcome.get("objectsTruncated") is False,
+ "J02 task hid failed execution or lacks complete canonical coverage")
+ receipts = outcome.get("objects", [])
+ require(isinstance(receipts, list) and len(receipts) == len(wanted), "J02 omitted or duplicated object receipts")
+ observed = {}
+ counters = dict(processed=len(wanted), healed=0, unchanged=0, skipped=0,
+ failed=0, unknown=0, attemptFailures=attempt_failures, overflowed=False)
+ for receipt in receipts:
+ identity = receipt.get("identity", {})
+ pair = identity.get("object"), public_version_id(identity.get("versionId"))
+ require(pair in wanted and pair not in observed and identity.get("kind") == "object" and identity.get("bucket") == bucket
+ and type(identity.get("poolIndex")) is int and identity["poolIndex"] == 0
+ and type(identity.get("setIndex")) is int and identity["setIndex"] == 0,
+ "J02 receipt has a foreign/duplicate object, version or erasure set")
+ expected_entry = wanted[pair]
+ state = expected_entry["state"]
+ require(state in {"repaired", "verified_healthy", "metadata_healthy", "failed", "dry_run_observed", "deferred"}
+ or state == "unknown" and pair in legacy_unknown,
+ "J02 unknown, skipped or absent work cannot become a successful fixture result")
+ disposition = receipt.get("disposition", {})
+ require(disposition.get("state") == state, "J02 disposition differs from the frozen physical fault plan")
+ if state == "deferred":
+ # Existence-check deferral cannot establish a version or incarnation.
+ # This is one key-level refusal, never a version repair receipt.
+ require(expected_entry.get("keyLevelRefusal") is True and pair[1] == "null"
+ and identity.get("versionId") is None and identity.get("bucketIncarnationId") is None
+ and disposition.get("details") == {"reason": "transient_existence_check", "retryNotBefore": None},
+ "J02 deferred dependency claimed a version/incarnation or changed its typed reason")
+ else:
+ require(isinstance(incarnation, str) and incarnation
+ and identity.get("bucketIncarnationId") == incarnation,
+ "J02 receipt escaped the original healthy-task bucket incarnation")
+ if state == "unknown":
+ counters["unknown"] += 1
+ require(disposition == {"state": "unknown"} and receipt.get("detail") is None,
+ "J02 legacy unknown changed its frozen proofless disposition")
+ if state == "failed":
+ require(disposition.get("details") == "retry_exhausted", "J02 below-R failure lost bounded retry exhaustion")
+ category = {"repaired": "healed", "verified_healthy": "unchanged", "metadata_healthy": "unchanged",
+ "failed": "failed", "dry_run_observed": "skipped", "deferred": "skipped", "unknown": "skipped"}[state]
+ counters[category] += 1
+ observed[pair] = state
+ actual_counters = outcome.get("counters", {})
+ require(all(type(actual_counters.get(name)) is type(value) and actual_counters[name] == value
+ for name, value in counters.items()), "J02 canonical counters hide retries or differ from complete dispositions")
+ return {"originalIncarnation": incarnation, "completeManifestReconciled": True,
+ "canonicalCounters": counters, "receipts": len(observed), "legacyObjectsHealedUsedAsRepairEvidence": False,
+ "deferredIsRepair": False, "failedIsRepair": False, "unknownIsRepair": False}
+
+
+def append_legacy_fixture(probe, fleet, directory, binding, expected, fixture):
+ """Keep an ACK-bound protected control while writing new real legacy objects."""
+ for node in fleet.nodes:
+ proof = fleet.record("one-way-legacy-write-phase", {"node": node, **fleet.remote(node, "outcome-legacy-phase")})
+ require(proof.get("runId") == binding["runId"] and proof.get("binarySha256") == binding["candidateBinarySha256"]
+ and proof.get("phase") == "legacy" and proof.get("oneWay") is True
+ and proof.get("candidateUnchanged") is True and proof.get("otherRuntimeSettingsUnchanged") is True
+ and proof.get("credentialsDisclosed") is False, "J02 phase acknowledgement differs from its original candidate")
+ fleet.start()
+ running = fleet.snapshot("actual-legacy-writing-processes")
+ require(all(scene.get("backgroundConfig", {}).get("RUSTFS_SHARD_INTEGRITY_WRITE") == "false"
+ and scene["backgroundConfig"].get("RUSTFS_HEAL_MRF_ENABLE") == "false"
+ and scene["backgroundConfig"].get("RUSTFS_SCANNER_ENABLED") == "false" for scene in running.values()),
+ "J02 legacy bytes require actual write-disabled, isolated candidate processes")
+ recorder = Recorder(probe, directory / "legacy-fixture")
+ appended = []
+ for index in range(2):
+ key = f"legacy/{index:04d}.bin"
+ versions = 3 if fixture == "DV" else 1
+ for version in range(versions):
+ body = payload(f"{binding['bucketPrefix']}/{key}/{version}", SIZE)
+ response = recorder.call("put_object", {"Bucket": binding["bucketPrefix"], "Key": key,
+ "ContentType": "application/octet-stream", "Metadata": {"heal-fixture": fixture, "nonce": f"{index}-{version}"},
+ "Tagging": "suite=heal&fixture=" + fixture}, body)
+ require(fixture != "DV" or response.get("VersionId") not in {None, "null"}, "J02 legacy DV PUT lacks an ACK version")
+ appended.append({"key": key, "versionId": response.get("VersionId", "null"), "isLatest": version == versions - 1,
+ "deleteMarker": False, "payload": {"sha256": hashlib.sha256(body).hexdigest(), "size": len(body)}})
+ if fixture == "DV" and index == 0:
+ response = recorder.call("delete_object", {"Bucket": binding["bucketPrefix"], "Key": key})
+ require(response.get("DeleteMarker") is True and response.get("VersionId"), "J02 legacy marker ACK is incomplete")
+ appended[-1]["isLatest"] = False
+ appended.append({"key": key, "versionId": response["VersionId"], "isLatest": True, "deleteMarker": True})
+ return expected + appended
+
+
+def legacy_manifest(original, bucket, expected):
+ """Require a complete physical cohort for every ACK version before admission."""
+ manifest, unknown, evidence = [], [], []
+ for entry in expected:
+ legacy = entry["key"].startswith("legacy/")
+ state = "metadata_healthy" if entry["deleteMarker"] else "unknown" if legacy else "verified_healthy"
+ manifest.append({**entry, "state": state})
+ if entry["deleteMarker"]:
+ continue
+ target = map_target(original, bucket, entry["key"], "data", 1, entry["versionId"], latest_required=False)
+ parts = target["logical"]["parts"]
+ member_parts = [version["parts"] for member in target["cohort"] for version in member["metadata"]["versions"]
+ if public_version_id(version.get("versionId")) == public_version_id(entry["versionId"])]
+ require(len(member_parts) == len(target["cohort"]) and all(parts and all(type(part.get("integrityDescriptorPresent")) is bool
+ and part["integrityDescriptorPresent"] is (not legacy) for part in parts) for parts in member_parts),
+ "J02 metadata lacks exact protected/legacy part descriptor evidence")
+ require(target["logical"]["size"] == entry["payload"]["size"], "J02 metadata size differs from the ACK")
+ evidence.append({"key": entry["key"], "versionId": entry["versionId"], "legacy": legacy,
+ "completeCohort": target["cohort"], "parts": parts})
+ if legacy:
+ unknown.append(version_identity(entry))
+ require(unknown and any(not item["legacy"] for item in evidence), "J02 legacy scene lacks an independent protected control")
+ return {"manifest": manifest, "unknownIdentities": unknown, "physicalLegacyEvidence": evidence,
+ "syntheticMetadataWritten": False, "legacyPayloadIntegrityCertified": False}
+
+
+def fault_plan(original, bucket, expected, branch):
+ require(branch in BRANCHES[:4], "J02 unknown diagnostic cannot mutate fleet files")
+ repair_entry = next(entry for entry in expected if entry["key"] == "objects/0001.bin" and not entry["deleteMarker"]
+ and (branch != "readonly-terminal-failure" or entry["isLatest"]))
+ failed_entry = next(entry for entry in expected if entry["key"] == "objects/0002.bin" and not entry["deleteMarker"])
+ repair = map_target(original, bucket, repair_entry["key"], "data", 1, repair_entry["versionId"], latest_required=False)
+ failed = map_target(original, bucket, failed_entry["key"], "data", 1, failed_entry["versionId"], latest_required=False)
+ if branch == "readonly-terminal-failure":
+ require(repair["logical"]["isLatest"] is True, "J02 readonly target must be the original current ACK version")
+ return {"repair": repair, "faults": [repair], "originalReadonlySourceProcedure": True}
+ ordered = sorted(failed["cohort"], key=lambda member: member["index"])
+ read_quorum = failed["logical"]["erasure"]["data_blocks"]
+ faults = [{**failed, **member} for member in ordered[read_quorum - 1:]]
+ if branch == "deferred":
+ faults = [metadata_target(original, target) for target in faults]
+ manifest = [{"key": failed["key"], "versionId": "null", "state": "deferred", "keyLevelRefusal": True}]
+ else:
+ faults.insert(0, repair)
+ manifest = [{**entry, "state": "dry_run_observed" if branch == "dry-run" else
+ "repaired" if version_identity(entry) == version_identity(repair) else
+ "failed" if version_identity(entry) == version_identity(failed) else
+ "metadata_healthy" if entry["deleteMarker"] else "verified_healthy"} for entry in expected]
+ require(len({(item["node"], item["disk"], item["relativePath"]) for item in faults}) == len(faults)
+ and len(ordered) - (len(faults) - (branch != "deferred")) == read_quorum - 1,
+ "J02 fault plan duplicates files or does not leave precisely R-1 authoritative members")
+ return {"repair": repair, "failed": failed, "faults": faults, "manifest": manifest,
+ "readQuorum": read_quorum, "retainedMembers": ordered[:read_quorum - 1]}
+
+
+def rescue(fleet):
+ for node, params in tuple(fleet.faults):
+ fleet.record("restore-fixture-without-relabeling-original-task", {
+ "node": node, **fleet.remote(node, "recover-fault", **params)})
+ fleet.faults.remove((node, params))
+
+
+def retained_failure_scene(original, faulted, after, plan, branch):
+ if branch in {"dry-run", "deferred"}:
+ require(physical_content(faulted) == physical_content(after), "J02 dry-run/deferred task mutated a physical file")
+ return {"physicalUnchanged": True, "productRepairClaimed": False}
+ # Restore only the intentionally unrecoverable files in the oracle view.
+ # The fleet remains faulted until after the original failed task is archived.
+ combined = json.loads(json.dumps(after))
+ originals, observed = files(original), files(after)
+ for target in plan["faults"][1:]:
+ identity = target["node"], target["disk"], target["relativePath"]
+ require(identity not in observed, "J02 below-R task invented an unrecoverable shard")
+ combined[target["node"]]["files"].append(originals[identity])
+ proof = validate_repaired_physical(original, combined, plan["repair"])
+ return {**proof, "failedMembersStillAbsent": len(plan["faults"]) - 1,
+ "oracleViewOnly": True, "fixtureRescueIsProductRepair": False}
+
+
+def readonly_failure(status, bucket, key, options):
+ """Freeze the source operation card's unbound permanent key-level failure."""
+ outcome = status.get("outcome") or {}
+ require(status.get("summary") == "stopped" and status.get("settings") == options
+ and status.get("truncated", False) is False and outcome.get("execution", {}).get("state") == "completed_with_errors"
+ and outcome.get("coverage") == "complete" and outcome.get("objectsTruncated") is False,
+ "J02 readonly task lost its complete permanent terminal failure")
+ objects = outcome.get("objects", [])
+ require(len(objects) == 1, "J02 readonly task lost its single key-level responsibility")
+ identity = objects[0].get("identity", {})
+ require(set(identity) == {"kind", "bucket", "object", "versionId", "bucketIncarnationId", "poolIndex", "setIndex"}
+ and identity.get("kind") == "object" and identity.get("bucket") == bucket and identity.get("object") == key
+ and identity.get("versionId") is None and identity.get("bucketIncarnationId") is None
+ and all(type(identity.get(name)) is int and identity[name] == 0 for name in ("poolIndex", "setIndex"))
+ and objects[0].get("disposition") == {"state": "failed", "details": "permanent"},
+ "J02 readonly failure invented a version/incarnation or changed its original failure class")
+ expected = {"processed": 1, "healed": 0, "unchanged": 0, "skipped": 0, "failed": 1,
+ "unknown": 0, "attemptFailures": 1, "overflowed": False}
+ counters = outcome.get("counters", {})
+ require(all(type(counters.get(name)) is type(value) and counters[name] == value for name, value in expected.items()),
+ "J02 readonly failure hid attempts or reported a repair")
+ return {"canonicalCounters": expected, "originalKeyLevelFailurePreserved": True, "repairClaimed": False}
+
+
+def unchanged_token(probe, fleet, admission, status, label):
+ response = fleet.record(label, probe.admin(admission["path"], [("clientToken", admission["clientToken"])], b""))
+ require(response.get("httpStatus") == 200 and response.get("response", {}).get("summary") == status["summary"]
+ and response["response"].get("settings") == status["settings"]
+ and response["response"].get("truncated", False) is False
+ and canonical(response["response"].get("outcome")) == canonical(status["outcome"]),
+ "J02 later work replaced the original readonly failure responsibility")
+
+
+def readonly_compensation(probe, fleet, binding, original, faulted, plan, expected, baseline, incarnation, active):
+ target = plan["repair"]
+ gate = fleet.record("readonly-actual-process-probe", {"node": target["node"],
+ **fleet.remote(target["node"], "drive-gate-probe")})
+ require(type(gate.get("disk")) is int and gate["disk"] == target["disk"] and type(gate.get("errno")) is int and gate["errno"] == errno.EROFS
+ and gate.get("name") == "EROFS" and gate.get("sameServiceIdentity") is True
+ and gate.get("privateMountNamespace") is True and gate.get("hostMountRemounted") is False,
+ "J02 readonly refusal was not observed in the original candidate namespace")
+ options = {**BASE_OPTIONS, "recursive": False}
+ scope = {"bucket": binding["bucketPrefix"], "key": target["key"]}
+ controls = [entry for entry in expected if entry["key"] != target["key"] and entry["isLatest"] and not entry["deleteMarker"]]
+ require(controls, "J02 readonly task lacks an independently healthy current control")
+ control = controls[0]
+ control_status = run_task(probe, fleet, {"bucket": scope["bucket"], "key": control["key"]}, 2, active,
+ option_overrides=options, status_endpoints=tuple(binding["endpoints"])[1:])
+ fleet.record("readonly-healthy-control-under-gate", control_status)
+ control_proof = validate_version_receipts(control_status, scope["bucket"], [control], 2)
+ require(control_status.get("settings") == options and control_proof["bucketIncarnationId"] == incarnation
+ and control_status["outcome"]["objects"][0]["identity"].get("kind") == "object",
+ "J02 readonly control lost the original incarnation or actual Deep verification")
+ require(physical_content(fleet.snapshot("readonly-after-gated-control-before-admission")) == physical_content(faulted),
+ "J02 healthy control changed a fault or its own original bytes")
+ admissions = []
+ status = run_task(probe, fleet, scope, 2, active, option_overrides=options,
+ status_endpoints=tuple(binding["endpoints"])[1:], admissions=admissions)
+ fleet.record("readonly-original-terminal-before-release", status)
+ negative = readonly_failure(status, scope["bucket"], scope["key"], options)
+ require(len(admissions) == 1, "J02 readonly task lacks one independent admission")
+ idle_fleet(fleet, binding)
+ fleet.stop()
+ require(physical_content(fleet.snapshot("readonly-original-failure-physical", quiescent=True)) == physical_content(faulted),
+ "J02 readonly task changed its still-unrepaired scene")
+ cleared = fleet.record("readonly-stopped-release", {"node": target["node"],
+ **fleet.remote(target["node"], "drive-gate", mode="clear")})
+ require(cleared.get("state") == "CLEAR", "J02 readonly gate was not released")
+ fleet.drive_gates.remove(target["node"])
+ fleet.start()
+ require(physical_content(fleet.snapshot("readonly-released-before-new-token")) == physical_content(faulted),
+ "J02 release repaired the object before compensating admission")
+ unchanged_token(probe, fleet, admissions[0], status, "readonly-original-token-after-release")
+ next_admissions = []
+ repaired = run_task(probe, fleet, scope, 2, active, option_overrides=options,
+ status_endpoints=tuple(binding["endpoints"])[1:], admissions=next_admissions)
+ require(len(next_admissions) == 1 and next_admissions[0]["clientToken"] != admissions[0]["clientToken"],
+ "J02 compensation reused the original failed token")
+ fleet.record("readonly-new-token-original-result", repaired)
+ validate_repaired_status(repaired, target)
+ require(repaired.get("settings") == options and repaired["outcome"]["objects"][0]["identity"].get("kind") == "object"
+ and repaired["outcome"]["objects"][0]["identity"].get("bucketIncarnationId") == incarnation,
+ "J02 compensation changed options or the original protected-control incarnation")
+ idle_fleet(fleet, binding)
+ fleet.stop()
+ after = fleet.snapshot("readonly-new-token-physical-before-get", quiescent=True)
+ physical = validate_repaired_physical(original, after, target)
+ verify_proof(fleet, target, "readonly-new-token-full-shard-bitrot-before-get")
+ rescue(fleet)
+ require(physical_content(fleet.snapshot("readonly-after-fixture-rescue", quiescent=True)) == physical_content(after),
+ "J02 fixture rescue changed the product's compensating repair")
+ fleet.start()
+ for endpoint in binding["endpoints"]:
+ inventory = fleet.record("readonly-compensated-all-peer-S3", Probe(binding, endpoint).inventory(
+ binding["bucketPrefix"], unversioned_current_tags=all(entry["versionId"] == "null" for entry in expected)))
+ verify_seed(expected, inventory)
+ compare_inventories(baseline, inventory)
+ unchanged_token(probe, fleet, admissions[0], status, "readonly-original-token-after-compensation")
+ return {"negative": negative, "physical": physical, "originalToken": admissions[0]["clientToken"],
+ "compensatingToken": next_admissions[0]["clientToken"], "sameTokenRecoveryClaimed": False,
+ "originalFailureUnchanged": True, "productRepairByNewToken": True}
+
+
+
+def opaque_metadata_outcome(status, bucket, key, options, incarnation):
+ """An unobserved Unknown must remain unobserved, never successful repair."""
+ require(isinstance(incarnation, str) and bool(incarnation)
+ and status.get("summary") in {"finished", "stopped"} and status.get("settings") == options
+ and status.get("truncated", False) is False, "J02 opaque metadata task did not terminate with its exact options")
+ outcome = status.get("outcome") or {}
+ counters = outcome.get("counters") or {}
+ names = ("processed", "healed", "unchanged", "skipped", "failed", "unknown", "attemptFailures")
+ require(all(type(counters.get(name)) is int and counters[name] >= 0 for name in names)
+ and counters.get("overflowed") is False and counters["processed"] == 1
+ and counters["processed"] == sum(counters[name] for name in ("healed", "unchanged", "skipped", "failed"))
+ and counters["unknown"] <= counters["skipped"]
+ and counters["healed"] == counters["unchanged"] == 0,
+ "J02 opaque metadata counters falsely resolve or omit original responsibility")
+ objects = outcome.get("objects")
+ require(outcome.get("coverage") == "complete" and outcome.get("objectsTruncated") is False
+ and isinstance(objects, list) and len(objects) == 1,
+ "J02 opaque metadata responsibility is incomplete")
+ identity = objects[0].get("identity") or {}
+ state = (objects[0].get("disposition") or {}).get("state")
+ require(identity.get("kind") == "object" and identity.get("bucket") == bucket and identity.get("object") == key
+ and identity.get("versionId") is None and identity.get("bucketIncarnationId") in {None, incarnation}
+ and type(identity.get("poolIndex")) is int and identity["poolIndex"] == 0
+ and type(identity.get("setIndex")) is int and identity["setIndex"] == 0
+ and state in {"unknown", "deferred", "failed"},
+ "J02 opaque metadata invented a healthy, absent or resolved identity")
+ require(counters["unknown"] == (1 if state == "unknown" else 0)
+ and counters["failed"] == (1 if state == "failed" else 0)
+ and counters["skipped"] == (0 if state == "failed" else 1)
+ and outcome.get("execution", {}).get("state") == ("completed_with_errors" if state == "failed" else "completed")
+ and status["summary"] == ("stopped" if state == "failed" else "finished"),
+ "J02 opaque metadata counters or execution disagree with the original disposition")
+ require(state != "unknown" or isinstance(status.get("detail"), str) and bool(status["detail"].strip()),
+ "J02 opaque Unknown lacks the original legacy warning")
+ return {"verdict": "PASS" if state == "unknown" else "NOT_OBSERVED", "unknownObserved": state == "unknown",
+ "originalCorruptMetadataSourceProcedure": True, "originalDisposition": state,
+ "canonicalCounters": counters, "productRepairClaimed": False, "canonicalPassClaimed": False}
+
+
+def original_opaque_probe(probe, fleet, binding, directory, expected, fixture, incarnation, active):
+ """Corrupt every original metadata copy through the existing file-fault path."""
+ bucket, key = binding["bucketPrefix"], "opaque/source.bin"
+ fleet.start()
+ body = payload(bucket + "/" + key, SIZE)
+ response = Recorder(probe, directory / "original-opaque-fixture").call("put_object", {
+ "Bucket": bucket, "Key": key, "ContentType": "application/octet-stream"}, body)
+ version = public_version_id(response.get("VersionId"))
+ require(fixture != "DV" or version != "null", "J02 opaque DV fixture lacks an exact ACK version")
+ entry = {"key": key, "versionId": version, "isLatest": True, "deleteMarker": False,
+ "payload": {"sha256": hashlib.sha256(body).hexdigest(), "size": len(body)}}
+ expected = [*expected, entry]
+ baseline = fleet.record("original-opaque-ACK-full-S3", probe.inventory(bucket, unversioned_current_tags=fixture == "DB"))
+ fleet.record("original-opaque-complete-ACK-manifest", {"expected": expected, **verify_seed(expected, baseline)})
+ options = {**BASE_OPTIONS, "recursive": False}
+ anchor = run_task(probe, fleet, {"bucket": bucket, "key": key}, 2, active, option_overrides=options,
+ status_endpoints=tuple(binding["endpoints"])[1:])
+ require(validate_version_receipts(anchor, bucket, [entry], 2)["bucketIncarnationId"] == incarnation,
+ "J02 opaque anchor changed the original incarnation")
+ idle_fleet(fleet, binding); fleet.stop()
+ original = fleet.snapshot("original-opaque-complete-physical", quiescent=True)
+ target = map_target(original, bucket, key, "data", 1, version, latest_required=False)
+ verify_proof(fleet, target, "original-opaque-full-cohort-bitrot")
+ targets = [metadata_target(original, {**target, **member}) for member in target["cohort"]]
+ require(len(targets) == len(fleet.nodes) * 4
+ and len({(row["node"], row["disk"]) for row in targets}) == len(targets),
+ "J02 opaque corruption must cover every original physical metadata copy")
+ fleet.record("original-opaque-metadata-fault-plan", targets)
+ metadata_faults, prior = {}, original
+ for index, row in enumerate(targets):
+ params = {"disk": row["disk"], "faultId": bounded_fault_id("J02", "original-opaque", index)}
+ fleet.faults.append((row["node"], params))
+ receipt = fleet.record("original-opaque-byte-zero-bitflip", {"node": row["node"], **fleet.remote(
+ row["node"], "apply-fault", **params, relativePath=row["relativePath"], expectedSha256=row["sha256"],
+ kind="bitflip", offset=0)})
+ require(receipt.get("kind") == "bitflip" and type(receipt.get("offset")) is int and receipt["offset"] == 0
+ and isinstance(receipt.get("fault"), dict), "J02 opaque injection changed its original byte-zero fault")
+ metadata_faults.setdefault(row["node"], {})[f"{row['disk']}:{row['relativePath']}"] = receipt["fault"]
+ damaged = fleet.snapshot("original-opaque-physical-injection", quiescent=True, metadata_faults=metadata_faults)
+ validate_fault(prior, damaged, row, receipt); prior = damaged
+ damaged_files = files(damaged)
+ require(all(damaged_files[(row["node"], row["disk"], row["relativePath"])].get("metadataError", {})
+ .get("exactWitnessedFault") is True
+ and damaged_files[(row["node"], row["disk"], row["relativePath"])]["metadataError"].get("validMetadataClaimed") is False
+ and "metadata" not in damaged_files[(row["node"], row["disk"], row["relativePath"])] for row in targets),
+ "J02 original opaque fixture retained a readable metadata copy")
+ fleet.start()
+ # A separate protected prefix remains observable during the opaque fault.
+ control = run_task(probe, fleet, {"bucket": bucket, "key": "objects/"}, 2, active,
+ option_overrides={**options, "recursive": True}, status_endpoints=tuple(binding["endpoints"])[1:])
+ require(validate_version_receipts(control, bucket, [item for item in expected if item["key"].startswith("objects/")], 2)
+ ["bucketIncarnationId"] == incarnation, "J02 opaque fault lost its independent healthy control")
+ require(physical_content(fleet.snapshot("original-opaque-before-admission", metadata_faults=metadata_faults)) == physical_content(damaged),
+ "J02 opaque physical fault changed before original admission")
+ admissions = []
+ status = run_task(probe, fleet, {"bucket": bucket, "key": key}, 2, active, option_overrides=options,
+ status_endpoints=tuple(binding["endpoints"])[1:], admissions=admissions)
+ fleet.record("original-opaque-terminal-before-rescue", status)
+ result = opaque_metadata_outcome(status, bucket, key, options, incarnation)
+ require(len(admissions) == 1, "J02 opaque original admission is ambiguous")
+ idle_fleet(fleet, binding); fleet.stop()
+ require(physical_content(fleet.snapshot("original-opaque-after-task-before-rescue", quiescent=True,
+ metadata_faults=metadata_faults)) == physical_content(damaged),
+ "J02 opaque original task changed metadata, payload or control bytes")
+ rescue(fleet)
+ require(physical_content(fleet.snapshot("original-opaque-restored-original-bytes", quiescent=True)) == physical_content(original),
+ "J02 opaque rescue did not restore the exact original metadata generation")
+ fleet.start()
+ for endpoint in binding["endpoints"]:
+ observed = fleet.record("original-opaque-restored-full-S3", Probe(binding, endpoint).inventory(
+ bucket, unversioned_current_tags=fixture == "DB"))
+ verify_seed(expected, observed); compare_inventories(baseline, observed)
+ unchanged_token(probe, fleet, admissions[0], status, "original-opaque-token-after-fixture-rescue")
+ idle_fleet(fleet, binding); fleet.stop()
+ require(physical_content(fleet.snapshot("original-opaque-after-restored-business-reads", quiescent=True)) == physical_content(original),
+ "J02 opaque business reads changed the restored generation")
+ result.update(originalMetadataCopiesCorrupted=len(targets), physicalFaultRetainedBeforeRescue=True,
+ originalBytesRestored=True, originalTokenUnchanged=True, fixtureRescueIsProductRepair=False)
+ return expected, baseline, result
+
+
+def run_branch(args, topology, nodes, fixture, branch, candidate):
+ directory = args.evidence / fixture / topology / branch
+ directory.mkdir(parents=True)
+ run_id = f"gh-{os.environ['GITHUB_RUN_ID']}-{os.environ.get('GITHUB_RUN_ATTEMPT', '1')}-{topology.lower()}"
+ fleet = Fleet(nodes, directory, run_id, args.tool_directory,
+ runtime_profile="outcome-legacy" if branch == "unknown-negative" else "outcome-grace" if branch == "deferred" else "manual")
+ bucket = "rv-heal-" + hashlib.sha256(run_id.encode()).hexdigest()[:16]
+ fleet.snapshot_buckets = [bucket]
+ access, secret = secrets.token_hex(16), secrets.token_hex(24)
+ os.environ.update(HEAL_ACCESS_KEY=access, HEAL_SECRET_KEY=secret)
+ binding = {"schemaVersion": 1, "runId": run_id, "jumpHostOnly": True, "exclusiveHealService": True,
+ "leaseExpiresAt": (datetime.now(timezone.utc) + timedelta(minutes=30)).isoformat(), "bucketPrefix": bucket,
+ "endpoints": {f"node{index}": f"http://rustfs-node{index}:9000" for index in range(1, len(nodes) + 1)},
+ "defaultEndpoint": "node1", "candidateCommit": candidate["source_sha"], "harnessCommit": args.testing_commit,
+ "candidatePackageSha256": candidate["package_sha256"], "candidateBinarySha256": args.binary_sha,
+ "rcBinarySha256": hashlib.sha256(args.rc_bin.read_bytes()).hexdigest(),
+ "maxInventoryObjects": 128, "maxInventoryBytes": 512 * 1024**2}
+ fleet.record("binding", binding)
+ contract = verify_quorum_contract(fleet.record("inspector-build", verify_inspector(args.tool_directory, candidate)), candidate)
+ probe, active, pending = Probe(binding), [], None
+ result = {"topology": topology, "fixture": fixture, "branch": branch, "verdict": "RUNNING", "cleanup": "PENDING"}
+ try:
+ fleet.deploy(args.binary_sha, bucket, access, secret)
+ fleet.record("topology-proof", validate_topology(fleet.snapshot("deployed-topology"), topology))
+ pending = {"operation": "seed", "bucket": bucket}
+ expected = seed(Recorder(probe, directory / "fixture"), bucket, fixture, 1 if branch == "unknown-negative" else 4 if branch == "readonly-terminal-failure" else 3, [SIZE])
+ baseline = fleet.record("ACK-bound-full-S3", probe.inventory(bucket, unversioned_current_tags=fixture == "DB"))
+ fleet.record("complete-ACK-manifest", {"expected": expected, **verify_seed(expected, baseline)})
+ pending = None
+ options = {**BASE_OPTIONS, "recursive": True}
+ anchor = run_task(probe, fleet, {"bucket": bucket, "key": ""}, 2, active,
+ option_overrides=options, status_endpoints=tuple(binding["endpoints"])[1:])
+ incarnation = validate_version_receipts(anchor, bucket, expected, 2)["bucketIncarnationId"]
+ idle_fleet(fleet, binding)
+ fleet.stop()
+ if branch == "unknown-negative":
+ pending = {"operation": "original-corrupt-metadata-source-probe", "bucket": bucket}
+ expected, baseline, result["originalCorruptMetadataProbe"] = original_opaque_probe(
+ probe, fleet, binding, directory, expected, fixture, incarnation, active)
+ pending = None
+ fleet.snapshot("protected-control-before-legacy-writing", quiescent=True)
+ pending = {"operation": "append-real-legacy-fixture", "bucket": bucket}
+ expected = append_legacy_fixture(probe, fleet, directory, binding, expected, fixture)
+ baseline = fleet.record("legacy-ACK-bound-full-S3", probe.inventory(bucket, unversioned_current_tags=fixture == "DB"))
+ fleet.record("complete-protected-and-legacy-ACK-manifest", {"expected": expected, **verify_seed(expected, baseline)})
+ pending = None
+ idle_fleet(fleet, binding)
+ fleet.stop()
+ if branch == "deferred":
+ pending = {"operation": "original-version-specific-grace-source-procedure", "bucket": bucket}
+ expected, baseline, result["originalGraceProcedure"] = original_grace_probe(
+ probe, fleet, binding, directory, expected, incarnation, active, unchanged_token)
+ source = result["originalGraceProcedure"]
+ require(source.get("originalGraceSourceProcedure") is True and source.get("sourceDiagnosticComplete") is True
+ and source.get("verdict") in {"PASS", "NOT_OBSERVED"}
+ and all(source.get(name) is False for name in ("fixtureRescueIsProductRepair", "sameTokenRecoveryClaimed",
+ "globalClockChanged", "metadataAgeRewritten", "canonicalPassClaimed")),
+ "J02 original grace procedure has incomplete source evidence")
+ pending = None
+ result.update(verdict=source["verdict"], recoveryIsProductRepair=False,
+ sourceProcedure="heal_status_batch.Batch.deferred")
+ return result
+ original = fleet.snapshot("healthy-physical-baseline", quiescent=True)
+ plan = legacy_manifest(original, bucket, expected) if branch == "unknown-negative" else fault_plan(original, bucket, expected, branch)
+ fleet.record("independent-manifest-and-frozen-fault-plan", plan)
+ for target in [plan[key] for key in ("repair", "failed") if key in plan]:
+ verify_proof(fleet, target, "original-full-cohort-bitrot")
+ prior = original
+ faulted = original
+ for index, target in enumerate(plan.get("faults", [])):
+ params = {"disk": target["disk"], "faultId": bounded_fault_id("J02", branch, index)}
+ fleet.faults.append((target["node"], params))
+ receipt = fleet.record("stopped-original-file-fault", {"node": target["node"], **fleet.remote(
+ target["node"], "apply-fault", **params, relativePath=target["relativePath"],
+ expectedSha256=target["sha256"], kind="missing")})
+ faulted = fleet.snapshot("readonly-single-missing-part" if branch == "readonly-terminal-failure" else "actual-R-minus-one-fault", quiescent=True)
+ validate_fault(prior, faulted, target, receipt)
+ prior = faulted
+ if branch == "readonly-terminal-failure":
+ target = plan["repair"]
+ fleet.drive_gates.add(target["node"])
+ fleet.record("readonly-stopped-gate-arm", {"node": target["node"], **fleet.remote(
+ target["node"], "drive-gate", disk=target["disk"], mode="read-only")})
+ fleet.start()
+ require(physical_content(faulted) == physical_content(fleet.snapshot("before-original-task")),
+ "J02 attribution window lost: background activity changed a fault before admission")
+ if branch == "readonly-terminal-failure":
+ result["readonly"] = readonly_compensation(probe, fleet, binding, original, faulted, plan,
+ expected, baseline, incarnation, active)
+ result.update(verdict="PASS", fixtureRescueIsProductRepair=False, sameTokenRecoveryClaimed=False)
+ return result
+ if branch == "mixed-healthy-repaired-failed":
+ for endpoint in binding["endpoints"]:
+ observed = fleet.record("pre-task-failed-version-GET", Probe(binding, endpoint).s3("get_object", {
+ "Bucket": bucket, "Key": plan["failed"]["key"], "VersionId": plan["failed"]["versionId"]}))
+ validate_unrecoverable_get(observed, contract)
+ options = {**options, "dryRun": branch == "dry-run", "recursive": branch != "deferred"}
+ scope = {"bucket": bucket, "key": plan["failed"]["key"] if branch == "deferred" else ""}
+ admissions = []
+ status = run_task(probe, fleet, scope, 2, active, option_overrides=options,
+ status_endpoints=tuple(binding["endpoints"])[1:], admissions=admissions)
+ require(len(admissions) == 1, "J02 original fault task lacks an exact independent admission")
+ fleet.record("original-terminal-before-any-rescue", status)
+ result["responsibility"] = reconcile(status, bucket, plan["manifest"], options, incarnation,
+ attempt_failures=FAILED_ATTEMPTS if branch == "mixed-healthy-repaired-failed" else 1 if branch == "deferred" else 0,
+ legacy_unknown=tuple(tuple(pair) for pair in plan.get("unknownIdentities", [])))
+ idle_fleet(fleet, binding)
+ fleet.stop()
+ after = fleet.snapshot("original-result-physical-scene", quiescent=True)
+ if branch == "unknown-negative":
+ require(physical_content(original) == physical_content(after), "J02 legacy negative mutated its original physical scene")
+ result["physical"] = {"physicalUnchanged": True, "productRepairClaimed": False,
+ "legacyPayloadIntegrityCertified": False}
+ else:
+ result["physical"] = retained_failure_scene(original, faulted, after, plan, branch)
+ if branch == "mixed-healthy-repaired-failed":
+ verify_proof(fleet, plan["repair"], "actual-repaired-shard-before-rescue-or-GET")
+ rescue(fleet)
+ restored = fleet.snapshot("fixture-restored-after-original-task-archival", quiescent=True)
+ if branch == "mixed-healthy-repaired-failed":
+ validate_repaired_physical(original, restored, plan["repair"])
+ else:
+ require(physical_content(original) == physical_content(restored), "J02 rescue changed the original ACK generation")
+ for target in [plan[key] for key in ("repair", "failed") if key in plan]:
+ verify_proof(fleet, target, "all-original-shards-after-fixture-rescue")
+ fleet.start()
+ for endpoint in binding["endpoints"]:
+ observed = fleet.record("all-peer-restored-full-S3", Probe(binding, endpoint).inventory(
+ bucket, unversioned_current_tags=fixture == "DB"))
+ verify_seed(expected, observed)
+ compare_inventories(baseline, observed)
+ repeated = fleet.record("original-token-after-rescue", probe.admin(admissions[0]["path"],
+ [("clientToken", admissions[0]["clientToken"])], b""))
+ require(repeated.get("httpStatus") == 200 and repeated.get("response", {}).get("summary") == status["summary"]
+ and repeated["response"].get("settings") == options
+ and repeated["response"].get("truncated", False) is False
+ and canonical(repeated["response"].get("outcome")) == canonical(status["outcome"]),
+ "J02 rescue overwrote the original failed/deferred/dry-run responsibility")
+ unobserved = any(result.get(name, {}).get("verdict") == "NOT_OBSERVED"
+ for name in ("originalCorruptMetadataProbe", "originalGraceProcedure"))
+ result.update(verdict="NOT_OBSERVED" if unobserved else "PASS", recoveryIsProductRepair=False)
+ except (Exception, KeyboardInterrupt) as exc:
+ result.update(verdict="FAIL", failure={"type": type(exc).__name__, "message": str(exc)})
+ if fleet.reserved:
+ try: fleet.snapshot("failure-scene-before-fault-rescue")
+ except Exception as capture: fleet.record("failure-scene-error", {"error": str(capture)})
+ finally:
+ for path, token in active:
+ try: fleet.record("stop-unresolved", probe.admin(path, [("clientToken", token), ("forceStop", "true")], b""))
+ except Exception as exc: fleet.record("stop-unresolved-error", {"error": str(exc)})
+ if pending is not None:
+ try: fleet.stop()
+ except Exception as exc: fleet.record("retain-stop-error", {"error": str(exc)})
+ fleet.record("retain-unknown-seed-receipt", {"state": "DIRTY", "namespacesRetained": True, "request": pending})
+ clean = False
+ else:
+ clean = fleet.cleanup()
+ result["cleanup"] = "PASS" if clean else "FAIL"
+ fleet.record("branch-result", result)
+ return result
+
+
+def main():
+ def interrupted(signum, frame): raise KeyboardInterrupt(f"J02 interrupted by signal {signum}")
+ signal.signal(signal.SIGTERM, interrupted)
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--candidate", type=Path, required=True)
+ parser.add_argument("--case-id", choices=(CASE_ID,), required=True)
+ parser.add_argument("--testing-commit", required=True)
+ parser.add_argument("--binary-sha", required=True)
+ parser.add_argument("--rc-bin", type=Path, required=True)
+ parser.add_argument("--evidence", type=Path, required=True)
+ parser.add_argument("--tool-directory", type=Path, required=True)
+ args = parser.parse_args()
+ require(select(CASE_ID)[0]["variants"] == list(BRANCHES), "J02 executor omitted a source branch")
+ candidate = json.loads(args.candidate.read_text())
+ require(candidate["source_sha"] in REVIEWED_CANDIDATES, "J02 retry/deferral contract is not reviewed for this candidate")
+ native = verify_manager(os.environ.get("VALIDATION_HEAL_MANAGER_DIRECTORY", ""), candidate, CASE_ID)
+ save(args.evidence / "independent-native-outcome-guard.json", native)
+ require(re.fullmatch(r"[0-9a-f]{64}", args.binary_sha)
+ and re.fullmatch(r"[1-9][0-9]*", os.environ.get("GITHUB_RUN_ID", "")), "J02 immutable workflow/binary identities required")
+ nodes = os.environ.get("RUSTFS_NODES", "vm000 vm001 vm002 vm003").split()
+ require(len(nodes) == len(set(nodes)) == 4 and all(re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", node) for node in nodes),
+ "J02 requires the four current distinct auto-testing Heal nodes")
+ results = []
+ try:
+ for topology, selected in (("H1", nodes[:1]), ("H2", nodes)):
+ for fixture in ("DB", "DV"):
+ for branch in BRANCHES:
+ result = run_branch(args, topology, selected, fixture, branch, candidate)
+ results.append(result)
+ if result["cleanup"] != "PASS": return 1
+ finally:
+ wanted = {(topology, fixture, branch) for topology in ("H1", "H2") for fixture in ("DB", "DV") for branch in BRANCHES}
+ complete = {(row["topology"], row["fixture"], row["branch"]) for row in results} == wanted and len(results) == len(wanted)
+ save(args.evidence / "healthy-summary.json", {"caseId": CASE_ID, "results": results,
+ "cleanup": "PASS" if results and all(row["cleanup"] == "PASS" for row in results) else "FAIL",
+ "diagnosticBranchesPassed": complete and all(row["verdict"] == row["cleanup"] == "PASS" for row in results),
+ "complete": False, "canonicalCasePassClaimed": False, "sourceAssertionGaps": list(SOURCE_ASSERTION_GAPS),
+ "canonicalAdmissionEnabled": False, "liveSourceProcedureAcceptancePending": True})
+ return 1
+
+
+if __name__ == "__main__": sys.exit(main())
diff --git a/scripts/validation_heal/outcome_grace.py b/scripts/validation_heal/outcome_grace.py
new file mode 100644
index 00000000..5dd1bf29
--- /dev/null
+++ b/scripts/validation_heal/outcome_grace.py
@@ -0,0 +1,284 @@
+"""J02: preserve exact version grace responsibility through new-token cleanup."""
+
+from datetime import datetime
+import hashlib
+import math
+import time
+import uuid
+
+try:
+ from . import orphan
+ from .authority import object_files
+ from .fleet_node import PROCESS_FIELDS
+ from .heal_probe import require
+ from .heal_workload import Recorder, payload, verify_seed
+ from .healthy import BASE_OPTIONS, physical_content
+ from .shards import files, map_target, run_task
+ from .stop import idle_fleet
+ from .versions import validate_version_receipts, verify_proof
+except ImportError:
+ import orphan
+ from authority import object_files
+ from fleet_node import PROCESS_FIELDS
+ from heal_probe import require
+ from heal_workload import Recorder, payload, verify_seed
+ from healthy import BASE_OPTIONS, physical_content
+ from shards import files, map_target, run_task
+ from stop import idle_fleet
+ from versions import validate_version_receipts, verify_proof
+
+
+GRACE_SECONDS = 120
+KEY = orphan.KEY
+SIZE = 5 * 1024**2 + 123
+
+
+def finite_epoch(value):
+ return type(value) in (int, float) and math.isfinite(value) and value >= 0
+
+
+def grace_outcome(status, bucket, old, current, options, incarnation, observed_at, *, cleanup=False):
+ require(finite_epoch(observed_at) and finite_epoch(old.get('lastModifiedEpoch'))
+ and old['versionId'] != current['versionId'] and isinstance(incarnation, str) and incarnation,
+ 'J02 grace lacks distinct ACK versions, original incarnation or a real observation clock')
+ outcome = status.get('outcome') or {}
+ require(status.get('summary') == 'finished' and status.get('settings') == options and status.get('truncated', False) is False
+ and outcome.get('execution', {}).get('state') == 'completed' and outcome.get('coverage') == 'complete'
+ and outcome.get('objectsTruncated') is False, 'J02 grace task lacks complete original terminal truth')
+ wanted = dict(processed=2, healed=1 if cleanup else 0, unchanged=1, skipped=0 if cleanup else 1,
+ failed=0, unknown=0, attemptFailures=0 if cleanup else 1, overflowed=False)
+ counters = outcome.get('counters') or {}
+ require(set(counters) == set(wanted) and all(type(counters[name]) is type(value) and counters[name] == value
+ for name, value in wanted.items()), 'J02 grace counters do not match both original versions')
+ objects = outcome.get('objects')
+ require(isinstance(objects, list) and len(objects) == 2, 'J02 grace omitted a version responsibility')
+ seen, due = set(), None
+ for item in objects:
+ identity = item.get('identity') or {}
+ version = identity.get('versionId')
+ require(version in {old['versionId'], current['versionId']} and version not in seen
+ and identity == dict(kind='object', bucket=bucket, object=KEY, versionId=version,
+ bucketIncarnationId=incarnation, poolIndex=0, setIndex=0)
+ and type(identity.get('poolIndex')) is int and type(identity.get('setIndex')) is int,
+ 'J02 grace rebound, duplicated or omitted an original version/incarnation')
+ seen.add(version)
+ disposition = item.get('disposition') or {}
+ if version == current['versionId']:
+ require(disposition == {'state': 'verified_healthy'}, 'J02 grace current ACK lost its Deep integrity')
+ elif cleanup:
+ require(disposition == {'state': 'repaired'}, 'J02 post-grace cleanup is not a distinct product repair')
+ else:
+ details = disposition.get('details') or {}
+ timestamp = details.get('retryNotBefore')
+ require(disposition.get('state') == 'deferred' and set(details) == {'reason', 'retryNotBefore'}
+ and details['reason'] == 'dangling_delete_grace' and isinstance(timestamp, dict)
+ and set(timestamp) == {'secs_since_epoch', 'nanos_since_epoch'}
+ and type(timestamp.get('secs_since_epoch')) is int and 0 <= timestamp['secs_since_epoch'] < 2**64
+ and type(timestamp.get('nanos_since_epoch')) is int and 0 <= timestamp['nanos_since_epoch'] < 10**9,
+ 'J02 grace lacks the exact typed future retry timestamp')
+ due = timestamp['secs_since_epoch'] + timestamp['nanos_since_epoch'] / 10**9
+ require(due > observed_at and abs(due - old['lastModifiedEpoch'] - GRACE_SECONDS) <= 5,
+ 'J02 grace retry timestamp is past or unrelated to the original server version age')
+ require(not cleanup or observed_at >= old['lastModifiedEpoch'] + GRACE_SECONDS,
+ 'J02 cleanup was attributed before natural grace expiry')
+ return {'bothExactVersionResponsibilities': True, 'canonicalCounters': counters, 'retryNotBeforeEpoch': due,
+ 'newTaskProductCleanup': cleanup, 'sameTokenRecoveryClaimed': False}
+
+
+def grace_fixture(initial, authority, injected, bucket, old, current, target, directory):
+ initial_refs = orphan.references(initial, bucket, KEY)
+ authority_refs = orphan.references(authority, bucket, KEY)
+ require(set(initial_refs) == set(authority_refs) and target in initial_refs
+ and all(set(value) == {old['versionId'], current['versionId']} for value in initial_refs.values())
+ and all(set(value) == {current['versionId']} for value in authority_refs.values()),
+ 'J02 grace DELETE did not establish complete exact-version authority on every original member')
+ require(all(values[current['versionId']] == authority_refs[member][current['versionId']]
+ and values[current['versionId']].get('deleted') is False
+ and values[current['versionId']].get('isLatest') is True
+ and values[old['versionId']].get('dataDir') == directory
+ and values[current['versionId']].get('dataDir') != directory
+ for member, values in initial_refs.items()),
+ 'J02 grace DELETE changed the acknowledged current logical version or its original dataDir')
+ left, right = files(initial), files(authority)
+ old_prefix = bucket + '/' + KEY + '/' + directory + '/'
+ metadata = bucket + '/' + KEY + '/xl.meta'
+ require({n: v['formats'] for n, v in initial.items()} == {n: v['formats'] for n, v in authority.items()}
+ and not any(identity[2].startswith(old_prefix) for identity in right),
+ 'J02 grace DELETE changed formats or retained an unacknowledged old payload')
+ require({key: value for key, value in left.items() if key[2] != metadata and not key[2].startswith(old_prefix)}
+ == {key: value for key, value in right.items() if key[2] != metadata},
+ 'J02 grace DELETE changed current/control payload bytes or sidecars')
+ if injected is not None:
+ observed = files(injected)
+ prefix = bucket + '/' + KEY + '/'
+ require({n: v['formats'] for n, v in injected.items()} == {n: v['formats'] for n, v in authority.items()}
+ and {key: value for key, value in observed.items() if not (key[:2] == target and key[2].startswith(prefix))}
+ == {key: value for key, value in right.items() if not (key[:2] == target and key[2].startswith(prefix))}
+ and {key: value for key, value in observed.items() if key[:2] == target and key[2].startswith(prefix)}
+ == {key: value for key, value in left.items() if key[:2] == target and key[2].startswith(prefix)},
+ 'J02 grace replay is not exactly one original member object tree')
+ return {'allOriginalMembers': len(initial_refs), 'exactHistoricalDeleteAcknowledged': True,
+ 'originalCurrentAndControlBytesPreserved': True, 'singleOriginalMemberReplay': injected is not None}
+
+
+def clocks(fleet, binding, label, previous=None):
+ samples = {}
+ for node in fleet.nodes:
+ start = time.time()
+ sample = fleet.record(label, {'node': node, **fleet.remote(node, 'process-clock')})
+ finish = time.time()
+ identity = sample.get('identity') or {}
+ require(sample.get('readOnly') is True and sample.get('clockChanged') is False
+ and finite_epoch(sample.get('nodeEpoch')) and start - 3 <= sample['nodeEpoch'] <= finish + 3
+ and type(identity.get('pid')) is int and identity['pid'] > 1
+ and identity.get('binarySha256') == binding['candidateBinarySha256']
+ and isinstance(identity.get('settings'), dict)
+ and identity.get('settings', {}).get('RUSTFS_HEAL_DANGLING_DELETE_GRACE_SECS') == str(GRACE_SECONDS),
+ 'J02 grace lacks the actual candidate process policy or synchronized node clock')
+ if previous is not None:
+ require(all(identity.get(key) == previous[node]['identity'].get(key) for key in PROCESS_FIELDS),
+ 'J02 original grace process changed between original and compensating tasks')
+ samples[node] = sample
+ return samples
+
+
+def archive_member(fleet, snapshot, bucket, target, label):
+ node, disk = target
+ params = dict(disk=disk, bucket=bucket, key=KEY)
+ expected = object_files(snapshot, bucket, KEY)[target]
+ start = time.time()
+ observed = fleet.record('J02-grace-stopped-member', {'node': node, **fleet.remote(node, 'orphan-observe', **params)})
+ require(observed.get('present') is True and observed.get('files') == expected
+ and finite_epoch(observed.get('nodeEpoch')) and start - 3 <= observed['nodeEpoch'] <= time.time() + 3,
+ 'J02 stopped grace object/clock differs from the original physical witness')
+ return fleet.record('J02-grace-archive-' + label, {'node': node, **fleet.remote(node, 'orphan-capture',
+ **params, generation=label, expectedFiles=expected, expectedPresent=True)})
+
+
+def run(probe, fleet, binding, directory, expected, incarnation, active, preserve_token):
+ """Use real SDK version history and natural wall-clock grace, never aged bytes."""
+ bucket = binding['bucketPrefix']
+ recorder = Recorder(probe, directory / 'original-grace-fixture')
+ fleet.start()
+ recorder.call('put_bucket_versioning', {'Bucket': bucket, 'VersioningConfiguration': {'Status': 'Enabled'}})
+ versions = []
+ for label in ('old', 'current'):
+ body = payload(bucket + '/J02-grace/' + label, SIZE)
+ response = recorder.call('put_object', {'Bucket': bucket, 'Key': KEY, 'ContentType': 'application/octet-stream'}, body)
+ version = response.get('VersionId')
+ require(isinstance(version, str) and str(uuid.UUID(version)) == version and uuid.UUID(version).int != 0
+ and version not in {value['versionId'] for value in versions}, 'J02 grace PUT lacks a distinct real ACK version')
+ head = fleet.record('J02-grace-ACK-server-age', probe.s3('head_object', {'Bucket': bucket, 'Key': KEY, 'VersionId': version}))
+ info = head.get('response') or {}
+ require(head.get('acknowledged') is True and head.get('resultUnknown') is False and info.get('VersionId') == version
+ and info.get('ContentLength') == len(body) and isinstance(response.get('ETag'), str) and response['ETag']
+ and info.get('ETag') == response['ETag'],
+ 'J02 grace server version age does not bind the actual PUT ACK')
+ modified = info.get('LastModified')
+ epoch = modified.timestamp() if isinstance(modified, datetime) else datetime.fromisoformat(modified).timestamp()
+ require(finite_epoch(epoch) and abs(epoch - time.time()) <= 5, 'J02 grace server version clock is not current')
+ if versions: versions[0]['isLatest'] = False
+ versions.append(dict(key=KEY, versionId=version, isLatest=True, deleteMarker=False,
+ payload={'sha256': hashlib.sha256(body).hexdigest(), 'size': len(body)}, lastModifiedEpoch=epoch))
+ old, current = versions
+ # Server age belongs to the grace witness, not the public S3 manifest.
+ public_versions = [{key: value for key, value in version.items() if key != 'lastModifiedEpoch'} for version in versions]
+ seeded = [*expected, *public_versions]
+ fleet.record('J02-grace-initial-ACK-S3', verify_seed(seeded, probe.inventory(bucket)))
+ healthy = run_task(probe, fleet, {'bucket': bucket, 'key': KEY}, 2, active,
+ option_overrides={'recursive': True}, status_endpoints=tuple(binding['endpoints'])[1:])
+ require(validate_version_receipts(healthy, bucket, versions, 2)['bucketIncarnationId'] == incarnation,
+ 'J02 grace initial versions escaped the independently anchored bucket incarnation')
+ idle_fleet(fleet, binding); fleet.stop()
+ initial = fleet.snapshot('J02-grace-original-two-version-physical', quiescent=True)
+ old_target = map_target(initial, bucket, KEY, 'data', 1, old['versionId'], latest_required=False)
+ current_target = map_target(initial, bucket, KEY, 'data', 1, current['versionId'])
+ verify_proof(fleet, old_target, 'J02-grace-original-old-full-bitrot')
+ verify_proof(fleet, current_target, 'J02-grace-original-current-full-bitrot')
+ target = (fleet.nodes[-1], 0)
+ archive_member(fleet, initial, bucket, target, 'old')
+ fleet.start()
+ deleted = recorder.call('delete_object', {'Bucket': bucket, 'Key': KEY, 'VersionId': old['versionId']})
+ require(deleted.get('VersionId') == old['versionId'] and deleted.get('DeleteMarker') in {None, False}
+ and deleted.get('ResponseMetadata', {}).get('HTTPStatusCode') == 204, 'J02 grace DELETE lacks its exact historical ACK')
+ expected = [*expected, public_versions[1]]
+ baseline = probe.inventory(bucket)
+ fleet.record('J02-grace-post-delete-ACK-S3', {'inventory': baseline, **verify_seed(expected, baseline)})
+ idle_fleet(fleet, binding); fleet.stop()
+ authority = fleet.snapshot('J02-grace-post-delete-all-member-authority', quiescent=True)
+ grace_fixture(initial, authority, None, bucket, old, current, target, old_target['dataDir'])
+ archive_member(fleet, authority, bucket, target, 'authority')
+ params = dict(disk=target[1], bucket=bucket, key=KEY, generation='authority')
+ fleet.orphan_faults.append((target[0], params))
+ fleet.record('J02-grace-arm-post-delete-authority', {'node': target[0], **fleet.remote(target[0], 'orphan-arm', **params)})
+ fleet.record('J02-grace-single-member-old-replay', {'node': target[0], **fleet.remote(target[0], 'orphan-install',
+ disk=target[1], bucket=bucket, key=KEY, dataDir=old_target['dataDir'], mode='object')})
+ faulted = fleet.snapshot('J02-grace-original-replayed-fault', quiescent=True)
+ physical = grace_fixture(initial, authority, faulted, bucket, old, current, target, old_target['dataDir'])
+ fleet.start()
+ before_clocks = clocks(fleet, binding, 'J02-grace-actual-process-clock-before-original')
+ options = {**BASE_OPTIONS, 'recursive': True, 'remove': True}
+ scope = dict(bucket=bucket, key=KEY)
+ result = {'originalGraceSourceProcedure': True, 'physicalFixture': physical, 'globalClockChanged': False,
+ 'metadataAgeRewritten': False, 'sameTokenRecoveryClaimed': False, 'canonicalPassClaimed': False}
+ trigger_at = time.time()
+ expires = old['lastModifiedEpoch'] + GRACE_SECONDS
+ fleet.record('J02-grace-original-admission-window', {'observedAt': trigger_at, 'graceSeconds': GRACE_SECONDS,
+ 'oldServerModifiedEpoch': old['lastModifiedEpoch'], 'releaseAt': expires + 5})
+ require(physical_content(fleet.snapshot('J02-grace-immediately-before-original-task')) == physical_content(faulted),
+ 'J02 grace fault changed before original admission')
+ if expires - trigger_at < 10:
+ result.update(verdict='NOT_OBSERVED', reason='Original pre-grace admission window elapsed')
+ else:
+ admissions = []
+ status = run_task(probe, fleet, scope, 2, active, option_overrides=options,
+ status_endpoints=tuple(binding['endpoints'])[1:], admissions=admissions)
+ observed_at = time.time()
+ fleet.record('J02-grace-original-terminal-before-compensation', {'observedAt': observed_at, 'status': status})
+ require(len(admissions) == 1, 'J02 grace original task has ambiguous admission')
+ idle_fleet(fleet, binding)
+ protected = fleet.snapshot('J02-grace-original-task-physical-before-any-GET')
+ if observed_at >= expires:
+ unchanged = physical_content(protected) == physical_content(faulted)
+ cleaned = physical_content(protected) == physical_content(authority)
+ require(unchanged or cleaned, 'J02 late grace observation lost current/control bytes')
+ grace_outcome(status, bucket, old, current, options, incarnation,
+ trigger_at if unchanged else observed_at, cleanup=not unchanged)
+ result.update(verdict='NOT_OBSERVED', reason='Original terminal observation missed the pre-grace window')
+ else:
+ result['original'] = grace_outcome(status, bucket, old, current, options, incarnation, observed_at)
+ require(physical_content(protected) == physical_content(faulted), 'J02 pre-grace task modified protected old/current bytes')
+ deadline = time.monotonic() + GRACE_SECONDS + 10
+ while time.time() < expires + 5:
+ require(time.monotonic() < deadline, 'J02 natural grace clock did not converge within its bounded wait')
+ time.sleep(min(1, max(0, expires + 5 - time.time())))
+ clocks(fleet, binding, 'J02-grace-actual-process-clock-after-expiry', before_clocks)
+ preserve_token(probe, fleet, admissions[0], status, 'J02-grace-original-token-after-natural-expiry')
+ require(physical_content(fleet.snapshot('J02-grace-original-fault-before-new-token')) == physical_content(protected),
+ 'J02 completed original token resumed or altered bytes after grace')
+ next_admissions = []
+ recovered = run_task(probe, fleet, scope, 2, active, option_overrides=options,
+ status_endpoints=tuple(binding['endpoints'])[1:], admissions=next_admissions)
+ require(len(next_admissions) == 1 and next_admissions[0]['clientToken'] != admissions[0]['clientToken'],
+ 'J02 grace compensation reused the original completed token')
+ fleet.record('J02-grace-new-token-product-terminal', recovered)
+ result['compensation'] = grace_outcome(recovered, bucket, old, current, options, incarnation, time.time(), cleanup=True)
+ idle_fleet(fleet, binding)
+ clocks(fleet, binding, 'J02-grace-actual-process-clock-after-compensation', before_clocks)
+ product = fleet.snapshot('J02-grace-product-cleaned-before-any-GET-or-rescue')
+ require(physical_content(product) == physical_content(authority), 'J02 new token did not restore exact post-DELETE physical authority')
+ orphan.verify_public(fleet, binding, bucket, expected, [old['versionId']], 'J02-grace-product-S3-before-rescue')
+ preserve_token(probe, fleet, admissions[0], status, 'J02-grace-original-token-after-new-task-and-GET')
+ clocks(fleet, binding, 'J02-grace-actual-process-clock-after-public-and-token-checks', before_clocks)
+ result.update(verdict='PASS', originalToken=admissions[0]['clientToken'], compensatingToken=next_admissions[0]['clientToken'],
+ originalTokenUnchanged=True, currentPayloadMatchesIndependentlyVerifiedOriginal=True,
+ productCleanupBeforeFixtureRescue=True, oldVersionAbsentAfterProductCleanup=True)
+ idle_fleet(fleet, binding); fleet.stop()
+ final = fleet.snapshot('J02-grace-complete-result-physical-before-fixture-rescue', quiescent=True)
+ if result['verdict'] == 'PASS':
+ require(physical_content(final) == physical_content(authority), 'J02 business reads changed the product cleanup scene')
+ orphan.recover_all(fleet)
+ require(physical_content(fleet.snapshot('J02-grace-post-delete-authority-after-fixture-finalization', quiescent=True)) == physical_content(authority),
+ 'J02 grace finalization failed to preserve exact acknowledged DELETE authority')
+ result.update(fixtureRescueIsProductRepair=False, sourceDiagnosticComplete=True)
+ return expected, baseline, result
diff --git a/scripts/validation_heal/outcome_recovery.py b/scripts/validation_heal/outcome_recovery.py
new file mode 100644
index 00000000..96313651
--- /dev/null
+++ b/scripts/validation_heal/outcome_recovery.py
@@ -0,0 +1,355 @@
+"""Restore and retain exactly archived J02 failures before negative task admission."""
+
+import copy
+import hashlib
+import json
+import re
+import uuid
+
+try:
+ from .healthy import physical_content, validate_topology
+ from .heal_probe import require
+ from .native_regressions import CANDIDATE
+ from .orphan import KEY, references
+ from .shards import files, map_target, validate_fault
+ from .heal_fault import bounded_fault_id
+ from .heal_workload import verify_seed
+ from .versions import public_version_id, metadata_target
+except ImportError:
+ from healthy import physical_content, validate_topology
+ from heal_probe import require
+ from native_regressions import CANDIDATE
+ from orphan import KEY, references
+ from shards import files, map_target, validate_fault
+ from heal_fault import bounded_fault_id
+ from heal_workload import verify_seed
+ from versions import public_version_id, metadata_target
+
+
+def digest(value):
+ return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(',', ':')).encode()).hexdigest()
+
+
+def acknowledged_grace_plan(artifact, failed_run, candidate):
+ case = 'heal.outcome-counters-and-error-truth'
+ paths = list(artifact.glob('*/run/*/cases/*.json'))
+ require(len(paths) == 1 and paths[0].name == case + '.json', 'J02 recovery requires one unmixed original result')
+ result = json.loads(paths[0].read_text()); provenance = result.get('provenance') or {}
+ require(result.get('case_id') == case and result.get('verdict') == result.get('cleanup_status') == 'FAIL'
+ and candidate.get('source_sha') == provenance.get('source_commit') == CANDIDATE
+ and candidate.get('package_sha256') == provenance.get('candidate_sha256'), 'J02 recovery candidate/result differs')
+ evidence = paths[0].parents[1] / 'evidence' / case
+ summary = json.loads((evidence / 'healthy-summary.json').read_text()); rows = summary.get('results') or []
+ require(summary.get('caseId') == case and summary.get('cleanup') == 'FAIL' and summary.get('complete') is False
+ and summary.get('canonicalCasePassClaimed') is False and len(rows) == 3
+ and [(row.get('topology'), row.get('fixture'), row.get('branch')) for row in rows] == [
+ ('H1', 'DB', branch) for branch in ('mixed-healthy-repaired-failed', 'dry-run', 'deferred')]
+ and all(row.get('cleanup') == 'PASS' for row in rows[:-1]) and rows[-1].get('cleanup') == 'FAIL'
+ and rows[-1].get('verdict') == 'FAIL' and rows[-1].get('failure') == {
+ 'type': 'FixtureStateMismatch', 'message': 'baseline content/version/marker differs from deterministic fixture ACK'},
+ 'J02 recovery supports only its pre-replay public-manifest refusal')
+ folder = evidence / 'DB/H1/deferred'
+ def one(label):
+ found = [p for p in folder.glob('*-' + label + '.json')
+ if re.fullmatch(r'[0-9]{5}-' + re.escape(label) + r'\.json', p.name)]
+ require(len(found) == 1, 'J02 recovery lacks exact ' + label)
+ return json.loads(found[0].read_text())
+ binding, failure, reservation = one('binding'), one('failure-scene-before-fault-rescue'), one('reserved')
+ run_id, bucket = binding.get('runId', ''), binding.get('bucketPrefix', '')
+ require(re.fullmatch(rf'gh-{failed_run}-[1-9][0-9]*-h1', run_id) is not None
+ and re.fullmatch(r'rv-heal-[0-9a-f]{16}', bucket) is not None
+ and binding.get('harnessCommit') == provenance.get('testing_commit')
+ and binding.get('candidateCommit') == candidate['source_sha']
+ and binding.get('candidatePackageSha256') == candidate['package_sha256']
+ and binding.get('candidateBinarySha256') == provenance.get('binary_sha256')
+ and set(failure) == {'vm000'} and reservation.get('node') == 'vm000' and reservation.get('reserved') is True,
+ 'J02 archived run/helper/node binding differs')
+ owner = reservation.get('owner') or {}
+ require(owner.get('runId') == run_id and owner.get('bucket') == bucket and owner.get('runtimeProfile') == 'outcome-grace'
+ and owner.get('binarySha256') == provenance['binary_sha256'] and len(owner.get('mounts', [])) == 4,
+ 'J02 original reservation differs')
+ require(one('retain-unknown-seed-receipt') == dict(state='DIRTY', namespacesRetained=True,
+ request=dict(operation='original-version-specific-grace-source-procedure', bucket=bucket)), 'J02 pending boundary differs')
+ require(not any('grace-' in path.name and not path.name.endswith('-J02-grace-ACK-server-age.json')
+ for path in folder.glob('*.json')),
+ 'J02 grace advanced beyond original version PUTs')
+ wanted = list(one('complete-ACK-manifest')['expected'])
+ for name, operations in (('fixture', ['create_bucket'] + ['put_object'] * 3),
+ ('original-grace-fixture', ['put_bucket_versioning', 'put_object', 'put_object'])):
+ requests = sorted((folder / name).glob('*-request.json')); acks = sorted((folder / name).glob('*-ack.json'))
+ require(len(requests) == len(acks) == len(operations), 'J02 source ledger is incomplete or contains a later mutation')
+ for sequence, (request_path, ack_path, operation) in enumerate(zip(requests, acks, operations, strict=True), 1):
+ request, ack = json.loads(request_path.read_text()), json.loads(ack_path.read_text())
+ observed = ack.get('observation') or {}; params = request.get('params') or {}
+ require(type(request.get('sequence')) is int and request['sequence'] == ack.get('sequence') == sequence
+ and type(ack.get('sequence')) is int and request.get('runId') == ack.get('runId') == run_id
+ and ack.get('requestFile') == request_path.name
+ and ack.get('requestSha256') == hashlib.sha256(request_path.read_bytes()).hexdigest()
+ and request.get('operation') == observed.get('operation') == operation
+ and params.get('Bucket') == bucket and params == observed.get('params')
+ and ack.get('operationCheck') == 'PASS' and observed.get('acknowledged') is True
+ and observed.get('clientCallInvoked') is True and observed.get('resultUnknown') is False
+ and observed.get('response', {}).get('ResponseMetadata', {}).get('HTTPStatusCode') == 200,
+ 'J02 source mutation has an unknown or rebound ACK')
+ if operation == 'put_object':
+ require(request.get('payload') == observed.get('payload') and re.fullmatch(r'[0-9a-f]{64}', request['payload']['sha256']),
+ 'J02 payload ACK differs from its durable request')
+ if name == 'original-grace-fixture':
+ version = observed.get('response', {}).get('VersionId')
+ require(params.get('Key') == KEY and isinstance(version, str) and str(uuid.UUID(version)) == version
+ and uuid.UUID(version).int != 0, 'J02 original grace PUT lacks a real version')
+ wanted.append(dict(key=KEY, versionId=version, payload=request['payload']))
+ else:
+ matches = [entry for entry in wanted if entry['key'] == params.get('Key')]
+ require(len(matches) == 1 and matches[0]['versionId'] == 'null'
+ and matches[0]['payload'] == request['payload'], 'J02 control manifest differs from original ACK')
+ require(len(wanted) == 5 and len({(entry['key'], entry['versionId']) for entry in wanted}) == 5,
+ 'J02 source lacks five distinct acknowledged objects/versions')
+ validate_topology(failure, 'H1')
+ files(failure)
+ require({entry['metadata']['key'] for entry in failure['vm000']['files'] if 'metadata' in entry}
+ == {entry['key'] for entry in wanted}
+ and all(any(entry['path'].startswith(bucket + '/' + version['key'] + '/') for version in wanted)
+ for entry in failure['vm000']['files']), 'J02 physical scene has another key/bucket')
+ for key in {entry['key'] for entry in wanted}:
+ expected = {entry['versionId'] for entry in wanted if entry['key'] == key}
+ require(all({public_version_id(version) for version in values} == expected
+ for values in references(failure, bucket, key).values()), 'J02 physical version authority differs from ACKs')
+ for entry in wanted:
+ target = map_target(failure, bucket, entry['key'], 'data', 1, entry['versionId'], latest_required=False)
+ require(target['logical']['size'] == entry['payload']['size'] and len(target['cohort']) == 4,
+ 'J02 ACK lacks its complete original physical cohort')
+ return [dict(kind='acknowledged-j02-before-replay', runId=run_id, nodes=['vm000'], owners={'vm000': owner},
+ before=failure, binarySha256=provenance['binary_sha256'], buckets=[bucket],
+ originalCaseSha256=hashlib.sha256(paths[0].read_bytes()).hexdigest())]
+
+
+def isolate_acknowledged_grace(fleet, plan, states):
+ require(len(states) == 1 and states[0].get('node') == 'vm000', 'J02 recovery fleet differs')
+ state = states[0]; retained = state.get('retained') or {}
+ if not retained.get('state') and not any(retained.get('roots', [])):
+ require(state['state']['exists'] and len(state['roots']) == 4 and all(root['exists'] for root in state['roots']),
+ 'J02 acknowledged fixture lost a root; retain remaining scene')
+ fleet.record('J02-acknowledged-recovery-stop', fleet.remote('vm000', 'stop'))
+ current = fleet.record('J02-acknowledged-current-physical', fleet.snapshot('J02-stopped-preserved-scene', quiescent=True))
+ require(physical_content(current) == physical_content(plan['before'])
+ and all(value.get('pid') == 0 and type(value.get('pid')) is int and value.get('quiescent') is True
+ and value.get('binarySha256') == plan['binarySha256'] for value in current.values()),
+ 'J02 acknowledged fixture drifted; no isolation or restoration performed')
+ params = dict(expectedOwner=plan['owners']['vm000'],
+ expectedOriginalSceneSha256=digest(physical_content(plan['before'])['vm000']))
+ audit = fleet.record('J02-acknowledged-archive-preflight', fleet.remote('vm000', 'retained-scene-audit', **params))
+ require(audit.get('inspectionOnly') is True and audit.get('mutated') is False
+ and audit.get('baseline', {}).get('originalSceneSha256') == params['expectedOriginalSceneSha256'], 'J02 archive preflight differs')
+ isolated = fleet.record('J02-acknowledged-archive-isolation', fleet.remote('vm000', 'retained-scene-isolate',
+ **params, expectedBaseline=audit['baseline']))
+ checked = fleet.record('J02-acknowledged-archive-post-audit', fleet.remote('vm000', 'retained-scene-audit', **params))
+ require(all(value.get('activeNamespaceIsolated') is True and value.get('canonicalPassClaimed') is False
+ and value.get('baseline') == audit['baseline'] for value in (isolated, checked))
+ and checked.get('inspectionOnly') is True and checked.get('mutated') is False, 'J02 retained scene isolation drifted')
+ return dict(runId=plan['runId'], cleanup='ISOLATED', activeNamespaceIsolated=True, backupRestorationUsed=False,
+ originalSceneRestored=False, originalBusinessFixtureRestored=False, canonicalPassClaimed=False,
+ originalVerdictRewritten=False, originalCaseSha256=plan['originalCaseSha256'])
+
+
+def interrupted_opaque_plan(artifact, failed_run, candidate):
+ case = 'heal.outcome-counters-and-error-truth'
+ paths = list(artifact.glob('*/run/*/cases/*.json'))
+ require(len(paths) == 1 and paths[0].name == case + '.json', 'opaque recovery requires one unmixed J02 result')
+ raw = paths[0].read_bytes(); result = json.loads(raw); provenance = result.get('provenance') or {}
+ require(result.get('case_id') == case and result.get('verdict') == result.get('cleanup_status') == 'FAIL'
+ and candidate.get('source_sha') == provenance.get('source_commit') == CANDIDATE
+ and candidate.get('package_sha256') == provenance.get('candidate_sha256'), 'opaque recovery candidate differs')
+ evidence = paths[0].parents[1] / 'evidence' / case
+ summary = json.loads((evidence / 'healthy-summary.json').read_text()); rows = summary.get('results') or []
+ require(summary.get('caseId') == case and summary.get('cleanup') == 'FAIL' and summary.get('complete') is False
+ and summary.get('canonicalCasePassClaimed') is False and len(rows) == 5
+ and [(r.get('topology'), r.get('fixture'), r.get('branch')) for r in rows] == [
+ ('H1', 'DB', branch) for branch in ('mixed-healthy-repaired-failed', 'dry-run', 'deferred',
+ 'readonly-terminal-failure', 'unknown-negative')]
+ and all(r.get('cleanup') == 'PASS' for r in rows[:-1]) and rows[-1].get('cleanup') == 'FAIL'
+ and rows[-1].get('verdict') == 'FAIL' and rows[-1].get('failure', {}).get('type') == 'ValueError'
+ and rows[-1]['failure']['message'].startswith('Heal node vm000 snapshot failed:')
+ and rows[-1]['failure']['message'].endswith('ValueError: metadata decoder failed outside an exact witnessed file fault\n'),
+ 'opaque recovery only supports its second-file witness refusal before task admission')
+ folder = evidence / 'DB/H1/unknown-negative'
+ records = sorted(folder.glob('*.json'))
+ require({p.name for p in folder.iterdir() if p.is_dir()} == {'fixture', 'original-opaque-fixture'},
+ 'opaque recovery contains another task or mutation directory')
+ labels = ['binding', 'inspector-build', 'preflight', 'integrity-fleet-attestation', 'reserved', 'inspector-upload',
+ 'started', 'deployed-topology', 'topology-proof', 'ACK-bound-full-S3', 'complete-ACK-manifest', 'trigger',
+ 'control', 'control-repeat', 'control-repeat', 'task-exit-background', 'quiescent', 'started',
+ 'original-opaque-ACK-full-S3', 'original-opaque-complete-ACK-manifest', 'trigger', 'control',
+ 'control-repeat', 'control-repeat', 'task-exit-background', 'quiescent', 'original-opaque-complete-physical',
+ *(['original-opaque-full-cohort-bitrot'] * 4), 'original-opaque-metadata-fault-plan',
+ 'original-opaque-byte-zero-bitflip', 'original-opaque-physical-injection', 'original-opaque-byte-zero-bitflip',
+ 'failure-scene-error', 'quiescent', 'retain-unknown-seed-receipt', 'branch-result']
+ require([p.name for p in records] == [f'{index:05d}-{label}.json' for index, label in enumerate(labels, 1)]
+ and set(folder.iterdir()) == set(records) | {folder / name for name in ('fixture', 'original-opaque-fixture')},
+ 'opaque recovery has a later task, mutation or unknown boundary')
+ def one(label):
+ found = [p for p in records if p.name[6:-5] == label]
+ require(len(found) == 1, 'opaque recovery lacks exact ' + label)
+ return json.loads(found[0].read_text())
+ binding, original, reservation = one('binding'), one('original-opaque-complete-physical'), one('reserved')
+ run_id, bucket = binding.get('runId', ''), binding.get('bucketPrefix', '')
+ require(re.fullmatch(rf'gh-{failed_run}-[1-9][0-9]*-h1', run_id) is not None
+ and re.fullmatch(r'rv-heal-[0-9a-f]{16}', bucket) is not None and set(original) == {'vm000'}
+ and binding.get('candidateCommit') == candidate['source_sha']
+ and binding.get('candidatePackageSha256') == candidate['package_sha256']
+ and binding.get('candidateBinarySha256') == provenance.get('binary_sha256')
+ and binding.get('harnessCommit') == provenance.get('testing_commit'), 'opaque archived ownership differs')
+ owner = reservation.get('owner') or {}
+ require(reservation.get('node') == 'vm000' and reservation.get('reserved') is True
+ and owner.get('runId') == run_id and owner.get('bucket') == bucket
+ and owner.get('runtimeProfile') == 'outcome-legacy' and len(owner.get('mounts', [])) == 4
+ and owner.get('binarySha256') == binding['candidateBinarySha256']
+ and original['vm000'].get('quiescent') is True and type(original['vm000'].get('pid')) is int
+ and original['vm000']['pid'] == 0 and original['vm000'].get('binarySha256') == binding['candidateBinarySha256'],
+ 'opaque original reservation or stopped candidate differs')
+ require(one('retain-unknown-seed-receipt') == dict(state='DIRTY', namespacesRetained=True,
+ request=dict(operation='original-corrupt-metadata-source-probe', bucket=bucket)), 'opaque pending source boundary differs')
+ wanted = one('original-opaque-complete-ACK-manifest')['expected']
+ require(len(wanted) == 2 and {e['key'] for e in wanted} == {'objects/0000.bin', 'opaque/source.bin'}
+ and all(e['versionId'] == 'null' for e in wanted), 'opaque source ACK manifest differs')
+ verify_seed(wanted, one('original-opaque-ACK-full-S3'))
+ validate_opaque_ledger(folder, run_id, bucket, wanted)
+ validate_topology(original, 'H1')
+ target = map_target(original, bucket, 'opaque/source.bin', 'data', 1, 'null', latest_required=False)
+ targets = [metadata_target(original, {**target, **member}) for member in target['cohort']]
+ require(one('original-opaque-metadata-fault-plan') == targets and len(targets) == 4, 'opaque original target plan differs')
+ receipts = [json.loads(p.read_text()) for p in records if p.name[6:-5] == 'original-opaque-byte-zero-bitflip']
+ first = one('original-opaque-physical-injection'); validate_fault(original, first, targets[0], receipts[0])
+ first_error = files(first)[('vm000', targets[0]['disk'], targets[0]['relativePath'])].get('metadataError') or {}
+ require(first_error.get('exactWitnessedFault') is True and first_error.get('validMetadataClaimed') is False,
+ 'opaque first metadata corruption lacks its exact decoder witness')
+ failure, faults, witnesses = copy.deepcopy(original), [], {'vm000': {}}
+ for index, (receipt, selected) in enumerate(zip(receipts, targets[:2], strict=True)):
+ fault_id = bounded_fault_id('J02', 'original-opaque', index)
+ require(receipt.get('node') == selected['node'] == 'vm000' and receipt.get('runId') == run_id
+ and receipt.get('faultId') == fault_id and receipt.get('kind') == 'bitflip'
+ and type(receipt.get('offset')) is int and receipt['offset'] == 0
+ and receipt.get('relativePath') == selected['relativePath'] and receipt.get('backup') == fault_id + '.original'
+ and receipt.get('before') == {'sha256': selected['sha256'], 'size': selected['size']}
+ and receipt.get('requiresQuiescentService') is True and receipt.get('backend') == 'logical-file-fault'
+ and receipt.get('fault', {}).get('size') == selected['size']
+ and re.fullmatch(r'[0-9a-f]{64}', receipt.get('fault', {}).get('sha256', '')) is not None,
+ 'opaque injected fault differs from the original planned file')
+ changed = copy.deepcopy(failure)
+ entry = next(e for e in changed['vm000']['files'] if e['disk'] == selected['disk'] and e['path'] == selected['relativePath'])
+ entry.pop('metadata'); entry.update(receipt['fault'], metadataError=first_error)
+ validate_fault(failure, changed, selected, receipt); failure = changed
+ faults.append(dict(node='vm000', disk=selected['disk'], faultId=fault_id,
+ receipt={k:v for k,v in receipt.items() if k != 'node'}))
+ witnesses['vm000'][f"{selected['disk']}:{selected['relativePath']}"] = receipt['fault']
+ return [dict(kind='interrupted-j02-opaque', runId=run_id, nodes=['vm000'], owners={'vm000': owner}, before=original,
+ failure=failure, faults=faults, metadataFaults=witnesses, buckets=[bucket], binarySha256=binding['candidateBinarySha256'],
+ originalCaseSha256=hashlib.sha256(raw).hexdigest())]
+
+
+def validate_opaque_ledger(folder, run_id, bucket, wanted):
+ for name, operations in (('fixture', ('create_bucket', 'put_object')), ('original-opaque-fixture', ('put_object',))):
+ requests = sorted((folder / name).glob('*-request.json')); acks = sorted((folder / name).glob('*-ack.json'))
+ require(len(requests) == len(acks) == len(operations)
+ and set((folder / name).iterdir()) == set(requests + acks), 'opaque original SDK ledger is incomplete')
+ for sequence, (request_path, ack_path, operation) in enumerate(zip(requests, acks, operations, strict=True), 1):
+ request, ack = json.loads(request_path.read_text()), json.loads(ack_path.read_text()); observed = ack.get('observation') or {}
+ require(type(request.get('sequence')) is int and type(ack.get('sequence')) is int
+ and request['sequence'] == ack['sequence'] == sequence and request.get('runId') == ack.get('runId') == run_id
+ and ack.get('requestFile') == request_path.name
+ and ack.get('requestSha256') == hashlib.sha256(request_path.read_bytes()).hexdigest()
+ and observed.get('operation') == request.get('operation') == operation
+ and observed.get('params') == request.get('params') and request['params'].get('Bucket') == bucket
+ and ack.get('operationCheck') == 'PASS' and observed.get('acknowledged') is True
+ and observed.get('clientCallInvoked') is True and observed.get('resultUnknown') is False
+ and observed.get('response', {}).get('ResponseMetadata', {}).get('HTTPStatusCode') == 200,
+ 'opaque original mutation is unknown or rebound')
+ if operation == 'put_object':
+ key = 'opaque/source.bin' if name == 'original-opaque-fixture' else 'objects/0000.bin'
+ expected = next(e for e in wanted if e['key'] == key)
+ require(request['params'].get('Key') == key and request.get('payload') == observed.get('payload') == expected['payload']
+ and public_version_id(observed['response'].get('VersionId')) == expected['versionId'], 'opaque original PUT differs from ACK')
+
+
+def acknowledged_legacy_plan(artifact, failed_run, candidate):
+ case = 'heal.outcome-counters-and-error-truth'
+ paths = list(artifact.glob('*/run/*/cases/*.json'))
+ require(len(paths) == 1 and paths[0].name == case + '.json', 'legacy recovery requires one unmixed J02 result')
+ raw = paths[0].read_bytes(); result = json.loads(raw); provenance = result.get('provenance') or {}
+ require(result.get('case_id') == case and result.get('verdict') == result.get('cleanup_status') == 'FAIL'
+ and candidate.get('source_sha') == provenance.get('source_commit') == '1c7fc818dd858247c45b39542abd8291b1e6e31b'
+ and candidate.get('package_sha256') == provenance.get('candidate_sha256'), 'legacy recovery candidate differs')
+ evidence = paths[0].parents[1] / 'evidence' / case
+ summary = json.loads((evidence / 'healthy-summary.json').read_text()); rows = summary.get('results') or []
+ require(summary.get('caseId') == case and summary.get('cleanup') == 'FAIL' and summary.get('complete') is False
+ and summary.get('canonicalCasePassClaimed') is False and len(rows) == 5
+ and [(r.get('topology'), r.get('fixture'), r.get('branch')) for r in rows] == [
+ ('H1', 'DB', branch) for branch in ('mixed-healthy-repaired-failed', 'dry-run', 'deferred',
+ 'readonly-terminal-failure', 'unknown-negative')]
+ and all(r.get('cleanup') == 'PASS' for r in rows[:-1]) and rows[-1].get('cleanup') == 'FAIL'
+ and rows[-1].get('verdict') == 'FAIL' and rows[-1].get('failure', {}).get('type') == 'ValueError'
+ and rows[-1]['failure']['message'].startswith('Heal node vm000 start failed:')
+ and rows[-1]['failure']['message'].endswith('ImportError: attempted relative import with no known parent package\n'),
+ 'legacy recovery requires the exact pre-SDK start refusal')
+ folder = evidence / 'DB/H1/unknown-negative'; records = sorted(folder.glob('*.json'))
+ require(len(records) == 71 and all(p.name.startswith(f'{i:05d}-') for i, p in enumerate(records, 1))
+ and [p.name[6:-5] for p in records[-7:]] == ['original-opaque-after-restored-business-reads',
+ 'protected-control-before-legacy-writing', 'one-way-legacy-write-phase', 'failure-scene-error',
+ 'quiescent', 'retain-unknown-seed-receipt', 'branch-result']
+ and set(folder.iterdir()) == set(records) | {folder / name for name in ('fixture', 'original-opaque-fixture')},
+ 'legacy recovery has another task or SDK mutation boundary')
+ def one(label):
+ found = [p for p in records if p.name[6:-5] == label]
+ require(len(found) == 1, 'legacy recovery lacks exact ' + label)
+ return json.loads(found[0].read_text())
+ binding, original, reserved = one('binding'), one('original-opaque-complete-physical'), one('reserved')
+ run_id, bucket = binding.get('runId', ''), binding.get('bucketPrefix', '')
+ owner = reserved.get('owner') or {}
+ require(re.fullmatch(rf'gh-{failed_run}-[1-9][0-9]*-h1', run_id) and set(original) == {'vm000'}
+ and re.fullmatch(r'rv-heal-[0-9a-f]{16}', bucket)
+ and binding.get('candidateCommit') == candidate['source_sha']
+ and binding.get('candidatePackageSha256') == candidate['package_sha256']
+ and binding.get('candidateBinarySha256') == provenance.get('binary_sha256')
+ and binding.get('harnessCommit') == provenance.get('testing_commit')
+ and reserved.get('node') == 'vm000' and reserved.get('reserved') is True
+ and owner.get('runId') == run_id and owner.get('bucket') == bucket and owner.get('runtimeProfile') == 'outcome-legacy'
+ and owner.get('binarySha256') == binding['candidateBinarySha256'] and len(owner.get('mounts', [])) == 4,
+ 'legacy recovery reservation differs')
+ require(one('retain-unknown-seed-receipt') == dict(state='DIRTY', namespacesRetained=True,
+ request=dict(operation='append-real-legacy-fixture', bucket=bucket)), 'legacy pending boundary differs')
+ require(one('one-way-legacy-write-phase') == dict(node='vm000', runId=run_id, binarySha256=owner['binarySha256'],
+ phase='legacy', oneWay=True, candidateUnchanged=True, credentialsDisclosed=False, otherRuntimeSettingsUnchanged=True),
+ 'legacy original phase acknowledgement differs')
+ wanted = one('original-opaque-complete-ACK-manifest')['expected']
+ require(len(wanted) == 2 and {e['key'] for e in wanted} == {'objects/0000.bin', 'opaque/source.bin'}
+ and all(e['versionId'] == 'null' for e in wanted), 'legacy pre-PUT manifest differs')
+ validate_opaque_ledger(folder, run_id, bucket, wanted)
+ verify_seed(wanted, one('original-opaque-ACK-full-S3')); verify_seed(wanted, one('original-opaque-restored-full-S3'))
+ validate_topology(original, 'H1')
+ for entry in wanted:
+ target = map_target(original, bucket, entry['key'], 'data', 1, 'null', latest_required=False)
+ require(len(target['cohort']) == 4 and target['logical']['size'] == entry['payload']['size'], 'legacy original cohort differs')
+ target = map_target(original, bucket, 'opaque/source.bin', 'data', 1, 'null', latest_required=False)
+ targets = [metadata_target(original, {**target, **member}) for member in target['cohort']]
+ require(one('original-opaque-metadata-fault-plan') == targets, 'legacy original metadata target plan differs')
+ injected = [json.loads(p.read_text()) for p in records if p.name[6:-5] == 'original-opaque-byte-zero-bitflip']
+ rescued = [json.loads(p.read_text()) for p in records if p.name[6:-5] == 'restore-fixture-without-relabeling-original-task']
+ require(len(injected) == len(rescued) == 4, 'legacy original rescue receipt census differs')
+ for i, (receipt, proof, target) in enumerate(zip(injected, rescued, targets, strict=True)):
+ fault_id = bounded_fault_id('J02', 'original-opaque', i)
+ require(receipt.get('faultId') == proof.get('faultId') == fault_id and receipt.get('node') == proof.get('node') == 'vm000'
+ and receipt.get('runId') == proof.get('runId') == run_id and receipt.get('relativePath') == target['relativePath']
+ and receipt.get('before') == {k:target[k] for k in ('sha256', 'size')}
+ and receipt.get('kind') == 'bitflip' and type(receipt.get('offset')) is int and receipt['offset'] == 0
+ and proof.get('restoredFromBackup') is True and proof.get('physicalRepairPassAllowed') is False,
+ 'legacy original rescue is not bound to its source fault')
+ for label in ('original-opaque-restored-original-bytes', 'original-opaque-after-restored-business-reads',
+ 'protected-control-before-legacy-writing'):
+ value = one(label)
+ require(physical_content(value) == physical_content(original)
+ and value['vm000'].get('quiescent') is True and type(value['vm000'].get('pid')) is int
+ and value['vm000']['pid'] == 0 and value['vm000'].get('binarySha256') == owner['binarySha256'],
+ 'legacy original business authority advanced before phase')
+ return [dict(kind='acknowledged-j02-before-legacy-put', runId=run_id, nodes=['vm000'], owners={'vm000':owner},
+ before=original, binarySha256=owner['binarySha256'], buckets=[bucket],
+ originalCaseSha256=hashlib.sha256(raw).hexdigest())]
diff --git a/scripts/validation_heal/overlap.py b/scripts/validation_heal/overlap.py
new file mode 100644
index 00000000..98590d79
--- /dev/null
+++ b/scripts/validation_heal/overlap.py
@@ -0,0 +1,366 @@
+#!/usr/bin/env python3
+"""A05: frozen merge policy and force replacement in real active windows."""
+
+from __future__ import annotations
+
+import argparse
+import hashlib
+import json
+import sys
+import time
+from urllib.parse import quote
+import xml.etree.ElementTree as ET
+
+try:
+ from .heal_probe import canonical, payload, require
+ from .healthy import BASE_OPTIONS, physical_content
+ from .cursor import SIZE, TASK_SECONDS, run_topology as dx_topology, main as dx_main, wanted_pairs
+ from .stop import start, status, idle_fleet, validate_retired_stop
+ from .versions import public_version_id
+except ImportError:
+ from heal_probe import canonical, payload, require
+ from healthy import BASE_OPTIONS, physical_content
+ from cursor import SIZE, TASK_SECONDS, run_topology as dx_topology, main as dx_main, wanted_pairs
+ from stop import start, status, idle_fleet, validate_retired_stop
+ from versions import public_version_id
+
+
+CASE_ID = "heal.admin-overlap-and-force-start"
+BRANCHES = ("same-scope", "parent-child", "disjoint", "force-start")
+SCOPE_OBJECTS = 257
+OBJECTS = 2 * SCOPE_OBJECTS
+PREFIXES = ("objects/a/child/", "objects/b/")
+PRIMARY = "objects/a/"
+MISSING = tuple(range(0, SCOPE_OBJECTS, 64))
+POLICY = {"candidateCommit": "072b57b37559fbe626aeda8abf61617fdcbdd926",
+ "runtime": {"RUSTFS_HEAL_OVERLAP_POLICY": "merge", "RUSTFS_HEAL_PAGE_PARALLEL_ENABLE": "false"},
+ "same-scope": "HTTP 200 and the original admitted token",
+ "parent-child": {"httpStatus": 409, "code": "OperationAborted",
+ "message": "heal request not admitted: admission=dropped, reason=overlapping_paths"},
+ "disjoint": "HTTP 200 and an independent token; original remains running",
+ "force-start": "HTTP 200 and independent token; old outcome aborted/cancelled with partial coverage",
+ "references": ["crates/config/src/constants/heal.rs", "crates/heal/src/heal/manager.rs:admin_overlap_rejection",
+ "rustfs/src/admin/handlers/heal.rs:reject_heal_admission"],
+ "itemRetention": 1024, "diagnosticObjects": 128,
+ "retentionGapClaimed": False, "nativeCommitRaceClaimed": False}
+
+
+def seed_scopes(recorder, bucket):
+ recorder.call("create_bucket", {"Bucket": bucket})
+ expected = []
+ for prefix in PREFIXES:
+ for index in range(SCOPE_OBJECTS):
+ key = f"{prefix}{index:04d}.bin"
+ body = payload(f"{bucket}/{key}/0", SIZE)
+ ack = recorder.call("put_object", {"Bucket": bucket, "Key": key,
+ "ContentType": "application/octet-stream", "Metadata": {"heal-fixture": "DX", "scope": prefix},
+ "Tagging": "suite=heal&fixture=DX"}, body)
+ require(ack.get("VersionId", "null") == "null", "DX fixture unexpectedly enabled versioning")
+ expected.append({"key": key, "versionId": "null", "deleteMarker": False, "isLatest": True,
+ "payload": {"size": SIZE, "sha256": hashlib.sha256(body).hexdigest()}})
+ wanted_pairs(expected, count=OBJECTS)
+ return expected
+
+
+def scope_expected(expected, prefix):
+ selected = [entry for entry in expected if entry["key"].startswith(prefix)]
+ require(len(selected) == SCOPE_OBJECTS, "scope escaped its frozen 257-object fixture")
+ return selected
+
+
+def counters(current, *, cancelled=False):
+ value = (current.get("outcome") or {}).get("counters", {})
+ for name in ("processed", "healed", "unchanged", "skipped", "failed", "unknown", "attemptFailures"):
+ require(type(value.get(name)) is int and value[name] >= 0, "scope omitted typed canonical counters")
+ cancellation_count = sum(receipt.get("disposition", {}).get("state") == "cancelled"
+ for receipt in (current.get("outcome") or {}).get("objects", []))
+ require(all(value[name] == 0 for name in ("failed", "unknown", "attemptFailures"))
+ and value["skipped"] == (cancellation_count if cancelled else 0)
+ and value.get("overflowed") is False
+ and value["processed"] == value["healed"] + value["unchanged"] + value["skipped"],
+ "scope counters hide failed, unknown or unresolved responsibility")
+ return value
+
+
+def active_window(current):
+ value = counters(current)
+ return (current.get("summary") == "running" and current.get("outcome", {}).get("execution", {}).get("state") == "running"
+ and 0 < value["processed"] < SCOPE_OBJECTS)
+
+
+def wait_window(probe, fleet, path, token):
+ deadline = time.monotonic() + TASK_SECONDS
+ while True:
+ current = status(probe, fleet, path, token, "overlap-running-window")
+ if active_window(current):
+ return current
+ require(current.get("summary") == "running", "NOT_OBSERVED: scope finished before a nonzero active window")
+ require(time.monotonic() < deadline, "NOT_OBSERVED: active scope exceeded the frozen window budget")
+ time.sleep(0.05)
+
+
+def monotonic(before, after, *, cancelled=False):
+ left, right = counters(before), counters(after, cancelled=cancelled)
+ require(all(right[name] >= left[name] for name in ("processed", "healed", "unchanged")),
+ "same-token scope counters moved backwards")
+
+
+def overlap_rejection(observed):
+ require(observed.get("httpStatus") == 409 and not observed.get("transportError"),
+ "parent-child overlap violated the frozen HTTP contract")
+ body = observed.get("response")
+ fields = {element.tag.rsplit("}", 1)[-1]: element.text for element in ET.fromstring(body).iter()} if isinstance(body, str) else body
+ require(isinstance(fields, dict) and (fields.get("Code") or fields.get("code")) == "OperationAborted"
+ and (fields.get("Message") or fields.get("message")) == POLICY["parent-child"]["message"],
+ "parent-child overlap lost its exact structured reason")
+ return POLICY["parent-child"]
+
+
+def scope_proof(current, bucket, expected, damaged, *, cancelled=False, healed=None, replacement=False):
+ wanted = wanted_pairs(expected, count=SCOPE_OBJECTS)
+ require(damaged and set(damaged) <= set(wanted), "scope repair plan escaped its exact ACK identities")
+ value = counters(current, cancelled=cancelled)
+ outcome = current.get("outcome") or {}
+ if cancelled:
+ require(current.get("summary") == "stopped" and outcome.get("execution") == {"state": "aborted", "reason": "cancelled"}
+ and outcome.get("coverage") == "partial" and 0 < value["processed"] < SCOPE_OBJECTS
+ and value["healed"] <= len(damaged), "forceStart falsely completed or lost the old partial responsibility")
+ else:
+ require(current.get("summary") == "finished" and outcome.get("execution") == {"state": "completed"}
+ and outcome.get("coverage") == "complete" and value["processed"] == SCOPE_OBJECTS
+ and type(healed) is int and value["healed"] == healed,
+ "scope failed complete coverage or its exact planned repair count")
+ receipts, incarnation, seen, dispositions = outcome.get("objects"), None, set(), {}
+ require(isinstance(receipts, list) and 0 < len(receipts) <= 128
+ and type(outcome.get("objectsTruncated")) is bool
+ and (cancelled or outcome["objectsTruncated"] is True), "scope omitted bounded diagnostic truth")
+ for receipt in receipts:
+ identity = receipt.get("identity", {})
+ pair = identity.get("object"), public_version_id(identity.get("versionId"))
+ require(pair in wanted and pair not in seen and identity.get("bucket") == bucket
+ and type(identity.get("poolIndex")) is int and identity["poolIndex"] == 0
+ and type(identity.get("setIndex")) is int and identity["setIndex"] == 0
+ and isinstance(identity.get("bucketIncarnationId"), str) and identity["bucketIncarnationId"],
+ "scope receipt crossed a bucket/key/version/set or omitted incarnation")
+ incarnation = incarnation or identity["bucketIncarnationId"]
+ require(incarnation == identity["bucketIncarnationId"], "scope mixed bucket incarnations")
+ state = receipt.get("disposition", {}).get("state")
+ require(state in ({"repaired", "verified_healthy", "cancelled"} if cancelled else {"repaired", "verified_healthy"})
+ and (state != "repaired" or pair in damaged)
+ and (state != "verified_healthy" or pair not in damaged or replacement),
+ "scope diagnostic concealed an unresolved or false repair")
+ dispositions[pair[0]] = state
+ seen.add(pair)
+ return {"canonicalCounters": value, "bucketIncarnationId": incarnation, "diagnosticDispositions": dispositions,
+ "diagnosticObjectsAreCompleteManifest": False, "coverage": outcome["coverage"]}
+
+
+def collect_scope_items(state, bucket, wanted, ledger, *, since=0):
+ """Retain original same-token items before completed-report byte eviction."""
+ next_seq, min_seq = state.get("nextSeq"), state.get("minSeq")
+ require(type(next_seq) is int and type(min_seq) is int and 1 <= min_seq <= next_seq <= SCOPE_OBJECTS + 1,
+ "scope item sequence bounds are missing or invalid")
+ require(next_seq > max(ledger, default=0), "same-token scope item sequence moved backwards")
+ require(min_seq == 1 or state.get("truncated") is True, "scope silently lost an early item sequence")
+ items = state.get("items", [])
+ first = max(since + 1, min_seq)
+ available = max(0, next_seq - first)
+ require(isinstance(items, list) and len(items) <= available
+ and (len(items) == available or state.get("truncated") is True), "scope silently omitted item sequences")
+ for index, item in enumerate(items, first):
+ pair = item.get("object"), public_version_id(item.get("versionId"))
+ require(item.get("bucket") == bucket and pair in wanted
+ and item.get("objectSize") == wanted[pair]["payload"]["size"], "scope item is outside its exact ACK manifest")
+ require(index not in ledger or canonical(ledger[index]) == canonical(item), "same-token scope item changed its original sequence")
+ require(not any(seq != index and (entry.get("object"), public_version_id(entry.get("versionId"))) == pair
+ for seq, entry in ledger.items()), "scope item duplicated a real object/version")
+ ledger[index] = item
+
+
+def completed_scope(probe, fleet, path, token, bucket, expected, damaged, *, healed, replacement=False,
+ observations=(), sample_other=None):
+ deadline = time.monotonic() + TASK_SECONDS
+ wanted = wanted_pairs(expected, count=SCOPE_OBJECTS)
+ ledger = {}
+ for observation in observations:
+ collect_scope_items(observation, bucket, wanted, ledger)
+ while True:
+ current = status(probe, fleet, path, token, "overlap-terminal-wait")
+ collect_scope_items(current, bucket, wanted, ledger)
+ if sample_other is not None:
+ sample_other()
+ if current.get("summary") != "running":
+ break
+ counters(current)
+ require(time.monotonic() < deadline, "scope exceeded its frozen completion budget")
+ time.sleep(0.05)
+ proof = scope_proof(current, bucket, expected, damaged, healed=healed, replacement=replacement)
+ # The completed-report byte budget can evict a prefix even below the
+ # 1024-item count cap. A complete proof needs the original running items.
+ cursor = 0
+ while cursor + 1 in ledger: cursor += 1
+ first_page = True
+ while first_page or cursor < SCOPE_OBJECTS:
+ first_page = False
+ observed = fleet.record("overlap-complete-items", probe.admin(path,
+ [("clientToken", token), ("sinceSeq", str(cursor))], b""))
+ require(observed.get("httpStatus") == 200 and isinstance(observed.get("response"), dict), "scope cursor failed")
+ page = observed["response"]
+ require(page.get("summary") == "finished" and page.get("nextSeq") == SCOPE_OBJECTS + 1
+ and canonical(page.get("outcome")) == canonical(current["outcome"]), "terminal scope changed, lost items or crossed tokens")
+ previous = cursor
+ collect_scope_items(page, bucket, wanted, ledger, since=cursor)
+ while cursor + 1 in ledger: cursor += 1
+ require(cursor == SCOPE_OBJECTS or cursor > previous,
+ "scope lost an uncollected early item; a response byte-limit gap cannot certify completeness")
+ require(time.monotonic() < deadline, "scope item suffix exceeded completion budget")
+ seen = {(item.get("object"), public_version_id(item.get("versionId"))) for item in ledger.values()}
+ require(set(ledger) == set(range(1, SCOPE_OBJECTS + 1)), "scope sequence ledger is incomplete")
+ require(seen == set(wanted), "terminal scope missed a real object/version")
+ evidence = {"path": path, "clientToken": token, "bucket": bucket,
+ "items": [{"seq": seq, "item": ledger[seq]} for seq in sorted(ledger)]}
+ fleet.record("overlap-complete-item-ledger", evidence)
+ return current, {**proof, "completeItemIdentities": len(seen), "retentionGapClaimed": False,
+ "completeItemLedgerSha256": hashlib.sha256(canonical(evidence).encode()).hexdigest()}
+
+
+def force_accounting(old, new, damaged):
+ require(old["bucketIncarnationId"] == new["bucketIncarnationId"]
+ and old["canonicalCounters"]["healed"] + new["canonicalCounters"]["healed"] == len(damaged),
+ "forceStart leaked or duplicated repair responsibility between its task identities")
+ for key in set(old["diagnosticDispositions"]) & set(new["diagnosticDispositions"]):
+ require(not (old["diagnosticDispositions"][key] == new["diagnosticDispositions"][key] == "repaired"),
+ "old and new tasks both committed a repair receipt for the same missing shard")
+ return {"independentTokens": True, "sameBucketIncarnation": True, "exactTotalRepairs": len(damaged)}
+
+
+def observe_branch(branch, probe, fleet, binding, bucket, expected, targets, active, *, original):
+ fleet.record("frozen-overlap-policy", POLICY)
+ path = "/rustfs/admin/v3/heal/" + quote(bucket, safe="") + "/" + PRIMARY
+ primary = scope_expected(expected, PRIMARY)
+ damaged = {(entry["key"], entry["versionId"]) for entry in targets if entry["key"].startswith(PRIMARY)}
+ old = start(probe, fleet, path, active)
+ before = wait_window(probe, fleet, path, old)
+ proof = {"oldTaskId": old, "runningBeforeRequest": counters(before), "policy": "merge"}
+ if branch in {"same-scope", "parent-child"}:
+ request_path = path if branch == "same-scope" else path + "child/"
+ observed = fleet.record("overlap-request", probe.admin(request_path, [], json.dumps({**BASE_OPTIONS, "recursive": True}).encode()))
+ if branch == "same-scope":
+ require(observed.get("httpStatus") == 200 and observed.get("response", {}).get("clientToken") == old,
+ "same-scope start did not reuse the active token under merge policy")
+ else:
+ proof["typedRejection"] = overlap_rejection(observed)
+ after = status(probe, fleet, path, old, "overlap-window-after-request")
+ require(active_window(after), "NOT_OBSERVED: overlap request was not bracketed by the same running task")
+ monotonic(before, after)
+ proof["runningAfterRequest"] = counters(after)
+ terminal, proof["completeResponsibility"] = completed_scope(probe, fleet, path, old, bucket, primary, damaged,
+ healed=len(damaged), observations=(before, after))
+ active.remove((path, old))
+ elif branch == "disjoint":
+ other_path = "/rustfs/admin/v3/heal/" + quote(bucket, safe="") + "/" + PREFIXES[1]
+ new = start(probe, fleet, other_path, active)
+ require(new != old, "disjoint scope reused another scope's token")
+ after = status(probe, fleet, path, old, "original-after-disjoint-start")
+ require(active_window(after), "NOT_OBSERVED: original task finished before the disjoint admission was bracketed")
+ monotonic(before, after)
+ other_observations = [wait_window(probe, fleet, other_path, new)]
+ other = scope_expected(expected, PREFIXES[1])
+ other_damaged = {(entry["key"], entry["versionId"]) for entry in targets if entry["key"].startswith(PREFIXES[1])}
+ def sample_disjoint():
+ other_observations.append(status(probe, fleet, other_path, new, "overlap-disjoint-parallel-status"))
+ terminal, proof["completeResponsibility"] = completed_scope(probe, fleet, path, old, bucket, primary, damaged,
+ healed=len(damaged), observations=(before, after), sample_other=sample_disjoint)
+ other_terminal, proof["disjointResponsibility"] = completed_scope(probe, fleet, other_path, new, bucket, other, other_damaged,
+ healed=len(other_damaged), observations=other_observations)
+ require(proof["completeResponsibility"]["bucketIncarnationId"] == proof["disjointResponsibility"]["bucketIncarnationId"],
+ "disjoint tasks crossed bucket incarnation")
+ active.remove((path, old)); active.remove((other_path, new))
+ proof.update(newTaskId=new, runningAfterRequest=counters(after))
+ else:
+ require(branch == "force-start", "unregistered overlap branch")
+ new = start(probe, fleet, path, active, force=True)
+ require(new != old, "forceStart reused the original token")
+ first_old = status(probe, fleet, path, old, "force-old-initial-cancellation")
+ require(first_old.get("summary") != "finished", "NOT_OBSERVED: original task completed before forceStart could cancel it")
+ initial = scope_proof(first_old, bucket, primary, damaged, cancelled=True)
+ monotonic(before, first_old, cancelled=True)
+ new_before = wait_window(probe, fleet, path, new)
+ retired = fleet.record("force-old-stop", probe.admin(path, [("clientToken", old), ("forceStop", "true")], b""))
+ proof["oldStopIsolation"] = validate_retired_stop(retired, old)
+ new_after = status(probe, fleet, path, new, "force-new-after-old-stop")
+ require(new_after.get("summary") != "stopped", "old token stopped the replacement task")
+ require(active_window(new_after), "NOT_OBSERVED: old stop was not bracketed by the same running replacement")
+ monotonic(new_before, new_after)
+ # The old worker can refine its cancelled report while retiring. Wait
+ # for both actual workers to drain before freezing final responsibility.
+ deadline = time.monotonic() + TASK_SECONDS
+ new_observations = [new_before, new_after]
+ while True:
+ draining = status(probe, fleet, path, new, "force-new-drain")
+ new_observations.append(draining)
+ if draining.get("summary") != "running":
+ break
+ counters(draining)
+ require(time.monotonic() < deadline, "replacement task exceeded completion budget")
+ time.sleep(0.05)
+ idle_fleet(fleet, binding)
+ old_final = status(probe, fleet, path, old, "force-old-settled")
+ settled = scope_proof(old_final, bucket, primary, damaged, cancelled=True)
+ require(settled["bucketIncarnationId"] == initial["bucketIncarnationId"]
+ and all(settled["canonicalCounters"][name] >= initial["canonicalCounters"][name]
+ for name in ("processed", "healed", "unchanged")), "cancelled refinement changed identity or moved backwards")
+ terminal, new_proof = completed_scope(probe, fleet, path, new, bucket, primary, damaged,
+ healed=len(damaged) - settled["canonicalCounters"]["healed"], replacement=True, observations=new_observations)
+ proof.update(newTaskId=new, initialCancellation=initial, settledCancellation=settled,
+ newRunningBeforeOldStop=counters(new_before), newRunningAfterOldStop=counters(new_after),
+ completeResponsibility=new_proof, responsibilityAccounting=force_accounting(settled, new_proof, damaged))
+ active.remove((path, old)); active.remove((path, new))
+ proof["allPeerDrain"] = idle_fleet(fleet, binding)
+ quiet = []
+ for index in range(3):
+ quiet.append(fleet.snapshot("overlap-live-quiet"))
+ stable = status(probe, fleet, path, old, "overlap-old-quiet-status")
+ require(canonical(stable.get("outcome")) == canonical((old_final if branch == "force-start" else terminal)["outcome"]),
+ "retired old task rewrote responsibility after actual drain")
+ if branch == "force-start":
+ require(canonical(status(probe, fleet, path, new, "overlap-new-quiet-status").get("outcome")) == canonical(terminal["outcome"]),
+ "old worker overwrote the replacement outcome after its completion")
+ elif branch == "disjoint":
+ require(canonical(status(probe, fleet, other_path, new, "overlap-disjoint-quiet-status").get("outcome")) == canonical(other_terminal["outcome"]),
+ "retired task rewrote a disjoint scope's responsibility")
+ if index < 2:
+ time.sleep(5)
+ require(all(physical_content(value) == physical_content(quiet[0])
+ and all((value[node]["pid"], value[node]["unit"]) == (quiet[0][node]["pid"], quiet[0][node]["unit"])
+ for node in fleet.nodes) for value in quiet[1:]), "retired task kept writing or service changed during live quiet proof")
+ return {branch: {"verdict": "PASS", **proof, "liveQuietSamples": 3, "betweenSamplesSeconds": 5,
+ "completePhysicalAndS3ProofRequired": True, "nativeCommitRaceClaimed": False}}
+
+
+def run_topology(args, topology, nodes, *, observer=None, branches=BRANCHES):
+ require(tuple(branches) == BRANCHES, "overlap executor omitted source branches")
+ results = []
+ for branch in branches:
+ branch_args = argparse.Namespace(**vars(args))
+ branch_args.evidence = args.evidence / branch
+ branch_args.evidence.mkdir(exist_ok=True)
+ indices = list(MISSING) + ([SCOPE_OBJECTS + index for index in MISSING] if branch == "disjoint" else [])
+ result = dx_topology(branch_args, topology, nodes,
+ observer=lambda *values, **options: observe_branch(branch, *values, **options), branches=(branch,),
+ source_branches=BRANCHES, count=OBJECTS, fixture_builder=seed_scopes, missing_indices=indices,
+ runtime_profile="overlap", policy_reference=POLICY)
+ results.append({"branch": branch, **result})
+ if result["cleanup"] != "PASS":
+ break
+ return {"topology": topology, "instances": results, "branches": {entry["branch"]: entry for entry in results},
+ "verdict": "PASS" if len(results) == len(branches) and all(entry["verdict"] == "PASS" for entry in results) else "FAIL",
+ "cleanup": "PASS" if results and all(entry["cleanup"] == "PASS" for entry in results) else "FAIL"}
+
+
+if __name__ == "__main__":
+ try:
+ dx_main(case_id=CASE_ID, branches=BRANCHES, runner=run_topology)
+ except Exception as exc:
+ print(f"Heal overlap validation failed: {exc}", file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_heal/owner_recovery.py b/scripts/validation_heal/owner_recovery.py
new file mode 100644
index 00000000..bdd45420
--- /dev/null
+++ b/scripts/validation_heal/owner_recovery.py
@@ -0,0 +1,177 @@
+"""Audit exact stopped J02 bytes before restoring four owner-marker UID/GID pairs."""
+
+from contextlib import ExitStack
+import hashlib
+import json
+import os
+from pathlib import Path
+import pwd
+import re
+import stat
+
+
+def require(value, message):
+ if not value:
+ raise ValueError(message)
+
+
+def encoded(value):
+ return json.dumps(value, sort_keys=True, separators=(',', ':')).encode()
+
+
+def sha(value):
+ return hashlib.sha256(encoded(value)).hexdigest()
+
+
+def identity(info):
+ return {name: getattr(info, 'st_' + name) for name in
+ ('dev', 'ino', 'uid', 'gid', 'mode', 'nlink', 'size', 'mtime_ns', 'ctime_ns')}
+
+
+def marker(path, checked, service):
+ with os.fdopen(os.open(checked(path, path.parent), os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK), 'rb') as stream:
+ before = os.fstat(stream.fileno())
+ require(stat.S_ISREG(before.st_mode) and before.st_nlink == 1 and before.st_size <= 16384
+ and stat.S_IMODE(before.st_mode) in {0o600, 0o644} and (before.st_uid, before.st_gid) in {(0, 0), service},
+ 'owner recovery marker has unknown permissions or ownership')
+ raw = stream.read(16385)
+ require(len(raw) == before.st_size and identity(before) == identity(os.fstat(stream.fileno())),
+ 'owner recovery marker changed during audit')
+ return json.loads(raw), dict(identity(before), sha256=hashlib.sha256(raw).hexdigest())
+
+
+def inspect(request, node):
+ state, roots = node['paths'](request['runId']); checked = node['checked']
+ expected = request['expectedOwner']; original = request['before']; faults = request['faults']
+ service = pwd.getpwnam('rustfs'); service = (service.pw_uid, service.pw_gid)
+ require(service[0] > 0 and isinstance(expected, dict) and expected.get('runId') == request['runId']
+ and expected.get('unit') == node['unit'](request['runId'])
+ and expected.get('hostname') == node['socket'].gethostname()
+ and expected.get('runtimeProfile') == 'outcome-legacy'
+ and re.fullmatch(r'rv-heal-[0-9a-f]{16}', expected.get('bucket', '')) is not None,
+ 'owner recovery requires the exact original J02 reservation')
+ state_info = checked(state, state.parent).lstat()
+ require(stat.S_ISDIR(state_info.st_mode) and state_info.st_uid == 0 and stat.S_IMODE(state_info.st_mode) == 0o700,
+ 'owner recovery lost its private root-owned state')
+ owner, central = marker(state / 'owner.json', checked, service)
+ require((central['uid'], central['gid']) == (0, 0) and owner == expected, 'owner recovery central reservation differs')
+ require(len(roots) == len(expected.get('mounts', [])) == 4
+ and all(all(a[k] == b[k] for k in ('target', 'source', 'fstype', 'uuid', 'maj:min'))
+ for a, b in zip(node['mounts'](), expected['mounts'], strict=True)), 'owner recovery mounts differ')
+ require(not any((state / name).exists() for name in ('J02-phase.json', 'J02-legacy-intent.json',
+ 'drive-gate.json', 'quorum-gate.json', 'write-quorum-gate.json', 'network-gate.json', 'disk-replacement.json')),
+ 'owner recovery has a phase or gate beyond the archived boundary')
+ stopped = node['stopped'](owner)
+ require(node['digest'](checked(state / 'rustfs', state)) == owner['binarySha256']
+ and owner.get('inspectorSha256') and node['digest'](checked(state / 'heal-meta-inspect', state)) == owner['inspectorSha256'],
+ 'owner recovery candidate or inspector changed')
+ wanted = {(e['disk'], e['path']): e for e in original['files']}
+ require(len(wanted) == len(original['files']) and 8 <= len(wanted) <= 1024 and len(original['formats']) == 4
+ and all(e['path'].startswith(owner['bucket'] + '/') for e in wanted.values()), 'owner recovery physical baseline differs')
+ require(len(faults) == 2 and len({f['disk'] for f in faults}) == 2
+ and all(type(f['disk']) is int and 0 <= f['disk'] < 4 and re.fullmatch(r'J02-[0-9a-f]{32}', f['faultId'])
+ and f['receipt']['relativePath'] == owner['bucket'] + '/opaque/source.bin/xl.meta'
+ and f['receipt']['runId'] == request['runId'] and f['receipt']['faultId'] == f['faultId']
+ and f['receipt']['kind'] == 'bitflip' and type(f['receipt']['offset']) is int
+ and f['receipt']['offset'] == 0 for f in faults), 'owner recovery original faults differ')
+ damaged = {(f['disk'], f['receipt']['relativePath']): f['receipt']['fault'] for f in faults}
+ require(all(f['receipt']['before'] == {k: wanted[(f['disk'], f['receipt']['relativePath'])][k] for k in ('size', 'sha256')}
+ and f['receipt']['backup'] == f['faultId'] + '.original'
+ and f['receipt']['fault']['size'] == f['receipt']['before']['size']
+ and re.fullmatch(r'[0-9a-f]{64}', f['receipt']['fault']['sha256']) for f in faults),
+ 'owner recovery faults are not bound to the original metadata')
+ raw_files, markers, backups, census = [], [], [], []
+ module, retained = node['fault_module'](), node['retained_scene_backend']
+ for disk, root in enumerate(roots):
+ checked(root, root.parent)
+ require(root.is_dir() and root.lstat().st_dev == node['MOUNTS'][disk].stat().st_dev
+ and not any(p == root or p.is_relative_to(root) for p in retained.mounted_paths()),
+ 'owner recovery lost a drive root or found another mount')
+ observed, evidence = marker(root / 'owner.json', checked, service)
+ require(observed == owner and evidence['sha256'] == central['sha256'] and evidence['mode'] == central['mode'], 'owner recovery drive reservation differs')
+ markers.append(dict(disk=disk, **evidence))
+ data = checked(root / 'data', root); folder = checked(data / owner['bucket'], data)
+ for directory, children, names in os.walk(folder, followlinks=False):
+ for name in (*children, *names): checked(Path(directory) / name, data)
+ for name in sorted(names):
+ path = Path(directory) / name; info = path.lstat(); relative = str(path.relative_to(data)); key = (disk, relative)
+ require(stat.S_ISREG(info.st_mode) and info.st_nlink == 1 and key in wanted
+ and info.st_size <= 64 * 1024**2, 'owner recovery has an extra or unsafe business file')
+ raw = dict(size=info.st_size, sha256=node['digest'](path))
+ require(identity(info) == identity(path.lstat()) and raw in
+ ({k: wanted[key][k] for k in ('size', 'sha256')}, damaged.get(key)),
+ 'owner recovery business bytes differ from original or exact fault')
+ raw_files.append(dict(disk=disk, path=relative, **raw))
+ format_path = checked(data / '.rustfs.sys/format.json', data)
+ require(format_path.stat().st_size < 65536 and original['formats'][disk]['disk'] == disk
+ and node['digest'](format_path) == original['formats'][disk]['sha256']
+ and json.loads(format_path.read_text()) == original['formats'][disk]['value'], 'owner recovery disk format differs')
+ for fault in (f for f in faults if f['disk'] == disk):
+ receipt = {k: v for k, v in fault['receipt'].items() if k != 'witnessedAt'}
+ with module.directory(root) as fd, module.parent(fd, ['quarantine']) as backup_fd:
+ binding = module.checked_owner(fd, request['runId'], recovery=True)
+ require(binding['bucketPrefix'] == owner['bucket'], 'owner recovery fault owner differs')
+ names = {fault['faultId'] + suffix for suffix in ('.original', '.intent.json', '.applied.json')}
+ preserved = fault['faultId'] + '.failed-' + receipt['fault']['sha256']
+ actual_names = set(os.listdir(backup_fd))
+ require(names <= actual_names <= names | {preserved}, 'owner recovery quarantine differs')
+ for name in sorted(actual_names):
+ raw = module.read_file(backup_fd, name, 64 * 1024**2)
+ if name.endswith('.intent.json'): require(json.loads(raw) == receipt, 'owner recovery intent differs')
+ elif name.endswith('.applied.json'): require(json.loads(raw) == fault['receipt'], 'owner recovery applied receipt differs')
+ else:
+ wanted_bytes = receipt['before'] if name.endswith('.original') else receipt['fault']
+ require(dict(size=len(raw), sha256=hashlib.sha256(raw).hexdigest()) == wanted_bytes, 'owner recovery backup bytes differ')
+ backups.append(dict(disk=disk, name=name, size=len(raw), sha256=hashlib.sha256(raw).hexdigest()))
+ if not any(f['disk'] == disk for f in faults):
+ require(not any(checked(root / 'quarantine', root).iterdir()), 'owner recovery has another fault backup')
+ census.append(retained.census(root, owner, state=False, node=node))
+ require(len(raw_files) == len(wanted) and {(e['disk'], e['path']) for e in raw_files} == set(wanted),
+ 'owner recovery lost an original business file')
+ contract = dict(runId=request['runId'], ownerSha256=sha(owner), beforeSha256=sha(original), faultsSha256=sha(faults))
+ intent_path = checked(state / 'owner-marker-recovery.intent.json', state)
+ prior = retained.private_json(intent_path) if intent_path.exists() else None
+ if prior is not None:
+ require(prior.get('contract') == contract and len(prior.get('markers', [])) == 4
+ and all(all(old[k] == now[k] for k in ('disk', 'dev', 'ino', 'mode', 'nlink', 'size', 'mtime_ns', 'sha256'))
+ for old, now in zip(prior['markers'], markers, strict=True)), 'owner recovery durable intent differs')
+ return dict(contract=contract, markers=markers, backupFiles=backups, rootCensus=census,
+ rawPhysicalSha256=sha(sorted(raw_files, key=lambda e: (e['disk'], e['path']))),
+ service=stopped, priorIntent=prior, markerRecoveryRequired=any((m['uid'], m['gid']) != (0, 0) for m in markers),
+ inspectionOnly=True, mutated=False, canonicalPassClaimed=False), state, roots
+
+
+def run(request, node):
+ fields = {'runId', 'action', 'expectedOwner', 'before', 'faults'}
+ if request['action'] == 'owner-markers-recover': fields.add('expectedAudit')
+ require(set(request) == fields and request['action'] in {'owner-markers-audit', 'owner-markers-recover'},
+ 'owner recovery accepts only its archived scene and audit')
+ audit, state, roots = inspect(request, node)
+ if request['action'] == 'owner-markers-audit': return audit
+ require(audit == request['expectedAudit'], 'owner recovery changed after read-only preflight')
+ module = node['fault_module'](); prior = audit['priorIntent']
+ if prior is None:
+ prior = {k: audit[k] for k in ('contract', 'markers', 'backupFiles', 'rootCensus', 'rawPhysicalSha256')}
+ with module.directory(state) as fd:
+ module.new_file(fd, 'owner-marker-recovery.intent.json', encoded(prior))
+ changed = []
+ with ExitStack() as stack:
+ descriptors = [stack.enter_context(os.fdopen(os.open(node['checked'](root / 'owner.json', root),
+ os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK), 'rb')) for root in roots]
+ for disk, stream in enumerate(descriptors):
+ fresh, _, _ = inspect(request, node)
+ require(fresh['rawPhysicalSha256'] == audit['rawPhysicalSha256'] and fresh['backupFiles'] == audit['backupFiles']
+ and all(all(a[k] == b[k] for k in ('disk', 'dev', 'ino', 'mode', 'nlink', 'size', 'mtime_ns', 'sha256'))
+ for a, b in zip(audit['markers'], fresh['markers'], strict=True)), 'owner recovery bytes or identities changed')
+ info = identity(os.fstat(stream.fileno()))
+ require(all(info[k] == fresh['markers'][disk][k] for k in info), 'owner recovery descriptor changed')
+ if (info['uid'], info['gid']) != (0, 0):
+ os.fchown(stream.fileno(), 0, 0); os.fsync(stream.fileno())
+ node['retained_scene_backend'].sync_directory(roots[disk])
+ changed.append(disk)
+ final, _, _ = inspect(request, node)
+ require(not final['markerRecoveryRequired'] and final['rawPhysicalSha256'] == audit['rawPhysicalSha256']
+ and final['backupFiles'] == audit['backupFiles'], 'owner recovery post-audit differs')
+ return dict(ownerMetadataRestored=True, changedDisks=changed, before=audit, after=final,
+ intentSha256=sha(prior), businessBytesChanged=False, backupRestorationUsed=False,
+ canonicalPassClaimed=False, originalVerdictRewritten=False)
diff --git a/scripts/validation_heal/performance.py b/scripts/validation_heal/performance.py
new file mode 100644
index 00000000..ad6bd94f
--- /dev/null
+++ b/scripts/validation_heal/performance.py
@@ -0,0 +1,444 @@
+#!/usr/bin/env python3
+"""J06: three fixed ABBA groups with concurrent ACK traffic and real repair."""
+
+from __future__ import annotations
+
+import argparse
+from concurrent.futures import ThreadPoolExecutor
+import copy
+from datetime import datetime, timedelta, timezone
+import hashlib
+import json
+import math
+import os
+from pathlib import Path
+import re
+import secrets
+import signal
+import sys
+import time
+from urllib.parse import quote
+
+try:
+ from .heal_probe import Probe, compare_inventories, payload, require, save
+ from .heal_workload import Recorder, seed, verify_seed
+ from .healthy import BASE_OPTIONS, Fleet, physical_content, validate_topology
+ from .inspector import verify as verify_inspector
+ from .matrix import select
+ from .heal_fault import bounded_fault_id
+ from .cursor import OBJECTS, SIZE, map_cohorts, verify_all_parts, validate_missing_batch, complete_outcome
+ from .shards import validate_repaired_physical
+ from .stop import idle_fleet, status
+ from .fleet_node import PROCESS_FIELDS
+except ImportError:
+ from heal_probe import Probe, compare_inventories, payload, require, save
+ from heal_workload import Recorder, seed, verify_seed
+ from healthy import BASE_OPTIONS, Fleet, physical_content, validate_topology
+ from inspector import verify as verify_inspector
+ from matrix import select
+ from heal_fault import bounded_fault_id
+ from cursor import OBJECTS, SIZE, map_cohorts, verify_all_parts, validate_missing_batch, complete_outcome
+ from shards import validate_repaired_physical
+ from stop import idle_fleet, status
+ from fleet_node import PROCESS_FIELDS
+
+
+CASE_ID = "heal.performance-abba"
+BRANCHES = ("abba-repeat-1", "abba-repeat-2", "abba-repeat-3")
+# The twelve fixed phases run close to the original 45-minute lease once
+# physical snapshots and remote cleanup are included. Keep the fault lease
+# alive for the whole candidate-bound run and leave room for final evidence.
+FAULT_LEASE_MINUTES = 120
+# These are acceptance gates chosen before deployment, not results derived
+# from the measured candidate. Changing one requires a new immutable run.
+PROTOCOL = {"groups": 3, "order": ["A", "B", "B", "A"], "secondsPerPhase": 30,
+ "requestsPerSecond": 4, "concurrency": 2, "readWriteRatio": "1:1",
+ "loadObjects": 16, "loadObjectBytes": 65536, "warmupRequests": 16,
+ "repairObjects": OBJECTS, "repairObjectBytes": SIZE, "missingIndices": list(range(0, OBJECTS, 64)),
+ "phaseAdmissionGraceSeconds": 1, "minimumSamplesPerOperation": 50,
+ "maxErrorRate": 0, "getP95Ms": 5000, "getP99Ms": 10000,
+ "putP95Ms": 5000, "putP99Ms": 10000, "maxPairedLatencyRatio": 3,
+ "minimumRunningOverlapSeconds": 1, "minimumBracketedCalls": 4,
+ "minimumRepairMiBPerSecond": .001, "maxProcessCpuCores": 2.5,
+ "maxProcessRssBytes": 2800 * 1024**2, "maxProcessFDs": 4096,
+ "metricsIntervalSeconds": 2, "repairDeadlineSeconds": 300,
+ "faultLeaseMinutes": FAULT_LEASE_MINUTES,
+ "cachePolicy": "same original four roots; restart every phase; fixed warmup; no host cache eviction",
+ "latencyScope": "collector scheduled admission through complete SDK response/body; includes queue delay and errors",
+ "retryPolicy": "no retries; every scheduled GET and PUT remains in the denominator",
+ "hardwareScope": "same candidate, rc, hosts, mounts and original disk UUIDs for all twelve phases"}
+
+
+def schedule(protocol=PROTOCOL):
+ require(protocol == PROTOCOL, "J06 requires the immutable preregistered protocol")
+ return [{"group": group, "phase": index, "mode": mode, "branch": BRANCHES[group - 1]}
+ for group in range(1, 4) for index, mode in enumerate(protocol["order"], 1)]
+
+
+def percentile(values, fraction):
+ require(values and all(type(value) in {int, float} and math.isfinite(value) and value >= 0 for value in values)
+ and fraction in {.95, .99}, "invalid performance sample or percentile")
+ return sorted(values)[math.ceil(len(values) * fraction) - 1]
+
+
+def load_body(bucket, index):
+ return payload(f"{bucket}/load/{index:02d}/0", PROTOCOL["loadObjectBytes"])
+
+
+def load_manifest(bucket):
+ return [{"key": f"load/{index:02d}.bin", "versionId": "null", "deleteMarker": False,
+ "isLatest": True, "payload": {"size": PROTOCOL["loadObjectBytes"],
+ "sha256": hashlib.sha256(load_body(bucket, index)).hexdigest()}}
+ for index in range(PROTOCOL["loadObjects"])]
+
+
+def prepare_load(recorder, bucket):
+ recorder.call("create_bucket", {"Bucket": bucket})
+ for index, entry in enumerate(load_manifest(bucket)):
+ response = recorder.call("put_object", {"Bucket": bucket, "Key": entry["key"]}, load_body(bucket, index))
+ require(response.get("VersionId") in {None, "null"}, "performance load bucket unexpectedly versioned")
+
+
+def repair_view(snapshot, bucket):
+ return {node: {**value, "files": [entry for entry in value["files"] if entry["path"].startswith(bucket + "/")]}
+ for node, value in snapshot.items()}
+
+
+def same_hardware(original, actual):
+ require(set(original) == set(actual), "ABBA hardware omitted or added a node")
+ for node in original:
+ require(original[node]["formats"] == actual[node]["formats"]
+ and original[node]["hostname"] == actual[node]["hostname"]
+ and original[node]["binarySha256"] == actual[node]["binarySha256"],
+ "ABBA candidate, host or disk identities changed")
+ return {"sameHostsCandidateAndDiskUUIDs": True}
+
+
+def request(probe, bucket, sequence, epoch, output):
+ operation = "get_object" if sequence % 2 == 0 else "put_object"
+ index = (sequence // 2) % PROTOCOL["loadObjects"]
+ body = load_body(bucket, index)
+ scheduled = epoch + sequence / PROTOCOL["requestsPerSecond"]
+ remaining = scheduled - time.monotonic()
+ if remaining > 0: time.sleep(remaining)
+ params = {"Bucket": bucket, "Key": f"load/{index:02d}.bin"}
+ prepared = {"sequence": sequence, "operation": operation, "params": params,
+ "scheduledMonotonicNs": int(scheduled * 1e9),
+ "expectedPayload": {"size": len(body), "sha256": hashlib.sha256(body).hexdigest()}}
+ path = output / f"{sequence:05d}-request.json"
+ save(path, prepared)
+ if time.monotonic() > epoch + PROTOCOL["secondsPerPhase"] + PROTOCOL["phaseAdmissionGraceSeconds"]:
+ observed = {"acknowledged": False, "resultUnknown": False, "clientCallInvoked": False,
+ "error": {"type": "AdmissionDeadline", "message": "scheduled sample missed the phase admission deadline"}}
+ else:
+ observed = probe.s3(operation, params, body if operation == "put_object" else None)
+ finished = time.monotonic_ns()
+ code = (observed.get("response") or {}).get("ResponseMetadata", {}).get("HTTPStatusCode")
+ success = observed.get("acknowledged") is True and observed.get("resultUnknown") is False and code == 200 \
+ and not observed.get("clientCleanupError")
+ if operation == "get_object": success = success and observed.get("payload") == prepared["expectedPayload"]
+ else: success = success and (observed.get("response") or {}).get("VersionId") in {None, "null"}
+ result = {**prepared, "requestSha256": hashlib.sha256(path.read_bytes()).hexdigest(), "observation": observed,
+ "finishedMonotonicNs": finished, "latencyMs": (finished - prepared["scheduledMonotonicNs"]) / 1e6,
+ "verdict": "PASS" if success else "FAIL"}
+ save(output / f"{sequence:05d}-ack.json", result)
+ return result
+
+
+def summarize(rows):
+ count = PROTOCOL["secondsPerPhase"] * PROTOCOL["requestsPerSecond"]
+ require(len(rows) == count and {row["sequence"] for row in rows} == set(range(count)),
+ "scheduled requests were omitted, duplicated or filtered")
+ statistics = {"scheduledRequests": count, "failedRequests": sum(row["verdict"] != "PASS" for row in rows)}
+ statistics["errorRate"] = statistics["failedRequests"] / count
+ for operation, label in (("get_object", "get"), ("put_object", "put")):
+ samples = [row["latencyMs"] for row in rows if row["operation"] == operation]
+ require(len(samples) == count // 2 >= PROTOCOL["minimumSamplesPerOperation"], "GET/PUT sample count is incomplete")
+ statistics[label] = {"requests": len(samples), "p95Ms": percentile(samples, .95), "p99Ms": percentile(samples, .99)}
+ return statistics
+
+
+def resource_delta(before, after):
+ require(set(before) == set(after) and len(before) == 4, "performance metrics require all four actual peers")
+ result = {}
+ for node, start in before.items():
+ end = after[node]
+ require(start.get("readOnly") is True and end.get("readOnly") is True
+ and all(start["identity"].get(name) == end["identity"].get(name) for name in PROCESS_FIELDS)
+ and start["clockDomain"] == end["clockDomain"] == start["identity"]["bootId"]
+ and type(start["clockTicksPerSecond"]) is int and start["clockTicksPerSecond"] > 0
+ and start["clockTicksPerSecond"] == end["clockTicksPerSecond"], "resource counters crossed a process or clock identity")
+ elapsed = (end["monotonicNs"] - start["monotonicNs"]) / 1e9
+ require(elapsed > 0 and end["cpuTicks"] >= start["cpuTicks"], "resource clock/CPU counters went backwards")
+ io = {}
+ for name in ("rchar", "wchar", "syscr", "syscw", "read_bytes", "write_bytes"):
+ require(type(start["io"].get(name)) is int and type(end["io"].get(name)) is int
+ and end["io"][name] >= start["io"][name], "process IO counters went backwards or were omitted")
+ io[name] = end["io"][name] - start["io"][name]
+ result[node] = {"elapsedSeconds": elapsed,
+ "cpuCores": (end["cpuTicks"] - start["cpuTicks"]) / start["clockTicksPerSecond"] / elapsed,
+ "maxSampledRssBytes": max(start["rssBytes"], end["rssBytes"]),
+ "maxSampledFDs": max(start["fileDescriptors"], end["fileDescriptors"]), "ioDelta": io}
+ return result
+
+
+def overlap(rows, windows):
+ intervals = []
+ for left, right in zip(windows, windows[1:]):
+ lc, rc = left["response"].get("outcome", {}).get("counters", {}), right["response"].get("outcome", {}).get("counters", {})
+ for counter in (lc, rc):
+ require(all(type(counter.get(name)) is int and counter[name] >= 0 for name in
+ ("processed", "healed", "unchanged", "skipped", "failed", "unknown", "attemptFailures")),
+ "active performance task lacks typed counters")
+ require(all(counter[name] == 0 for name in ("failed", "unknown", "attemptFailures"))
+ and counter.get("overflowed") is False, "performance task has failed or unknown responsibility")
+ require(rc["processed"] >= lc["processed"] and rc["healed"] >= lc["healed"], "same performance task progress went backwards")
+ if all(row["response"].get("summary") == "running" and row["response"].get("outcome", {}).get("execution", {}).get("state") == "running"
+ for row in (left, right)) and 0 < lc["processed"] < rc["processed"] < OBJECTS:
+ start, end = left["callFinishedMonotonicNs"], right["callStartedMonotonicNs"]
+ if end > start: intervals.append((start, end))
+ calls = [row for row in rows if row["observation"].get("clientCallInvoked") is True and any(
+ start <= row["observation"]["callStartedMonotonicNs"] <= row["observation"]["callFinishedMonotonicNs"] <= end
+ for start, end in intervals)]
+ duration = sum((end - start) / 1e9 for start, end in intervals)
+ require(duration >= PROTOCOL["minimumRunningOverlapSeconds"] and len(calls) >= PROTOCOL["minimumBracketedCalls"]
+ and {row["operation"] for row in calls} == {"get_object", "put_object"},
+ "NOT_OBSERVED: workload GET/PUT did not overlap independently witnessed nonzero partial repair")
+ return {"bracketedRunningSeconds": duration, "bracketedCalls": len(calls), "bothOperationsDuringRealRepair": True}
+
+
+def acceptance(phases):
+ require([{name: phase[name] for name in ("group", "phase", "mode", "branch")} for phase in phases] == schedule(),
+ "ABBA phases were omitted, reordered, repeated or selected after measurement")
+ ratios = []
+ for phase in phases:
+ require(phase["statistics"]["errorRate"] <= PROTOCOL["maxErrorRate"], "performance request error-rate gate failed")
+ for operation in ("get", "put"):
+ for quantile in ("95", "99"):
+ value = phase["statistics"][operation]["p" + quantile + "Ms"]
+ require(math.isfinite(value) and 0 <= value <= PROTOCOL[operation + "P" + quantile + "Ms"],
+ "absolute performance latency gate failed")
+ for node in phase["resources"].values():
+ require(node["cpuCores"] <= PROTOCOL["maxProcessCpuCores"]
+ and node["maxSampledRssBytes"] <= PROTOCOL["maxProcessRssBytes"]
+ and node["maxSampledFDs"] <= PROTOCOL["maxProcessFDs"], "performance resource gate failed")
+ if phase["mode"] == "B":
+ require(phase["overlap"]["bothOperationsDuringRealRepair"] is True and phase["repair"]["physicalVerified"] is True
+ and phase["repair"]["MiBPerSecond"] >= PROTOCOL["minimumRepairMiBPerSecond"],
+ "performance phase did not prove actual physical repair throughput")
+ else:
+ require(phase["noHeal"] is True and phase["repair"] is None, "baseline phase contains a heal task")
+ for group in range(1, 4):
+ selected = [phase for phase in phases if phase["group"] == group]
+ for operation in ("get", "put"):
+ for quantile in ("95", "99"):
+ name = "p" + quantile + "Ms"
+ a = sum(phase["statistics"][operation][name] for phase in selected if phase["mode"] == "A") / 2
+ b = sum(phase["statistics"][operation][name] for phase in selected if phase["mode"] == "B") / 2
+ require(a > 0 and b / a <= PROTOCOL["maxPairedLatencyRatio"], "paired ABBA latency regression gate failed")
+ ratios.append({"group": group, "operation": operation, "percentile": name, "aMeanMs": a,
+ "bMeanMs": b, "differenceMs": b - a, "ratio": b / a})
+ return {"allTwelvePhasesAccepted": True, "pairedComparisons": ratios,
+ "thresholdsFrozenBeforeDeployment": True, "functionalChecksAreSeparate": True}
+
+
+def sample_metrics(fleet, label):
+ return fleet.record(label, {node: fleet.remote(node, "process-metrics") for node in fleet.nodes})
+
+
+def run_phase(spec, fleet, probe, binding, bucket, expected, load_bucket, hardware, cohorts, active, directory):
+ label = f"g{spec['group']}-p{spec['phase']}-{spec['mode']}"
+ output = directory / label
+ output.mkdir()
+ fleet.record("phase-protocol-" + label, {**spec, "protocol": PROTOCOL})
+ idle_fleet(fleet, binding)
+ fleet.stop()
+ original = repair_view(fleet.snapshot("physical-phase-baseline-" + label, quiescent=True), bucket)
+ same_hardware(hardware, original)
+ targets = [cohorts[index] for index in PROTOCOL["missingIndices"]]
+ registrations, receipts = [], []
+ if spec["mode"] == "B":
+ for index, target in enumerate(targets):
+ params = {"disk": target["disk"], "relativePath": target["relativePath"], "kind": "missing",
+ "faultId": bounded_fault_id("J06", label, index), "expectedSha256": target["sha256"]}
+ fleet.faults.append((target["node"], params))
+ registrations.append((target["node"], params))
+ receipts.append(fleet.record("fault-" + label, {"node": target["node"], **fleet.remote(target["node"], "apply-fault", **params)}))
+ faulted = repair_view(fleet.snapshot("physical-phase-fault-" + label, quiescent=True), bucket)
+ fleet.record("fault-proof-" + label, validate_missing_batch(original, faulted, targets, receipts))
+ else: faulted = original
+ fleet.start()
+ before_trigger = repair_view(fleet.snapshot("physical-phase-before-trigger-" + label), bucket)
+ require(physical_content(faulted) == physical_content(before_trigger), "ATTRIBUTION_NOT_OBSERVED: ABBA fault changed before trigger")
+ for index in range(PROTOCOL["warmupRequests"]):
+ observed = fleet.record("warmup-" + label, probe.s3("get_object", {"Bucket": load_bucket, "Key": f"load/{index:02d}.bin"}))
+ require(observed.get("acknowledged") is True and observed.get("payload") == load_manifest(load_bucket)[index]["payload"], "fixed warmup failed")
+ initial_metrics = sample_metrics(fleet, "metrics-before-" + label)
+ path, token, windows, terminal = "/rustfs/admin/v3/heal/" + quote(bucket, safe=""), None, [], None
+ repair_started = None
+ if spec["mode"] == "B":
+ repair_started = time.monotonic()
+ start = fleet.record("trigger-" + label, probe.admin(path, [], json.dumps({**BASE_OPTIONS, "recursive": True}).encode()))
+ require(start.get("httpStatus") == 200 and isinstance(start.get("response"), dict), "ABBA repair admission failed")
+ token = start["response"].get("clientToken")
+ require(isinstance(token, str) and token, "ABBA repair omitted task identity")
+ active.append((path, token))
+ else: idle_fleet(fleet, binding)
+ epoch = time.monotonic()
+ samples, metric_rows = [], [initial_metrics]
+ next_metric = epoch + PROTOCOL["metricsIntervalSeconds"]
+ repair_finished = None
+ with ThreadPoolExecutor(max_workers=PROTOCOL["concurrency"]) as workers:
+ futures = [workers.submit(request, probe, load_bucket, sequence, epoch, output)
+ for sequence in range(PROTOCOL["secondsPerPhase"] * PROTOCOL["requestsPerSecond"])]
+ while not all(future.done() for future in futures) or token and terminal is None:
+ if token and terminal is None:
+ observed = fleet.record("performance-running-" + label, probe.admin(path, [("clientToken", token)], b""))
+ require(observed.get("httpStatus") == 200 and isinstance(observed.get("response"), dict), "ABBA task status failed")
+ windows.append(observed)
+ if observed["response"].get("summary") != "running":
+ terminal, repair_finished = observed["response"], time.monotonic()
+ active.remove((path, token))
+ require(time.monotonic() - repair_started <= PROTOCOL["repairDeadlineSeconds"], "ABBA task exceeded frozen deadline")
+ if time.monotonic() >= next_metric:
+ metric_rows.append(sample_metrics(fleet, "metrics-during-" + label))
+ if spec["mode"] == "A": idle_fleet(fleet, binding)
+ next_metric = time.monotonic() + PROTOCOL["metricsIntervalSeconds"]
+ time.sleep(.2)
+ samples = [future.result() for future in futures]
+ remaining = epoch + PROTOCOL["secondsPerPhase"] - time.monotonic()
+ if remaining > 0: time.sleep(remaining)
+ metric_rows.append(sample_metrics(fleet, "metrics-after-" + label))
+ resources = resource_delta(metric_rows[0], metric_rows[-1])
+ for left, right in zip(metric_rows, metric_rows[1:]):
+ delta = resource_delta(left, right)
+ for node in resources:
+ for name in ("cpuCores", "maxSampledRssBytes", "maxSampledFDs"):
+ resources[node][name] = max(resources[node][name], delta[node][name])
+ statistics = summarize(samples)
+ proof = {**spec, "statistics": statistics, "resources": resources, "taskId": token, "repair": None, "noHeal": token is None}
+ if token:
+ repaired_pairs = {(target["key"], "null") for target in targets}
+ proof["responsibility"] = complete_outcome(terminal, bucket, expected, repaired_pairs)
+ proof["overlap"] = overlap(samples, windows)
+ require(repair_finished > repair_started, "repair throughput has no measured positive duration")
+ idle_fleet(fleet, binding)
+ fleet.stop()
+ repaired = repair_view(fleet.snapshot("physical-phase-after-" + label, quiescent=True), bucket)
+ same_hardware(hardware, repaired)
+ if token:
+ fleet.record("physical-proof-" + label, validate_repaired_physical(original, repaired, targets[0], repair_targets=targets))
+ fleet.record("streaming-proof-" + label, verify_all_parts(fleet, cohorts, "part-batch-" + label))
+ byte_count = sum(target["size"] for target in targets)
+ proof["repair"] = {"physicalVerified": True, "originalShardBytes": byte_count,
+ "seconds": repair_finished - repair_started, "MiBPerSecond": byte_count / 1024**2 / (repair_finished - repair_started),
+ "scope": "actual reconstructed target shard bytes; not logical S3 bytes"}
+ for registration in registrations: fleet.faults.remove(registration)
+ else:
+ require(physical_content(original) == physical_content(repaired), "no-heal baseline changed the repair fixture")
+ fleet.start()
+ fleet.record("load-functional-after-" + label, verify_seed(load_manifest(load_bucket), probe.inventory(load_bucket, unversioned_current_tags=True)))
+ if token:
+ # A complete outcome was already tied to the original token before
+ # physical verification. Later GETs cannot create its repair evidence.
+ require(status(probe, fleet, path, token, "retained-terminal-" + label).get("outcome") == terminal.get("outcome"),
+ "physical/workload checks changed the retained original outcome")
+ fleet.record("phase-result-" + label, proof)
+ return proof
+
+
+def run(args, nodes):
+ directory = args.evidence / "H2"
+ directory.mkdir()
+ run_id = f"gh-{os.environ['GITHUB_RUN_ID']}-{os.environ.get('GITHUB_RUN_ATTEMPT', '1')}-h2"
+ bucket = "rv-heal-" + hashlib.sha256(run_id.encode()).hexdigest()[:16]
+ load_bucket = bucket + "-load"
+ fleet = Fleet(nodes, directory, run_id, args.tool_directory, resource_profile="cursor")
+ fleet.snapshot_buckets = [bucket, load_bucket]
+ candidate = json.loads(args.candidate.read_text())
+ access, secret = secrets.token_hex(16), secrets.token_hex(24)
+ os.environ.update(HEAL_ACCESS_KEY=access, HEAL_SECRET_KEY=secret)
+ binding = {"schemaVersion": 1, "runId": run_id, "jumpHostOnly": True, "exclusiveHealService": True,
+ "leaseExpiresAt": (datetime.now(timezone.utc) + timedelta(minutes=FAULT_LEASE_MINUTES)).isoformat(), "bucketPrefix": bucket,
+ "endpoints": {f"node{index}": f"http://rustfs-node{index}:9000" for index in range(1, 5)},
+ "defaultEndpoint": "node1", "candidateCommit": candidate["source_sha"], "harnessCommit": args.testing_commit,
+ "candidatePackageSha256": candidate["package_sha256"], "candidateBinarySha256": args.binary_sha,
+ "rcBinarySha256": hashlib.sha256(args.rc_bin.read_bytes()).hexdigest(),
+ "maxInventoryObjects": 1100, "maxInventoryBytes": 2 * 1024**3}
+ fleet.record("pre-registered-performance-protocol", copy.deepcopy(PROTOCOL))
+ fleet.record("binding", binding)
+ fleet.record("inspector-build", verify_inspector(args.tool_directory, candidate))
+ require(select(CASE_ID)[0]["variants"] == list(BRANCHES), "performance executor omitted a source repetition")
+ probe, active = Probe(binding), []
+ result = {"topology": "H2", "verdict": "RUNNING", "branches": {}, "phases": [], "cleanup": "PENDING"}
+ try:
+ fleet.deploy(args.binary_sha, bucket, access, secret)
+ fleet.record("topology-proof", validate_topology(fleet.snapshot("deployed-topology"), "H2"))
+ expected = seed(Recorder(probe, directory / "repair-fixture"), bucket, "DX", OBJECTS, [SIZE])
+ prepare_load(Recorder(probe, directory / "load-fixture"), load_bucket)
+ baseline = fleet.record("s3-repair-baseline", probe.inventory(bucket, unversioned_current_tags=True, page_size=127))
+ fleet.record("repair-ACK-manifest", {"expected": expected, **verify_seed(expected, baseline)})
+ load_baseline = fleet.record("s3-load-baseline", probe.inventory(load_bucket, unversioned_current_tags=True))
+ fleet.record("load-ACK-manifest", verify_seed(load_manifest(load_bucket), load_baseline))
+ fleet.stop()
+ hardware = repair_view(fleet.snapshot("physical-baseline", quiescent=True), bucket)
+ cohorts = map_cohorts(hardware, bucket, expected)
+ fleet.record("complete-streaming-baseline", verify_all_parts(fleet, cohorts, "baseline-part-batch"))
+ fleet.start()
+ for spec in schedule():
+ result["phases"].append(run_phase(spec, fleet, probe, binding, bucket, expected, load_bucket, hardware, cohorts, active, directory))
+ result["performanceAcceptance"] = acceptance(result["phases"])
+ for endpoint in binding["endpoints"]:
+ peer = Probe(binding, endpoint)
+ actual = fleet.record("complete-repair-S3-after", {"endpoint": endpoint, **peer.inventory(bucket, unversioned_current_tags=True, page_size=127)})
+ fleet.record("complete-repair-S3-proof", {"endpoint": endpoint, **verify_seed(expected, actual), **compare_inventories(baseline, actual)})
+ load_actual = fleet.record("complete-load-S3-after", {"endpoint": endpoint, **peer.inventory(load_bucket, unversioned_current_tags=True)})
+ fleet.record("complete-load-S3-proof", {"endpoint": endpoint, **verify_seed(load_manifest(load_bucket), load_actual),
+ **compare_inventories(load_baseline, load_actual)})
+ for branch in BRANCHES:
+ result["branches"][branch] = {"verdict": "PASS", "phases": [row for row in result["phases"] if row["branch"] == branch],
+ "independentPerformanceAndFunctionalGates": True}
+ result["verdict"] = "PASS"
+ except (Exception, KeyboardInterrupt) as exc:
+ result.update(verdict="FAIL", failure={"type": type(exc).__name__, "message": str(exc)})
+ try:
+ fleet.stop()
+ fleet.snapshot("physical-failure", quiescent=True)
+ except Exception as error: result["failureEvidenceError"] = str(error)
+ finally:
+ for path, token in active:
+ try: fleet.record("stop-unresolved", probe.admin(path, [("clientToken", token), ("forceStop", "true")], b""))
+ except Exception as exc: fleet.record("stop-error", {"error": str(exc)})
+ result["cleanup"] = "PASS" if fleet.cleanup() else "FAIL"
+ fleet.record("topology-result", result)
+ return result
+
+
+def main():
+ def interrupted(signum, frame): raise KeyboardInterrupt(f"Heal performance interrupted by signal {signum}")
+ signal.signal(signal.SIGTERM, interrupted)
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--candidate", type=Path, required=True)
+ parser.add_argument("--case-id", choices=(CASE_ID,), required=True)
+ parser.add_argument("--testing-commit", required=True)
+ parser.add_argument("--binary-sha", required=True)
+ parser.add_argument("--rc-bin", type=Path, required=True)
+ parser.add_argument("--evidence", type=Path, required=True)
+ parser.add_argument("--tool-directory", type=Path, required=True)
+ args = parser.parse_args()
+ nodes = os.environ.get("RUSTFS_NODES", "vm000 vm001 vm002 vm003").split()
+ require(len(nodes) >= 4 and len(nodes) == len(set(nodes)) and all(re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", node)
+ for node in nodes), "four current Heal nodes required")
+ require(re.fullmatch(r"[1-9][0-9]*", os.environ.get("GITHUB_RUN_ID", "")) and re.fullmatch(r"[0-9a-f]{64}", args.binary_sha),
+ "immutable workflow and candidate identities required")
+ result = run(args, nodes[:4])
+ complete = result["verdict"] == "PASS" and result["cleanup"] == "PASS"
+ save(args.evidence / "healthy-summary.json", {"caseId": CASE_ID, "results": [result], "cleanup": result["cleanup"], "complete": complete})
+ require(complete, "J06 requires all three full ABBA groups, independent performance/functional gates and cleanup")
+
+
+if __name__ == "__main__":
+ try: main()
+ except Exception as exc:
+ print(f"Heal performance validation failed: {exc}", file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_heal/persistence.py b/scripts/validation_heal/persistence.py
new file mode 100644
index 00000000..6c7b83c4
--- /dev/null
+++ b/scripts/validation_heal/persistence.py
@@ -0,0 +1,857 @@
+#!/usr/bin/env python3
+"""F07 checkpoint corruption prerequisites; incomplete branches stay blocked."""
+
+import argparse
+import copy
+from datetime import datetime, timedelta, timezone
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import secrets
+import signal
+import sys
+import time
+
+try:
+ from . import checkpoint as checkpoints, journal_fault as journal
+ from .heal_probe import Probe, canonical, compare_inventories, require, save
+ from .heal_workload import Recorder, seed, verify_seed
+ from .healthy import Fleet, physical_content, validate_topology
+ from .inspector import verify as verify_inspector
+ from .manager_fixture import verify as verify_manager
+ from .matrix import select
+ from .shards import files, map_target, run_task, validate_fault, validate_repaired_physical
+ from .stop import idle_fleet, status
+ from .root_restart import recovery_status, same_owner, TIMEOUT_SECONDS
+ from .scanner_repair import JournalWindow
+ from .fleet_node import PROCESS_FIELDS
+ from .persistence_mrf import run as run_mrf_checksum
+ from . import persistence_native
+ from .versions import public_version_id, verify_proof, version_identity
+except ImportError:
+ import checkpoint as checkpoints, journal_fault as journal
+ from heal_probe import Probe, canonical, compare_inventories, require, save
+ from heal_workload import Recorder, seed, verify_seed
+ from healthy import Fleet, physical_content, validate_topology
+ from inspector import verify as verify_inspector
+ from manager_fixture import verify as verify_manager
+ from matrix import select
+ from shards import files, map_target, run_task, validate_fault, validate_repaired_physical
+ from stop import idle_fleet, status
+ from root_restart import recovery_status, same_owner, TIMEOUT_SECONDS
+ from scanner_repair import JournalWindow
+ from fleet_node import PROCESS_FIELDS
+ from persistence_mrf import run as run_mrf_checksum
+ import persistence_native
+ from versions import public_version_id, verify_proof, version_identity
+
+
+CASE_ID = "heal.checkpoint-and-mrf-corruption"
+BRANCHES = ("checkpoint-truncate", "mrf-checksum", "future-schema", "stale-generation",
+ "journal-write-failure", "native-persist-fence")
+IMPLEMENTED = {"checkpoint-truncate": "truncate", "future-schema": "future-schema",
+ "stale-generation": "stale-generation"}
+BLOCKERS = {}
+SIZE = 5 * 1024**2 + 123
+TERMINAL_OPTIONS_REFERENCE = {"candidateCommits": (
+ "072b57b37559fbe626aeda8abf61617fdcbdd926", "1aa9be87d18cf941e5020f19e8622c26ba4b10ac"),
+ "sourceFiles": {
+ "crates/heal/src/heal/manager/scheduler.rs": "79ce1f9c278c664231b6a1eca7c521f8e98742fe6d17d8f852368da4e77cc33b",
+ "crates/heal/src/heal/manager.rs": "ba9289942fbbe8ce2632686ae86b9d1be0ae9923420e3b091d67148b4ee02094",
+ "crates/config/src/constants/heal.rs": "82b8614dfa7cc111e694138035e4a84ee23035c719f0286fc0565d7181ca014f"},
+ "runtimeSetting": {"RUSTFS_HEAL_TASK_TIMEOUT_SECS": str(TIMEOUT_SECONDS)},
+ "normalization": "scheduler get_or_insert fills only an explicit null timeout; all other fields stay exact"}
+
+
+def checkpoint_fixture(branch, fixture):
+ require(branch in {*IMPLEMENTED, "journal-write-failure"} and fixture in {"DB", "DV"},
+ "unreviewed checkpoint fixture")
+ if branch == "journal-write-failure":
+ # Keep all data and inodes inside the four fixed 128 MiB filesystems.
+ return (169 if fixture == "DB" else 36), 1024**2 + 123
+ if branch == "stale-generation":
+ # More real healthy responsibilities keep both durable generations
+ # observable without changing task deadlines or synthesizing records.
+ return (511 if fixture == "DB" else 144), 2 * 1024**2 + 123
+ return (257 if fixture == "DB" else 80), SIZE
+
+
+def same_terminal_options(admitted, terminal):
+ if not isinstance(admitted, dict) or not isinstance(terminal, dict):
+ return False
+ if journal.same_json(admitted, terminal):
+ return True
+ # The source-bound scheduler fills None from the process-verified fleet
+ # setting. It preserves explicit budgets, including a checkpointed one.
+ if "timeout" not in admitted or admitted["timeout"] is not None:
+ return False
+ return journal.same_json(terminal, {**admitted, "timeout": {"secs": TIMEOUT_SECONDS, "nanos": 0}})
+
+
+def exact_records(observed, token, family, *, allow_frozen=False):
+ names = journal.names(family, token)
+ require(isinstance(observed, dict) and len(observed) in {1, 4}, "journal omitted bound physical nodes")
+ present = {}
+ for node, census in observed.items():
+ rows = census.get("records")
+ require(census.get("readOnly") is True and (census.get("quiescent") is True
+ or allow_frozen and census.get("stable") is True and census.get("serviceFrozen") is True
+ and census.get("processBound") is True and census.get("independentReads") == 2)
+ and census.get("family") == family and census.get("taskId") == token
+ and census.get("directoryEnumerationUsed") is False
+ and isinstance(rows, list) and len(rows) == 4 * len(names),
+ "retention proof needs a stopped exact-path census")
+ seen = set()
+ for row in rows:
+ pair = row.get("disk"), row.get("relativePath")
+ require(type(pair[0]) is int and 0 <= pair[0] < 4 and pair[1] in names and pair not in seen
+ and type(row.get("present")) is bool, "journal census duplicated or escaped an exact disk/path")
+ seen.add(pair)
+ if row["present"]:
+ checkpoints.bytes_of(row)
+ require(pair[1] not in present, "journal record has multiple physical owners")
+ present[pair[1]] = {"node": node, **row}
+ else:
+ require(not any(name in row for name in ("size", "sha256", "rawBase64")),
+ "absent journal record contains invented bytes")
+ return present
+
+
+def failed_task(current):
+ outcome = current.get("outcome") or {}
+ require(current.get("summary") == "stopped" and current.get("settings") == checkpoints.OPTIONS
+ and outcome.get("execution") == {"state": "aborted", "reason": "untraversable"}
+ and outcome.get("coverage") == "partial"
+ and isinstance(current.get("detail"), str) and "checkpoint" in current["detail"].lower(),
+ "corrupt original checkpoint was silently acknowledged, cancelled or lost its diagnostic")
+ counters = outcome.get("counters") or {}
+ require(all(type(counters.get(name)) is int and counters[name] == 0 for name in
+ ("processed", "healed", "unchanged", "skipped", "failed", "unknown", "attemptFailures"))
+ and counters.get("overflowed") is False and outcome.get("objects") == []
+ and outcome.get("objectsTruncated") is False,
+ "checkpoint load failure invented object processing or repair")
+
+
+def retained_corruption(observed, roots, frozen, receipt, current, *, earlier=None):
+ token = frozen["taskId"]
+ failed_task(current)
+ require(receipt.get("applied") is True and receipt.get("originalBackupRetained") is True
+ and receipt.get("productRepairClaimed") is False
+ and receipt.get("family") == "checkpoint" and receipt.get("taskId") == token
+ and receipt.get("disk") == frozen["disk"]
+ and receipt.get("relativePath") == frozen["checkpoint"]["relativePath"]
+ and journal.same_json(receipt.get("before"), journal.entry(checkpoints.bytes_of(frozen["checkpoint"]))),
+ "journal injection receipt changed the frozen original task or bytes")
+ if receipt.get("kind") == "stale-generation": stale_receipt(earlier, frozen, receipt)
+ require(receipt.get("kind") in IMPLEMENTED.values()
+ and receipt.get("injected") == journal.entry(journal.transform(
+ checkpoints.bytes_of(frozen["checkpoint"]), receipt, {"bucket": frozen["bucket"]}, None,
+ earlier=checkpoints.bytes_of(earlier["checkpoint"]) if earlier is not None else None)),
+ "journal corruption differs from its reviewed transformation")
+ records = exact_records(observed, token, "checkpoint")
+ names = journal.names("checkpoint", token)
+ require(set(records) == set(names[:3]), "corrupt checkpoint/resume/blocked fence was deleted or unexpectedly replaced")
+ for row in records.values():
+ require((row["node"], row["disk"]) == (frozen["node"], frozen["disk"]),
+ "corrupt checkpoint or diagnostic moved to another physical owner")
+ require(journal.entry(checkpoints.bytes_of(records[names[0]])) == receipt.get("injected")
+ and checkpoints.bytes_of(records[names[1]]) == checkpoints.bytes_of(frozen["resume"])
+ and checkpoints.bytes_of(records[names[2]]) == b"blocked",
+ "product overwrote unknown bytes, changed resume ownership or omitted its blocked diagnostic")
+ root = exact_records(roots, token, "root")
+ root_names = journal.names("root", token)
+ # An explicit durable failed terminal may retire the runnable admission.
+ # The corruption and blocked checkpoint remain independently retained.
+ require(set(root) in (set(root_names[1:]), set(root_names)),
+ "original responsibility has no exact durable failure terminal and report")
+ owner = frozen["rootAdmission"]
+ require(all((row["node"], row["disk"]) == (owner["node"], owner["disk"]) for row in root.values()),
+ "durable failure report changed the original root owner")
+ terminal = journal.decode_json(checkpoints.bytes_of(root[root_names[1]]))
+ report = journal.decode_json(checkpoints.bytes_of(root[root_names[2]]))
+ require(type(terminal.get("schema")) is int and terminal["schema"] == 1 and terminal.get("task_id") == token
+ and journal.same_json(terminal.get("heal_type"), owner["value"]["heal_type"])
+ and same_terminal_options(owner["value"]["options"], terminal.get("options"))
+ and isinstance(terminal.get("status"), dict) and set(terminal["status"]) == {"Failed"}
+ and isinstance(terminal["status"]["Failed"], dict)
+ and isinstance(terminal["status"]["Failed"].get("error"), str)
+ and "checkpoint" in terminal["status"]["Failed"]["error"].lower()
+ and type(report.get("schema")) is int and report["schema"] == 1
+ and journal.same_json(report.get("terminal"), terminal)
+ and canonical(report.get("outcome")) == canonical(current["outcome"]),
+ "original responsibility lost its durable checkpoint failure diagnostic")
+ if root_names[0] in root:
+ require(checkpoints.bytes_of(root[root_names[0]]) == checkpoints.bytes_of(owner),
+ "retained runnable admission changed original responsibility")
+ return {"originalTaskId": token, "corruptBytesRetained": True, "resumeBytesRetained": True,
+ "blockedDiagnosticRetained": True, "durableOriginalFailureRetained": True,
+ "originalTaskRepairClaimed": False, "backupRescueUsed": False}
+
+
+def rescan_outcome(current, bucket, expected, repairs, incarnation):
+ wanted = {version_identity(entry): entry for entry in expected}
+ require(wanted and len(wanted) == len(expected) and repairs <= set(wanted), "rescan manifest duplicated or escaped ACKs")
+ outcome = current.get("outcome") or {}
+ # HealTaskStatusPayload omits a false legacy truncation flag. Canonical
+ # receipt truncation remains explicit and independently reconciled below.
+ require(current.get("summary") == "finished" and outcome.get("execution") == {"state": "completed"}
+ and current.get("settings") == checkpoints.OPTIONS and type(current.get("truncated", False)) is bool
+ and outcome.get("coverage") == "complete"
+ and outcome.get("objectsTruncated") is (len(wanted) > 128), "safe rescan lacks complete truthful coverage")
+ counts = {"processed": len(wanted), "healed": len(repairs), "unchanged": len(wanted) - len(repairs),
+ "skipped": 0, "failed": 0, "unknown": 0, "attemptFailures": 0, "overflowed": False}
+ require(all(type(outcome.get("counters", {}).get(name)) is type(value)
+ and outcome["counters"][name] == value for name, value in counts.items()),
+ "safe rescan counters differ from independent complete ACK and physical manifests")
+ objects = outcome.get("objects")
+ require(isinstance(objects, list) and len(objects) == min(128, len(wanted)), "rescan omitted bounded diagnostics")
+ seen = set()
+ for receipt in objects:
+ identity = receipt.get("identity") or {}
+ pair = identity.get("object"), public_version_id(identity.get("versionId"))
+ require(pair in wanted and pair not in seen and identity.get("kind") == "object"
+ and identity.get("bucket") == bucket and identity.get("bucketIncarnationId") == incarnation
+ and type(identity.get("poolIndex")) is int and identity["poolIndex"] == 0
+ and type(identity.get("setIndex")) is int and identity["setIndex"] == 0,
+ "rescan diagnostic changed an ACK identity, incarnation or set")
+ state = "repaired" if pair in repairs else "metadata_healthy" if wanted[pair]["deleteMarker"] else "verified_healthy"
+ require(receipt.get("disposition", {}).get("state") == state, "rescan diagnostic invented repair or hid unknown work")
+ seen.add(pair)
+ return {"canonicalCounters": counts, "completeIndependentManifestsRequired": True,
+ "boundedDiagnosticsAreCompleteManifest": len(wanted) <= 128, "originalFailedTaskReclassified": False}
+
+
+def stale_receipt(earlier, frozen, receipt):
+ require(isinstance(earlier, dict), "stale rollback has no original earlier generation")
+ checkpoints.after_generation(frozen, earlier)
+ require(earlier["value"]["admin"]["outcome"]["counters"]["healed"] == 0,
+ "NOT_OBSERVED: earlier capture is after the fixture's first actual repair")
+ require(receipt.get("kind") == "stale-generation" and receipt.get("family") == "checkpoint"
+ and receipt.get("taskId") == frozen["taskId"] and type(receipt.get("disk")) is int
+ and receipt["disk"] == frozen["disk"]
+ and receipt.get("relativePath") == frozen["checkpoint"]["relativePath"]
+ and receipt.get("captureId") == earlier["captureId"]
+ and journal.same_json(receipt.get("before"), journal.entry(checkpoints.bytes_of(frozen["checkpoint"])))
+ and journal.same_json(receipt.get("injected"), journal.entry(checkpoints.bytes_of(earlier["checkpoint"])))
+ and journal.same_json(receipt.get("capturedGeneration"), receipt["injected"])
+ and receipt.get("applied") is True and receipt.get("originalBackupRetained") is True
+ and receipt.get("productRepairClaimed") is False,
+ "stale injection changed the original task, physical owner or earlier product bytes")
+ return {"originalTaskId": frozen["taskId"], "exactEarlierProductBytesInstalled": True,
+ "oldCheckpointManagerCasClaimed": False, "staleRecordRejectionClaimed": False}
+
+
+def completed_original(roots, frozen, current):
+ token, owner = frozen["taskId"], frozen["rootAdmission"]
+ records = exact_records(roots, token, "root")
+ names = journal.names("root", token)
+ require(set(records) == set(names[1:]) and all((row["node"], row["disk"]) ==
+ (owner["node"], owner["disk"]) for row in records.values()),
+ "original replay lost its terminal/report owner or retained runnable responsibility")
+ terminal = journal.decode_json(checkpoints.bytes_of(records[names[1]]))
+ report = journal.decode_json(checkpoints.bytes_of(records[names[2]]))
+ require(type(terminal.get("schema")) is int and terminal["schema"] == 1
+ and terminal.get("task_id") == token and terminal.get("status") == "Completed"
+ and journal.same_json(terminal.get("heal_type"), owner["value"]["heal_type"])
+ and same_terminal_options(owner["value"]["options"], terminal.get("options"))
+ and type(report.get("schema")) is int and report["schema"] == 1
+ and journal.same_json(report.get("terminal"), terminal)
+ and journal.same_json(report.get("outcome"), current.get("outcome")),
+ "original replay silently retired work or changed its admitted scope/options/outcome")
+ return {"sameOriginalTaskAndPhysicalOwner": True, "durableCompletionMatchesFullManifest": True,
+ "originalResponsibilityRecreated": False, "backupRescueUsed": False}
+
+
+def successful_stale_replay(fleet, probe, binding, expected, baseline, original, targets,
+ frozen, earlier, receipt, fault_scene, current, active):
+ token, bucket = frozen["taskId"], frozen["bucket"]
+ stale_receipt(earlier, frozen, receipt)
+ indexed = files(fault_scene)
+ missing = [target for target in targets if (target["node"], target["disk"], target["relativePath"]) not in indexed]
+ require(missing, "NOT_OBSERVED: rollback had no remaining actual object repair responsibility")
+ proof = rescan_outcome(current, bucket, expected, {version_identity(target) for target in missing}, frozen["bucketIncarnationId"])
+ proof.pop("originalFailedTaskReclassified")
+ active.remove((checkpoints.PATH, token))
+ idle_fleet(fleet, binding)
+ fleet.stop()
+ roots = checkpoints.records(fleet, token, "completed-original-stale-replay", quiescent=True, family="root")
+ durable = completed_original(roots, frozen, current)
+ healed = fleet.snapshot("original-replay-physical-before-any-rescan-or-GET", quiescent=True)
+ fleet.record("complete-original-replay-physical-proof", validate_repaired_physical(original, healed, targets[0], repair_targets=targets))
+ for target in targets: verify_proof(fleet, target, "original-replay-full-cohort-integrity")
+ fleet.start()
+ admissions = []
+ rescanned = run_task(probe, fleet, {"bucket": bucket, "key": ""}, 2, active,
+ option_overrides={"recursive": True}, admissions=admissions)
+ require(len(admissions) == 1 and admissions[0]["clientToken"] != token,
+ "independent stale-record rescan reused the original responsibility")
+ rescan = rescan_outcome(rescanned, bucket, expected, set(), frozen["bucketIncarnationId"])
+ repeated = recovery_status(probe, fleet, token)
+ require(all(canonical(repeated.get(name)) == canonical(current.get(name)) for name in ("summary", "settings", "outcome", "truncated")),
+ "independent rescan rewrote the original replay result")
+ for endpoint in binding["endpoints"]:
+ peer = Probe(binding, endpoint)
+ same = status(peer, fleet, checkpoints.PATH, token, "same-original-stale-replay-peer")
+ require(all(canonical(same.get(name)) == canonical(current.get(name)) for name in ("summary", "settings", "outcome", "truncated")),
+ "original stale replay token did not converge on every peer")
+ inventory = fleet.record("complete-peer-S3", {"endpoint": endpoint, **peer.inventory(bucket,
+ unversioned_current_tags=expected[0]["versionId"] == "null")})
+ verify_seed(expected, inventory); compare_inventories(baseline, inventory)
+ fleet.stop()
+ require(physical_content(healed) == physical_content(fleet.snapshot("physical-after-rescan-and-business-reads", quiescent=True)),
+ "rescan or business reads repaired work missing from the original replay proof")
+ retained = checkpoints.records(fleet, token, "retained-original-stale-terminal-after-rescan", quiescent=True, family="root")
+ completed_original(retained, frozen, repeated)
+ require(canonical(exact_records(retained, token, "root")) == canonical(exact_records(roots, token, "root")),
+ "rescan or business reads changed original durable completion bytes")
+ fleet.record("complete-original-generation-reconciliation", {"earlier": earlier, "later": frozen,
+ "injection": receipt, "originalCompletion": proof, "durableCompletion": durable, "separateRescan": rescan,
+ "oldRecordResurrectionUsed": False, "canonicalCasePassClaimed": False})
+ # A completed product task may remove its checkpoint. Exact original-byte
+ # evidence and full repair proof precede discarding rescue registrations.
+ fleet.journal_faults.clear()
+ fleet.faults.clear()
+ return {"verdict": "PASS", "originalGenerationReconciliation": proof, "durableOriginalCompletion": durable,
+ "separateRescan": rescan, "staleRecordRejectionClaimed": False,
+ "completePhysicalAndS3ManifestsVerified": True, "canonicalCasePassClaimed": False}
+
+
+def retained_write_failure(observed, roots, frozen, current, events):
+ token = frozen["taskId"]
+ outcome = current.get("outcome") or {}
+ detail = current.get("detail", "")
+ require(current.get("summary") == "stopped" and current.get("settings") == checkpoints.OPTIONS
+ and outcome.get("execution") == {"state": "aborted", "reason": "untraversable"}
+ and outcome.get("coverage") == "partial" and isinstance(detail, str)
+ and "failed to save checkpoint" in detail.lower()
+ and any(word in detail.lower() for word in ("no space", "enospc", "os error 28")),
+ "journal write refusal was not an actual checkpoint ENOSPC diagnostic")
+ expected = frozen["value"]["admin"]["outcome"]
+ require(all(canonical(outcome.get(name)) == canonical(expected.get(name))
+ for name in ("counters", "objects", "objectsTruncated")),
+ "failed checkpoint write lost or invented acknowledged durable progress")
+ owner = checkpoints.owned_checkpoint(observed, token, frozen["bucket"])
+ require(owner["checkpoint"] == frozen["checkpoint"] and owner["resume"] == frozen["resume"],
+ "journal failure removed or rewrote the last original durable checkpoint/resume")
+ root = exact_records(roots, token, "root", allow_frozen=True)
+ names, original = journal.names("root", token), frozen["rootAdmission"]
+ require(set(root) in ({names[0]}, set(names[1:]), set(names))
+ and all((row["node"], row["disk"]) == (original["node"], original["disk"]) for row in root.values()),
+ "journal failure silently retired or changed its original durable owner")
+ if names[0] in root:
+ raw = checkpoints.bytes_of(root[names[0]])
+ if raw != checkpoints.bytes_of(original):
+ # checkpoint_failed_execution may consume the original timeout;
+ # every other admitted field and the physical owner stay fixed.
+ same_owner(original, {**root[names[0]], "value": journal.decode_json(raw)})
+ if names[1] in root:
+ terminal = journal.decode_json(checkpoints.bytes_of(root[names[1]]))
+ report = journal.decode_json(checkpoints.bytes_of(root[names[2]]))
+ require(type(terminal.get("schema")) is int and terminal["schema"] == 1 and terminal.get("task_id") == token
+ and journal.same_json(terminal.get("heal_type"), original["value"]["heal_type"])
+ and same_terminal_options(original["value"]["options"], terminal.get("options"))
+ and terminal.get("status") == {"Failed": {"error": detail}}
+ and type(report.get("schema")) is int and report["schema"] == 1
+ and journal.same_json(report.get("terminal"), terminal)
+ and journal.same_json(report.get("outcome"), outcome),
+ "journal failure lost its exact durable original failed terminal/report")
+ diagnostics = [event for values in events.values() for event in values
+ if event.get("target") == "rustfs::heal::task" and event.get("message") == "Heal task failed"
+ and event.get("task_id") == token and isinstance(event.get("error"), str)
+ and "failed to save checkpoint" in event["error"].lower()
+ and any(word in event["error"].lower() for word in ("no space", "enospc", "os error 28"))]
+ require(diagnostics, "NOT_OBSERVED: candidate has no original-task checkpoint write error")
+ return {"originalTaskId": token, "lastDurableProgressRetained": True, "originalResponsibilityRetained": True,
+ "originalAdmissionRetained": names[0] in root,
+ "actualCheckpointWriteFailure": True, "diagnostics": diagnostics,
+ "failedWriteAcknowledged": False, "originalFailureReclassified": False, "backupRescueUsed": False}
+
+
+def recover_write_before_rescan(fleet, probe, binding, expected, baseline, original, targets,
+ frozen, failed, failure_proof, failure_scene, remaining, events, active):
+ token, bucket = frozen["taskId"], frozen["bucket"]
+ # Export the first failed attempt before product recovery may legitimately
+ # replace its runtime status with a successful replay of the same token.
+ fleet.record("original-write-failure-before-recovery", {"firstFailedAttempt": failed, "proof": failure_proof})
+ fleet.stop()
+ require(physical_content(fleet.snapshot("write-failure-before-original-recovery", quiescent=True))
+ == physical_content(failure_scene), "space release changed the original failed repair scene")
+ retained_write_failure(checkpoints.records(fleet, token, "write-failure-checkpoint-before-recovery", quiescent=True),
+ checkpoints.records(fleet, token, "write-failure-root-before-recovery", quiescent=True, family="root"),
+ frozen, failed, events)
+ fleet.start()
+ deadline = time.monotonic() + 180
+ while True:
+ require(time.monotonic() < deadline, "released original write responsibility did not reach a bounded recovery outcome")
+ current = recovery_status(probe, fleet, token)
+ require(time.monotonic() < deadline, "released original write responsibility did not reach a bounded recovery outcome")
+ if current.get("summary") != "running": break
+ time.sleep(.2)
+ fleet.stop()
+ roots = checkpoints.records(fleet, token, "write-original-root-after-recovery", quiescent=True, family="root")
+ if current.get("summary") != "finished":
+ require(all(canonical(current.get(name)) == canonical(failed.get(name))
+ for name in ("summary", "settings", "outcome", "detail")),
+ "original write recovery neither retained its failure nor completed verified work")
+ retained = retained_write_failure(
+ checkpoints.records(fleet, token, "write-original-checkpoint-after-recovery", quiescent=True),
+ roots, frozen, current, events)
+ require(retained["originalAdmissionRetained"] is False,
+ "original write responsibility still owns its durable scope after recovery; separate rescan is not admitted")
+ require(physical_content(fleet.snapshot("write-original-failed-recovery-physical", quiescent=True))
+ == physical_content(failure_scene), "failed original recovery changed its acknowledged repair scene")
+ fleet.start()
+ return None
+ completion = rescan_outcome(current, bucket, expected,
+ {version_identity(target) for target in remaining}, frozen["bucketIncarnationId"])
+ completion.pop("originalFailedTaskReclassified")
+ durable = completed_original(roots, frozen, current)
+ healed = fleet.snapshot("write-original-replay-physical-before-rescan-or-GET", quiescent=True)
+ fleet.record("write-original-replay-complete-physical-proof",
+ validate_repaired_physical(original, healed, targets[0], repair_targets=targets))
+ for target in targets: verify_proof(fleet, target, "write-original-replay-full-shard-integrity")
+ fleet.start()
+ admissions = []
+ rescanned = run_task(probe, fleet, {"bucket": bucket, "key": ""}, 2, active,
+ option_overrides={"recursive": True}, admissions=admissions)
+ require(len(admissions) == 1 and admissions[0]["clientToken"] != token,
+ "write recovery rescan reused or relabelled the original responsibility")
+ rescan = rescan_outcome(rescanned, bucket, expected, set(), frozen["bucketIncarnationId"])
+ repeated = recovery_status(probe, fleet, token)
+ require(all(canonical(repeated.get(name)) == canonical(current.get(name))
+ for name in ("summary", "settings", "outcome", "detail", "truncated")),
+ "separate rescan rewrote the original write recovery result")
+ for endpoint in binding["endpoints"]:
+ peer = Probe(binding, endpoint)
+ same = status(peer, fleet, checkpoints.PATH, token, "same-original-write-replay-peer")
+ require(all(canonical(same.get(name)) == canonical(current.get(name))
+ for name in ("summary", "settings", "outcome", "detail", "truncated")),
+ "original write replay did not converge on every peer")
+ inventory = fleet.record("complete-peer-S3", {"endpoint": endpoint, **peer.inventory(bucket,
+ unversioned_current_tags=expected[0]["versionId"] == "null")})
+ verify_seed(expected, inventory); compare_inventories(baseline, inventory)
+ fleet.stop()
+ require(physical_content(healed) == physical_content(fleet.snapshot("write-replay-after-rescan-and-reads", quiescent=True)),
+ "rescan or business reads repaired work missing from the original write replay proof")
+ retained = checkpoints.records(fleet, token, "retained-write-original-completion", quiescent=True, family="root")
+ completed_original(retained, frozen, repeated)
+ require(canonical(exact_records(retained, token, "root")) == canonical(exact_records(roots, token, "root")),
+ "rescan or business reads changed the original write replay durable completion")
+ replay = {"realSameTaskReplayBeforeSeparateRescan": True, "failedFirstAttemptRetained": True,
+ "separateRescanIsOriginalTaskCompletion": False, "durableOriginalCompletion": durable}
+ fleet.record("complete-original-journal-write-reconciliation", {"firstFailedAttempt": failed,
+ "retainedFailureProof": failure_proof, "originalReplayCompletion": current,
+ "originalReplayPhysicalAndCounters": completion, "separateRescan": rescan, "restartProof": replay})
+ fleet.faults.clear()
+ return {"verdict": "PASS", "originalWriteFailure": failure_proof, "separateRescan": rescan,
+ "originalAfterRestart": replay, "completePhysicalAndS3ManifestsVerified": True, "canonicalCasePassClaimed": False}
+
+
+def journal_write_failure(fleet, probe, binding, expected, baseline, original, targets, frozen, active):
+ token, bucket = frozen["taskId"], frozen["bucket"]
+ require(frozen["value"]["admin"]["outcome"]["counters"]["healed"] == 0,
+ "NOT_OBSERVED: journal failure missed the checkpoint before any actual repair")
+ window = JournalWindow(fleet, "F07-original-checkpoint-write-failure", event_targets={"rustfs::heal::task", "rustfs::heal::manager"})
+ for node in fleet.nodes:
+ filled = fleet.record("stopped-private-journal-exhaustion", {"node": node,
+ **fleet.remote(node, "bounded-volumes-fill", quiescent=True)})
+ require(filled.get("quiescent") is True and filled.get("hostSharedMountRemounted") is False
+ and filled.get("candidateProcessWitnessRequired") is True
+ and len(filled.get("changes", [])) == 4
+ and all(row.get("actualErrno") == 28 and row.get("freeBlocks") == 0 for row in filled["changes"]),
+ "journal failure did not exhaust only its four fixed owned volumes")
+ fleet.start()
+ for node in fleet.nodes:
+ identity = fleet.remote(node, "process")
+ witness = fleet.record("candidate-private-journal-ENOSPC", {"node": node,
+ **fleet.remote(node, "bounded-volumes-witness", expected=identity)})
+ require(witness.get("sameOriginalProcess") is True and witness.get("hostSharedMountRemounted") is False
+ and len(witness.get("disks", [])) == 4 and all(row.get("writeErrno") == 28
+ and row.get("sameServiceUidAndMountNamespace") is True for row in witness["disks"]),
+ "journal write fault lacks a real candidate UID/mount/process ENOSPC witness")
+ deadline = time.monotonic() + 180
+ while True:
+ current = recovery_status(probe, fleet, token)
+ if current.get("summary") != "running": break
+ require(time.monotonic() < deadline, "original checkpoint write failure exceeded its bounded observation window")
+ time.sleep(.2)
+ # Pause every process before exporting the failed durable scene or freeing
+ # space. Partial pause/fill ACKs remain recoverable by the Fleet registry.
+ for node in fleet.nodes:
+ identity = fleet.record("write-failure-before-freeze", {"node": node, **fleet.remote(node, "process")})
+ fleet.frozen_processes[node] = {name: identity[name] for name in identity if name != "node"}
+ fleet.record("write-failure-freeze", {"node": node, **fleet.remote(node, "process-signal",
+ signal="SIGSTOP", expected=fleet.frozen_processes[node])})
+ after = fleet.remote(node, "process")
+ require(after.get("state") == "T" and all(after.get(name) == identity.get(name) for name in PROCESS_FIELDS),
+ "journal failure freeze missed its original candidate")
+ proof = retained_write_failure(checkpoints.records(fleet, token, "original-write-failed-checkpoint"),
+ checkpoints.records(fleet, token, "original-write-failed-admission", family="root"), frozen, current, window.capture())
+ scene = fleet.snapshot("original-journal-write-failure-physical")
+ indexed = files(scene)
+ remaining = [target for target in targets if (target["node"], target["disk"], target["relativePath"]) not in indexed]
+ require(remaining, "NOT_OBSERVED: failed journal task left no actual repair for its separate rescan")
+ partial = copy.deepcopy(original)
+ missing = {(target["node"], target["disk"], target["relativePath"]) for target in remaining}
+ for node, value in partial.items():
+ value["files"] = [row for row in value["files"] if (node, row["disk"], row["path"]) not in missing]
+ validate_repaired_physical(partial, scene, targets[0], repair_targets=targets)
+ for node in fleet.nodes:
+ released = fleet.record("release-only-owned-journal-filler", {"node": node,
+ **fleet.remote(node, "bounded-volumes-release", expected=fleet.frozen_processes[node])})
+ require(released.get("sameOriginalProcess") is True and released.get("hostSharedMountRemounted") is False
+ and len(released.get("disks", [])) == 4 and all(row.get("writeErrno") == 0
+ and row.get("freeBlocks", 0) > 0 for row in released["disks"]),
+ "journal release did not restore its candidate's exact owned resource")
+ for node, identity in tuple(fleet.frozen_processes.items()):
+ fleet.record("write-failure-resume-after-release", {"node": node,
+ **fleet.remote(node, "process-signal", signal="SIGCONT", expected=identity)})
+ del fleet.frozen_processes[node]
+ active.remove((checkpoints.PATH, token))
+ idle_fleet(fleet, binding)
+ if proof["originalAdmissionRetained"]:
+ recovered = recover_write_before_rescan(fleet, probe, binding, expected, baseline, original, targets,
+ frozen, current, proof, scene, remaining, window.events, active)
+ if recovered is not None: return recovered
+ admissions = []
+ rescanned = run_task(probe, fleet, {"bucket": bucket, "key": ""}, 2, active,
+ option_overrides={"recursive": True}, admissions=admissions)
+ require(len(admissions) == 1 and admissions[0]["clientToken"] != token,
+ "journal release rescan reused the failed original responsibility")
+ rescan = rescan_outcome(rescanned, bucket, expected, {version_identity(target) for target in remaining}, frozen["bucketIncarnationId"])
+ repeated = recovery_status(probe, fleet, token)
+ require(all(canonical(repeated.get(name)) == canonical(current.get(name)) for name in ("summary", "settings", "outcome", "detail")),
+ "safe rescan rewrote the original journal write failure")
+ fleet.stop()
+ repaired = fleet.snapshot("journal-rescan-complete-physical-before-any-GET", quiescent=True)
+ validate_repaired_physical(original, repaired, targets[0], repair_targets=targets)
+ for target in targets: verify_proof(fleet, target, "journal-rescan-full-shard-integrity")
+ retained_write_failure(checkpoints.records(fleet, token, "retained-write-failed-checkpoint", quiescent=True),
+ checkpoints.records(fleet, token, "retained-write-failed-admission", quiescent=True, family="root"), frozen, repeated, window.events)
+ fleet.start()
+ deadline = time.monotonic() + 180
+ while True:
+ final = recovery_status(probe, fleet, token)
+ if final.get("summary") != "running": break
+ require(time.monotonic() < deadline, "released original responsibility has no bounded restart outcome")
+ time.sleep(.2)
+ if final.get("summary") == "finished":
+ replay = rescan_outcome(final, bucket, expected, set(), frozen["bucketIncarnationId"])
+ replay.pop("originalFailedTaskReclassified")
+ replay.update(realSameTaskReplayAfterSeparateRescan=True, failedFirstAttemptRetained=True,
+ separateRescanIsOriginalTaskCompletion=False)
+ else:
+ require(all(canonical(final.get(name)) == canonical(current.get(name)) for name in ("summary", "settings", "outcome", "detail")),
+ "restart neither retained the original failure nor completed a verified same-task replay")
+ replay = {"originalFailureStillRetained": True, "sameTaskCompletionClaimed": False}
+ for endpoint in binding["endpoints"]:
+ peer = Probe(binding, endpoint)
+ same = status(peer, fleet, checkpoints.PATH, token, "same-original-write-failure-peer")
+ require(all(canonical(same.get(name)) == canonical(final.get(name)) for name in ("summary", "settings", "outcome", "detail")),
+ "peer lost the failed original journal responsibility")
+ inventory = fleet.record("complete-peer-S3", {"endpoint": endpoint, **peer.inventory(bucket,
+ unversioned_current_tags=expected[0]["versionId"] == "null")})
+ verify_seed(expected, inventory); compare_inventories(baseline, inventory)
+ fleet.stop()
+ require(physical_content(repaired) == physical_content(fleet.snapshot("journal-after-business-reads", quiescent=True)),
+ "business reads repaired objects missing from the journal rescan proof")
+ final_roots = checkpoints.records(fleet, token, "retained-write-admission-after-reads", quiescent=True, family="root")
+ if final.get("summary") == "finished": replay["durableOriginalCompletion"] = completed_original(final_roots, frozen, final)
+ else:
+ retained_write_failure(checkpoints.records(fleet, token, "retained-write-failure-after-reads", quiescent=True),
+ final_roots, frozen, final, window.events)
+ fleet.record("complete-original-journal-write-reconciliation", {"firstFailedAttempt": current,
+ "retainedFailureProof": proof, "separateRescan": rescan, "originalAfterRestart": final, "restartProof": replay})
+ fleet.faults.clear()
+ return {"verdict": "PASS", "originalWriteFailure": proof, "separateRescan": rescan, "originalAfterRestart": replay,
+ "completePhysicalAndS3ManifestsVerified": True, "canonicalCasePassClaimed": False}
+
+
+def run_checkpoint_fault(args, topology, nodes, fixture, branch, candidate):
+ require(branch in IMPLEMENTED or branch == "journal-write-failure", "unimplemented journal branch cannot mutate the fleet")
+ require(candidate.get("source_sha") in TERMINAL_OPTIONS_REFERENCE["candidateCommits"],
+ "unsupported candidate terminal-options contract")
+ directory = args.evidence / fixture / topology / branch
+ directory.mkdir(parents=True)
+ run_id = f"gh-{os.environ['GITHUB_RUN_ID']}-{os.environ.get('GITHUB_RUN_ATTEMPT', '1')}-{topology.lower()}"
+ bounded = branch == "journal-write-failure"
+ fleet = Fleet(nodes, directory, run_id, args.tool_directory, resource_profile="cursor",
+ runtime_profile="journal-bounded" if bounded else "root-restart")
+ bucket = "rv-heal-" + hashlib.sha256(run_id.encode()).hexdigest()[:16]
+ fleet.snapshot_buckets = [bucket]
+ access, secret = secrets.token_hex(16), secrets.token_hex(24)
+ os.environ.update(HEAL_ACCESS_KEY=access, HEAL_SECRET_KEY=secret)
+ count, object_size = checkpoint_fixture(branch, fixture)
+ binding = {"schemaVersion": 1, "runId": run_id, "jumpHostOnly": True, "exclusiveHealService": True,
+ "leaseExpiresAt": (datetime.now(timezone.utc) + timedelta(minutes=45)).isoformat(), "bucketPrefix": bucket,
+ "endpoints": {f"node{index}": f"http://rustfs-node{index}:9000" for index in range(1, len(nodes) + 1)},
+ "defaultEndpoint": "node1", "candidateCommit": candidate["source_sha"], "harnessCommit": args.testing_commit,
+ "candidatePackageSha256": candidate["package_sha256"], "candidateBinarySha256": args.binary_sha,
+ "rcBinarySha256": hashlib.sha256(args.rc_bin.read_bytes()).hexdigest(),
+ "maxInventoryObjects": 512, "maxInventoryBytes": 2 * 1024**3}
+ fleet.record("binding", binding)
+ fleet.record("candidate-bound-inspector", verify_inspector(args.tool_directory, candidate))
+ fleet.record("candidate-terminal-options-contract", {
+ **TERMINAL_OPTIONS_REFERENCE, "candidateCommit": candidate["source_sha"]})
+ probe, active, pending = Probe(binding), [], None
+ result = {"topology": topology, "fixture": fixture, "branch": branch, "verdict": "RUNNING", "cleanup": "PENDING"}
+ def allocate():
+ for node in nodes:
+ fleet.record("fixed-private-journal-volume-allocation", {"node": node,
+ **fleet.remote(node, "bounded-volumes-create", branch="bounded-volume-enospc")})
+ try:
+ if bounded: fleet.deploy(args.binary_sha, bucket, access, secret, before_start=allocate)
+ else: fleet.deploy(args.binary_sha, bucket, access, secret)
+ fleet.record("topology-proof", validate_topology(fleet.snapshot("deployed-topology"), topology))
+ pending = {"operation": "seed-original-checkpoint-fixture"}
+ expected = seed(Recorder(probe, directory / "fixture"), bucket, fixture, count, [object_size])
+ baseline = fleet.record("ACK-bound-complete-S3", probe.inventory(bucket, unversioned_current_tags=fixture == "DB"))
+ fleet.record("complete-ACK-manifest", {"expected": expected, **verify_seed(expected, baseline)})
+ pending = None
+ idle_fleet(fleet, binding)
+ fleet.stop()
+ original = fleet.snapshot("healthy-physical-baseline", quiescent=True)
+ targets = [map_target(original, bucket, f"objects/{index:04d}.bin", "data", 1,
+ next(entry["versionId"] for entry in expected if entry["key"] == f"objects/{index:04d}.bin"
+ and not entry["deleteMarker"]), latest_required=False) for index in range(count - 4, count)]
+ prior = original
+ for index, target in enumerate(targets):
+ verify_proof(fleet, target, "baseline-full-cohort-bitrot")
+ params = {"disk": target["disk"], "faultId": f"F07-shard-{index}"}
+ fleet.faults.append((target["node"], params))
+ receipt = fleet.record("missing-original-shard", fleet.remote(target["node"], "apply-fault", **params,
+ relativePath=target["relativePath"], expectedSha256=target["sha256"], kind="missing"))
+ current = fleet.snapshot("actual-missing-shard", quiescent=True)
+ validate_fault(prior, current, target, receipt)
+ prior = current
+ fleet.start()
+ require(physical_content(prior) == physical_content(fleet.snapshot("before-original-admission")),
+ "ATTRIBUTION_NOT_OBSERVED: background activity changed the original shard faults")
+ pending = {"operation": "admit-original-checkpoint-task"}
+ token = checkpoints.admit(probe, fleet, active)
+ pending = None
+ earlier = checkpoints.capture_earlier(fleet, token, bucket, "F07-earlier-generation", advance=True) if branch == "stale-generation" else None
+ if earlier is not None:
+ require(earlier["value"]["admin"]["outcome"]["counters"]["healed"] == 0,
+ "NOT_OBSERVED: earlier generation missed the pre-repair capture window")
+ frozen, identity = checkpoints.freeze(fleet, token, bucket, earlier=earlier)
+ frozen = checkpoints.stop_for_fault(fleet, token, bucket, frozen, identity)
+ if bounded:
+ result.update(journal_write_failure(fleet, probe, binding, expected, baseline, original, targets, frozen, active))
+ return result
+ params = {"family": "checkpoint", "taskId": token, "disk": frozen["disk"],
+ "relativePath": frozen["checkpoint"]["relativePath"], "faultId": "F07-" + branch,
+ "kind": IMPLEMENTED[branch], "expectedBefore": journal.entry(checkpoints.bytes_of(frozen["checkpoint"]))}
+ if earlier is not None: params["captureId"] = earlier["captureId"]
+ fleet.journal_faults.append((frozen["node"], params))
+ receipt = fleet.record("stopped-original-checkpoint-injection", fleet.remote(frozen["node"], "journal-apply", **params))
+ if earlier is not None:
+ fleet.record("original-earlier-generation-injection-proof", stale_receipt(earlier, frozen, receipt))
+ injected = checkpoints.owned_checkpoint(checkpoints.records(fleet, token, "independent-injected-original-generation", quiescent=True), token, bucket)
+ require(checkpoints.bytes_of(injected["checkpoint"]) == checkpoints.bytes_of(earlier["checkpoint"])
+ and injected["resume"] == frozen["resume"], "stale rollback changed its original companion or actual bytes")
+ fault_scene = fleet.snapshot("physical-at-original-generation-replay-boundary", quiescent=True)
+ fleet.start()
+ for node in fleet.nodes:
+ identity = fleet.record("post-injection-candidate-process", {"node": node, **fleet.remote(node, "process")})
+ require(identity.get("settings", {}).get("RUSTFS_HEAL_TASK_TIMEOUT_SECS") == str(TIMEOUT_SECONDS),
+ "terminal normalization has no original candidate runtime timeout witness")
+ deadline = time.monotonic() + 180
+ while True:
+ failed = recovery_status(probe, fleet, token)
+ if failed.get("summary") != "running": break
+ require(time.monotonic() < deadline, "original corrupt checkpoint task did not produce a bounded diagnostic")
+ time.sleep(.2)
+ if earlier is not None and failed.get("summary") == "finished":
+ result.update(successful_stale_replay(fleet, probe, binding, expected, baseline, original, targets,
+ frozen, earlier, receipt, fault_scene, failed, active))
+ return result
+ failed_task(failed)
+ active.remove((checkpoints.PATH, token))
+ idle_fleet(fleet, binding)
+ fleet.stop()
+ corrupt_scene = checkpoints.records(fleet, token, "original-corrupt-checkpoint", quiescent=True)
+ failure_roots = checkpoints.records(fleet, token, "original-durable-failure", quiescent=True, family="root")
+ result["originalFailure"] = retained_corruption(corrupt_scene, failure_roots, frozen, receipt, failed, earlier=earlier)
+ before_rescan = fleet.snapshot("failure-scene-before-any-rescan", quiescent=True)
+ indexed = files(before_rescan)
+ remaining = [target for target in targets if (target["node"], target["disk"], target["relativePath"]) not in indexed]
+ require(remaining, "NOT_OBSERVED: no original object responsibility remains for the separate safe rescan")
+ partial = copy.deepcopy(original)
+ missing = {(target["node"], target["disk"], target["relativePath"]) for target in remaining}
+ for node, value in partial.items():
+ value["files"] = [row for row in value["files"] if (node, row["disk"], row["path"]) not in missing]
+ fleet.record("partial-original-repair-and-control-proof", validate_repaired_physical(
+ partial, before_rescan, targets[0], repair_targets=targets))
+ fleet.start()
+ admissions = []
+ rescanned = run_task(probe, fleet, {"bucket": bucket, "key": ""}, 2, active,
+ option_overrides={"recursive": True}, admissions=admissions)
+ require(len(admissions) == 1 and admissions[0]["clientToken"] != token, "safe rescan reused or relabelled the failed original token")
+ result["separateRescan"] = rescan_outcome(rescanned, bucket, expected,
+ {version_identity(target) for target in remaining}, frozen["bucketIncarnationId"])
+ # Complete independent manifests cover the bounded public diagnostics.
+ # The small-result peer helper intentionally rejects such truncation.
+ for endpoint in tuple(binding["endpoints"])[1:]:
+ deadline = time.monotonic() + 30
+ while True:
+ peer = status(Probe(binding, endpoint), fleet, admissions[0]["path"], admissions[0]["clientToken"], "same-rescan-peer")
+ if all(canonical(peer.get(name)) == canonical(rescanned.get(name))
+ for name in ("summary", "settings", "outcome", "truncated")): break
+ require(time.monotonic() < deadline, "same original rescan token did not converge on an actual peer")
+ time.sleep(.2)
+ fleet.stop()
+ repaired = fleet.snapshot("complete-physical-before-any-GET", quiescent=True)
+ fleet.record("complete-physical-proof", validate_repaired_physical(original, repaired, targets[0], repair_targets=targets))
+ for target in targets: verify_proof(fleet, target, "rescan-full-cohort-bitrot")
+ # New-task repair must not erase or advance the retained corrupt record.
+ fleet.start()
+ repeated = status(probe, fleet, checkpoints.PATH, token, "original-failure-after-separate-rescan")
+ require(canonical(repeated.get("outcome")) == canonical(failed.get("outcome")), "safe rescan rewrote the original failure")
+ for endpoint in binding["endpoints"]:
+ inventory = fleet.record("complete-peer-S3", {"endpoint": endpoint, **Probe(binding, endpoint).inventory(
+ bucket, unversioned_current_tags=fixture == "DB")})
+ fleet.record("complete-peer-ACK-and-content-proof", {**verify_seed(expected, inventory), **compare_inventories(baseline, inventory)})
+ fleet.stop()
+ require(physical_content(repaired) == physical_content(fleet.snapshot("physical-after-GET", quiescent=True)),
+ "business reads repaired data that the independent rescan had not proved")
+ retained_checkpoint = checkpoints.records(fleet, token, "retained-corrupt-checkpoint-after-rescan", quiescent=True)
+ retained_roots = checkpoints.records(fleet, token, "retained-original-failure-after-rescan", quiescent=True, family="root")
+ retained_corruption(retained_checkpoint, retained_roots, frozen, receipt, repeated, earlier=earlier)
+ require(canonical(exact_records(retained_checkpoint, token, "checkpoint")) == canonical(exact_records(corrupt_scene, token, "checkpoint"))
+ and canonical(exact_records(retained_roots, token, "root")) == canonical(exact_records(failure_roots, token, "root")),
+ "separate rescan or business reads changed the original corrupt record or durable failure bytes")
+ fleet.faults.clear() # Complete physical/S3 proof precedes discarding shard rescue registrations.
+ result.update(verdict="PASS", originalFailureReclassified=False, completePhysicalAndS3ManifestsVerified=True)
+ except (Exception, KeyboardInterrupt) as exc:
+ result.update(verdict="FAIL", failure={"type": type(exc).__name__, "message": str(exc)})
+ try:
+ fleet.stop()
+ fleet.snapshot("original-failure-scene", quiescent=True)
+ except Exception as error: result["failureEvidenceError"] = str(error)
+ finally:
+ clean = fleet.cleanup(preserve_namespaces=pending is not None)
+ result["cleanup"] = "PASS" if clean and pending is None else "FAIL"
+ if pending is not None: result["unresolvedMutation"] = pending
+ fleet.record("checkpoint-branch-result", result)
+ return result
+
+
+def collect_native_persistence(args, candidate):
+ """Execute and re-verify the exact hosted ECStore subset when supplied.
+
+ The six directory-fsync regressions are an independent native subset. They
+ share the candidate identity with the manager evidence, but they never
+ mutate the fleet and never certify the canonical F07 case. Missing native
+ artifacts keep the diagnostic prerequisite visible without fabricating a
+ PASS; a retained native failure is written as evidence for later triage.
+ """
+ raw = getattr(args, "native_directory", None) or os.environ.get("VALIDATION_HEAL_NATIVE_DIRECTORY")
+ if not raw:
+ return None
+ root = Path(raw)
+ tools, evidence = root / "native-tools", root / "native-evidence"
+ out = args.evidence / "native-persistence"
+ args.evidence.mkdir(parents=True, exist_ok=True)
+ try:
+ manifest, _ = persistence_native.binding(tools, evidence)
+ require(manifest["candidateCommit"] == candidate["source_sha"],
+ "F07 native and fleet candidates differ")
+ persistence_native.run(tools, evidence, out, args.testing_commit)
+ proof = persistence_native.verify(tools, evidence, out, args.testing_commit)
+ require(proof.get("candidateCommit") == candidate["source_sha"],
+ "F07 verified native proof changed the selected candidate")
+ save(args.evidence / "independent-native-persistence.json", proof)
+ return proof
+ except (Exception, KeyboardInterrupt) as exc:
+ failure = {"complete": False, "canonicalCasePassClaimed": False,
+ "fleetPassClaimed": False, "nativeDirectory": str(root.resolve()),
+ "failure": {"type": type(exc).__name__, "message": str(exc)}}
+ save(args.evidence / "independent-native-persistence-failure.json", failure)
+ return failure
+
+
+def native_branch_result(topology, fixture, manager_proof, persistence_proof):
+ complete = isinstance(persistence_proof, dict) and persistence_proof.get("complete") is True
+ attempted = persistence_proof is not None
+ if complete:
+ require(manager_proof.get("candidateCommit") == persistence_proof.get("candidateCommit")
+ and manager_proof.get("candidateCommit") is not None,
+ "F07 manager and directory-fsync proofs mix candidates")
+ result = {"requestedTopology": topology, "requestedFixture": fixture, "topology": None,
+ "fixture": None, "branch": "native-persist-fence", "verdict": "PREREQUISITE_PASS",
+ "cleanup": "PASS", "nativeProof": manager_proof, "nativePersistenceProof": persistence_proof,
+ "nativeExecutionAttempted": attempted, "fleetBranchExecuted": False,
+ "canonicalCasePassClaimed": False,
+ "evidenceScope": "exact-candidate checkpoint/MRF publication plus ECStore directory-fsync native subset",
+ "individualFsyncHooksClaimed": False, "physicalPowerLossClaimed": False,
+ "requiredNativeCoverageComplete": complete,
+ "requiredNativeCoverageScope": "source native-persist-fence: manager commit interruptions plus ECStore directory-fsync regressions",
+ "remainingNativeCoverage": None if complete else
+ "provide and pass the exact candidate-bound ECStore directory-fsync artifact"}
+ if attempted and not complete:
+ result["nativeFailureRetained"] = True
+ return result
+
+
+def main():
+ def interrupted(signum, frame): raise KeyboardInterrupt(f"F07 interrupted by signal {signum}")
+ signal.signal(signal.SIGTERM, interrupted)
+ parser = argparse.ArgumentParser(description=__doc__)
+ for name in ("candidate", "rc-bin", "evidence", "tool-directory"): parser.add_argument("--" + name, type=Path, required=True)
+ parser.add_argument("--testing-commit", required=True)
+ parser.add_argument("--binary-sha", required=True)
+ parser.add_argument("--native-directory", type=Path, default=os.environ.get("VALIDATION_HEAL_NATIVE_DIRECTORY"))
+ parser.add_argument("--case-id", choices=(CASE_ID,), required=True)
+ args = parser.parse_args()
+ require(select(CASE_ID)[0]["variants"] == list(BRANCHES), "F07 recipe branches changed")
+ candidate = json.loads(args.candidate.read_text())
+ require(candidate["source_sha"] in {checkpoints.REFERENCE["candidateCommit"], "1aa9be87d18cf941e5020f19e8622c26ba4b10ac"},
+ "F07 checkpoint failure contract requires an exact reviewed candidate with identical checkpoint sources")
+ native = verify_manager(os.environ.get("VALIDATION_HEAL_MANAGER_DIRECTORY", ""), candidate, CASE_ID)
+ save(args.evidence / "independent-native-persistence-prerequisites.json", native)
+ native_persistence = collect_native_persistence(args, candidate)
+ require(re.fullmatch(r"[0-9a-f]{40}", args.testing_commit) and re.fullmatch(r"[0-9a-f]{64}", args.binary_sha)
+ and re.fullmatch(r"[1-9][0-9]*", os.environ.get("GITHUB_RUN_ID", "")), "F07 requires immutable workflow and binary identities")
+ nodes = os.environ.get("RUSTFS_NODES", "vm000 vm001 vm002 vm003").split()
+ require(len(nodes) == len(set(nodes)) == 4 and all(re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", node) for node in nodes),
+ "F07 requires the four distinct current auto-testing Heal nodes")
+ results = []
+ try:
+ for topology, selected in (("H1", nodes[:1]), ("H2", nodes)):
+ for fixture in ("DB", "DV"):
+ for branch in BRANCHES:
+ if branch in IMPLEMENTED or branch == "journal-write-failure":
+ result = run_checkpoint_fault(args, topology, selected, fixture, branch, candidate)
+ elif branch == "mrf-checksum":
+ result = run_mrf_checksum(args, topology, selected, fixture, candidate)
+ elif branch == "native-persist-fence":
+ result = native_branch_result(topology, fixture, native, native_persistence)
+ else:
+ result = {"topology": topology, "fixture": fixture, "branch": branch, "verdict": "BLOCKED", "cleanup": "PASS",
+ "reason": BLOCKERS[branch], "fleetBranchExecuted": False, "canonicalCasePassClaimed": False}
+ results.append(result)
+ if result["cleanup"] != "PASS": return 1
+ finally:
+ def branch_passed(row):
+ if row["cleanup"] != "PASS":
+ return False
+ if row["branch"] in BLOCKERS:
+ return row["verdict"] == "BLOCKED"
+ if row["branch"] == "native-persist-fence":
+ return row["verdict"] == "PREREQUISITE_PASS" and (
+ not row.get("nativeExecutionAttempted") or row.get("requiredNativeCoverageComplete") is True)
+ return row["verdict"] == "PASS"
+ save(args.evidence / "healthy-summary.json", {"caseId": CASE_ID, "results": results, "complete": False,
+ "cleanup": "PASS" if results and all(row["cleanup"] == "PASS" for row in results) else "FAIL",
+ "diagnosticBranchesPassed": len(results) == 24 and all(branch_passed(row) for row in results),
+ "nativePersistenceProof": native_persistence, "canonicalCasePassClaimed": False,
+ "reason": ("independent complete live audit remains pending"
+ if native_persistence and native_persistence.get("complete") is True else
+ "exact candidate-bound ECStore native evidence and independent complete live audit remain pending")})
+ return 1
+
+
+if __name__ == "__main__": sys.exit(main())
diff --git a/scripts/validation_heal/persistence_mrf.py b/scripts/validation_heal/persistence_mrf.py
new file mode 100644
index 00000000..e4d0a982
--- /dev/null
+++ b/scripts/validation_heal/persistence_mrf.py
@@ -0,0 +1,288 @@
+"""F07: corrupt actual committed MRF CRCs and retain original responsibility."""
+
+import base64
+from datetime import datetime, timedelta, timezone
+import hashlib
+import os
+import secrets
+import time
+
+try:
+ from . import journal_fault as journal, mrf_records as records, mrf_replay as mrf
+ from .heal_probe import Probe, compare_inventories, require
+ from .heal_workload import Recorder, seed, verify_seed
+ from .healthy import Fleet, physical_content, validate_topology
+ from .inspector import verify as verify_inspector
+ from .scanner_repair import JournalWindow
+ from .shards import map_target, run_task, verify_cohort
+ from .stop import idle_fleet
+ from .versions import validate_version_receipts, verify_quorum_contract
+except ImportError:
+ import journal_fault as journal, mrf_records as records, mrf_replay as mrf
+ from heal_probe import Probe, compare_inventories, require
+ from heal_workload import Recorder, seed, verify_seed
+ from healthy import Fleet, physical_content, validate_topology
+ from inspector import verify as verify_inspector
+ from scanner_repair import JournalWindow
+ from shards import map_target, run_task, verify_cohort
+ from stop import idle_fleet
+ from versions import validate_version_receipts, verify_quorum_contract
+
+
+CASE_ID = "heal.checkpoint-and-mrf-corruption"
+SIZE = 5 * 1024**2 + 123
+
+
+def indexed(observed):
+ require(observed.get("readOnly") is True and observed.get("stable") is True,
+ "F07 MRF evidence is not an independent stable read")
+ rows = observed.get("records")
+ expected = {(disk, path) for disk in range(4) for path in records.FILES}
+ require(isinstance(rows, list) and len(rows) == len(expected), "F07 MRF census is incomplete")
+ result = {}
+ for row in rows:
+ pair = row.get("disk"), row.get("relativePath")
+ require(type(pair[0]) is int and pair in expected and pair not in result and type(row.get("present")) is bool,
+ "F07 MRF census duplicated or escaped its exact physical scope")
+ if row["present"]:
+ raw = base64.b64decode(row["rawBase64"], validate=True)
+ require(journal.entry(raw) == {key: row.get(key) for key in ("size", "sha256")}
+ and len(raw) <= records.FILES[pair[1]], "F07 MRF evidence differs from original bytes")
+ result[pair] = raw
+ else:
+ require(not any(key in row for key in ("size", "sha256", "rawBase64")), "F07 absent MRF file has invented bytes")
+ result[pair] = None
+ return result
+
+
+def corruption_targets(observed, bucket):
+ """Cover every committed replay source, including readable older slots."""
+ selected = records.checkpoint(observed)
+ require(selected.get("present") is True, "F07 MRF owner has no committed replay source")
+ latest_identity = journal.active_mrf_identity(selected, bucket)
+ entries, targets = indexed(observed), []
+ for disk in range(4):
+ for slot in (0, 1):
+ manifest = entries[disk, f".rustfs.sys/.heal-mrf-commit.{slot}.bin"]
+ if manifest is None:
+ continue
+ path = f".rustfs.sys/.heal-mrf-snapshot.{slot}.bin"
+ raw = entries[disk, path]
+ binding = records.commit(manifest, raw)
+ exact = records.checkpoint(observed, replica=(disk, slot))
+ identity = journal.active_mrf_identity(exact, bucket)
+ require(identity["owner"] == latest_identity["owner"]
+ and identity["bucketIncarnationId"] == latest_identity["bucketIncarnationId"],
+ "F07 MRF replay sources mixed process owners or bucket incarnations")
+ targets.append({"disk": disk, "relativePath": path, **binding,
+ "mrfIdentity": identity, "before": journal.entry(raw)})
+ require(targets, "F07 MRF corruption has no bound replay source")
+ return targets
+
+
+def retained_corruption(original, current, receipts, bucket):
+ require(set(original) == set(current) and len(original) in {1, 4}, "F07 MRF evidence lost a bound physical node")
+ expected, changed = {node: indexed(value) for node, value in original.items()}, set()
+ require(receipts, "F07 MRF corruption has no actual original payload mutation")
+ for receipt in receipts:
+ node, disk, path = receipt.get("node"), receipt.get("disk"), receipt.get("relativePath")
+ identity = node, disk, path
+ require(identity not in changed and node in expected and type(disk) is int and (disk, path) in expected[node]
+ and path in {f".rustfs.sys/.heal-mrf-snapshot.{slot}.bin" for slot in (0, 1)},
+ "F07 MRF mutation changed scope or duplicated a payload owner")
+ raw = expected[node][disk, path]
+ require(raw is not None and records.decode_journal(raw) and receipt.get("family") == "mrf"
+ and receipt.get("taskId") is None and receipt.get("kind") == "checksum" and receipt.get("applied") is True
+ and receipt.get("originalBackupRetained") is True and receipt.get("productRepairClaimed") is False
+ and journal.same_json(receipt.get("before"), journal.entry(raw)), "F07 MRF mutation lost its original payload binding")
+ corrupt = journal.transform(raw, receipt, {"bucket": bucket}, records)
+ require(journal.same_json(receipt.get("injected"), journal.entry(corrupt)), "F07 MRF corruption changed bytes outside the actual CRC")
+ expected[node][disk, path] = corrupt
+ changed.add(identity)
+ require(all(expected[node] == indexed(value) for node, value in current.items()),
+ "F07 product deleted, rewrote or rebound original corrupt MRF records, manifests or lifecycle responsibility")
+ return {"originalCorruptRecordsRetained": True, "commitsAndLifecycleUnchanged": True,
+ "corruptedPayloads": len(changed), "originalResponsibilityRetired": False,
+ "safeRescanIsOriginalMrfCompletion": False, "backupRescueUsed": False}
+
+
+def diagnostics(events, mutated_nodes):
+ found = {}
+ for node in mutated_nodes:
+ matching = [event for event in events.get(node, [])
+ if event.get("target") == "rustfs::heal::mrf"
+ and event.get("message") == "MRF committed replay checkpoint could not be inspected"
+ and isinstance(event.get("error"), str)
+ and any(word in event["error"].lower() for word in ("corrupt", "invalid", "integrity"))]
+ require(matching, "NOT_OBSERVED: original MRF corruption has no candidate diagnostic on " + node)
+ found[node] = matching
+ require(found, "F07 MRF diagnostic has no bound mutated process")
+ return found
+
+
+def observe(fleet, label):
+ return fleet.record(label, {node: fleet.remote(node, "mrf-records", quiescent=True) for node in fleet.nodes})
+
+
+def validate_member_denial(value, disk):
+ control = value.get("systemMetadataControl") or {}
+ require(value.get("scope") == "bucket" and type(value.get("disk")) is int and value["disk"] == disk
+ and value.get("readProbe", {}).get("errno") == 13 and value.get("errno") == 13
+ and value.get("dacBypassRemoved") is True and value.get("systemMetadataAccessible") is True
+ and control.get("sameServiceIdentity") is True
+ and all(type(control.get(kind, {}).get("errno")) is int and control[kind]["errno"] == 0
+ for kind in ("read", "write")),
+ "F07 needs exact business read/write denial with accessible candidate system metadata")
+
+
+def run(args, topology, nodes, fixture, candidate):
+ require(topology in {"H1", "H2"} and fixture in {"DB", "DV"}, "F07 MRF source fixture changed")
+ directory = args.evidence / fixture / topology / "mrf-checksum"
+ directory.mkdir(parents=True)
+ run_id = f"gh-{os.environ['GITHUB_RUN_ID']}-{os.environ.get('GITHUB_RUN_ATTEMPT', '1')}-{topology.lower()}"
+ fleet = Fleet(nodes, directory, run_id, args.tool_directory, runtime_profile="mrf-crash")
+ bucket = "rv-heal-" + hashlib.sha256(run_id.encode()).hexdigest()[:16]
+ fleet.snapshot_buckets = [bucket]
+ access, secret = secrets.token_hex(16), secrets.token_hex(24)
+ os.environ.update(HEAL_ACCESS_KEY=access, HEAL_SECRET_KEY=secret)
+ binding = {"schemaVersion": 1, "runId": run_id, "jumpHostOnly": True, "exclusiveHealService": True,
+ "leaseExpiresAt": (datetime.now(timezone.utc) + timedelta(minutes=45)).isoformat(), "bucketPrefix": bucket,
+ "endpoints": {f"node{index}": f"http://rustfs-node{index}:9000" for index in range(1, len(nodes) + 1)},
+ "defaultEndpoint": "node1", "candidateCommit": candidate["source_sha"], "harnessCommit": args.testing_commit,
+ "candidatePackageSha256": candidate["package_sha256"], "candidateBinarySha256": args.binary_sha,
+ "rcBinarySha256": hashlib.sha256(args.rc_bin.read_bytes()).hexdigest(),
+ "maxInventoryObjects": 128, "maxInventoryBytes": 256 * 1024**2}
+ fleet.record("binding", binding)
+ tool = fleet.record("candidate-bound-inspector", verify_inspector(args.tool_directory, candidate))
+ verify_quorum_contract(tool, candidate)
+ fleet.record("candidate-MRF-source-contract", records.verify_reference(candidate))
+ probe, active, pending = Probe(binding), [], None
+ result = {"topology": topology, "fixture": fixture, "branch": "mrf-checksum", "verdict": "RUNNING",
+ "cleanup": "PENDING", "canonicalCasePassClaimed": False}
+ try:
+ fleet.deploy(args.binary_sha, bucket, access, secret)
+ fleet.record("actual-topology", validate_topology(fleet.snapshot("deployed-topology"), topology))
+ recorder = Recorder(probe, directory / "fixture")
+ pending = {"operation": "seed-original-MRF-fixture"}
+ expected = seed(recorder, bucket, fixture, 3, [SIZE])
+ verify_seed(expected, probe.inventory(bucket, unversioned_current_tags=fixture == "DB"))
+ pending = None
+ initial = run_task(probe, fleet, {"bucket": bucket, "key": ""}, 2, active, option_overrides={"recursive": True})
+ incarnation = validate_version_receipts(initial, bucket, expected, 2)["bucketIncarnationId"]
+ idle_fleet(fleet, binding)
+ fleet.stop()
+ before = fleet.snapshot("healthy-before-partial-write-gate", quiescent=True)
+ entry = next(item for item in expected if not item["deleteMarker"])
+ target = map_target(before, bucket, entry["key"], "data", 1, entry["versionId"], latest_required=False)
+ shard = next(row for row in reversed(target["cohort"]) if row["node"] == nodes[-1])
+ fleet.drive_gates.add(shard["node"])
+ fleet.record("actual-partial-write-gate", fleet.remote(shard["node"], "drive-gate", disk=shard["disk"],
+ mode="permission-denied", scope="bucket"))
+ fleet.start()
+ denial = fleet.record("actual-member-denial", fleet.remote(shard["node"], "drive-gate-probe"))
+ validate_member_denial(denial, shard["disk"])
+ earliest = int(time.time() * 1000) - 5000
+ pending = {"operation": "partial-write-durable-ACKs"}
+ expected, obligations, _ = mrf.acknowledge(recorder, bucket, "offline-put", expected, fixture=fixture)
+ baseline = fleet.record("complete-degraded-ACK-S3", probe.inventory(bucket, unversioned_current_tags=fixture == "DB"))
+ verify_seed(expected, baseline)
+ pending = None
+ deadline = time.monotonic() + mrf.PERSIST_SECONDS
+ while True:
+ try:
+ bindings = mrf.pending_bindings(mrf.read_checkpoints(fleet, nodes, "original-committed-MRF"), obligations, earliest)
+ break
+ except ValueError as exc:
+ if not str(exc).startswith("NOT_OBSERVED:"): raise
+ require(time.monotonic() < deadline, "F07 ACK-bound durable MRF window was not observed")
+ time.sleep(.5)
+ require(all(owner["bucketIncarnationId"] == incarnation for owners in bindings.values() for owner in owners),
+ "F07 MRF ACK responsibility changed the original incarnation")
+ fleet.stop()
+ original = observe(fleet, "original-stopped-MRF-records")
+ stopped = {node: records.checkpoint(value) for node, value in original.items()}
+ bindings = mrf.pending_bindings(stopped, obligations, earliest)
+ fleet.record("original-durable-responsibilities", {"obligations": obligations,
+ "bindings": [{"identity": list(key), "owners": value} for key, value in bindings.items()]})
+ authorities = fleet.snapshot("original-partial-write-physical", quiescent=True)
+ mutation_plan = {node: corruption_targets(observed, bucket) for node, observed in original.items()
+ if any(owner["node"] == node for owners in bindings.values() for owner in owners)}
+ require(mutation_plan, "F07 MRF fault plan has no original responsibility owner")
+ fleet.record("all-original-committed-MRF-corruption-targets", mutation_plan)
+ receipts = []
+ # A healthy replica or older committed slot can still replay. Corrupt
+ # every owned source while retaining all manifests and lifecycle bytes.
+ for node, targets in mutation_plan.items():
+ for target in targets:
+ disk, path = target["disk"], target["relativePath"]
+ params = {"family": "mrf", "taskId": None, "disk": disk, "relativePath": path,
+ "faultId": f"F07-mrf-checksum-{nodes.index(node)}-{disk}-{path[-5]}", "kind": "checksum",
+ "expectedBefore": target["before"], "mrfIdentity": target["mrfIdentity"]}
+ fleet.journal_faults.append((node, params.copy()))
+ receipts.append(fleet.record("actual-original-MRF-CRC-injection", {"node": node,
+ **fleet.remote(node, "journal-apply", **params)}))
+ retained_corruption(original, observe(fleet, "injected-stopped-MRF-records"), receipts, bucket)
+ require({(row.get("node"), row.get("disk"), row.get("relativePath")) for row in receipts} ==
+ {(node, row["disk"], row["relativePath"]) for node, targets in mutation_plan.items() for row in targets},
+ "F07 MRF corruption omitted an original readable replay source")
+ window = JournalWindow(fleet, "F07-corrupt-MRF-replay", event_targets={"rustfs::heal::mrf"})
+ # Only business data is denied; disk initialization can still read
+ # system metadata. Keep the same gate active through startup so MRF
+ # replay cannot repair the target before the readiness observation.
+ gate = fleet.record("stopped-original-business-gate", fleet.remote(shard["node"], "gate-state"))
+ require(gate.get("scope") == "bucket" and gate.get("mode") == "permission-denied"
+ and gate.get("phase") == "active" and type(gate.get("disk")) is int and gate["disk"] == shard["disk"],
+ "F07 restart gate changed its original denied member")
+ fleet.start()
+ denied = fleet.record("restarted-original-member-denial", fleet.remote(shard["node"], "drive-gate-probe"))
+ validate_member_denial(denied, shard["disk"])
+ deadline = time.monotonic() + 60
+ while True:
+ try:
+ result["originalCorruptionDiagnostics"] = diagnostics(window.capture(), {row["node"] for row in receipts})
+ break
+ except ValueError as exc:
+ if not str(exc).startswith("NOT_OBSERVED:"): raise
+ require(time.monotonic() < deadline, str(exc))
+ time.sleep(.5)
+ fleet.stop()
+ result["originalResponsibilityBeforeRescan"] = retained_corruption(
+ original, observe(fleet, "retained-corrupt-MRF-before-rescan"), receipts, bucket)
+ require(physical_content(authorities) == physical_content(fleet.snapshot("before-separate-rescan", quiescent=True)),
+ "F07 corrupt MRF replay silently changed original object responsibility")
+ fleet.record("release-original-member-gate", fleet.remote(shard["node"], "drive-gate", mode="clear"))
+ fleet.drive_gates.remove(shard["node"])
+ fleet.start()
+ rescanned = run_task(probe, fleet, {"bucket": bucket, "key": ""}, 2, active, option_overrides={"recursive": True})
+ result["separateRescan"] = validate_version_receipts(rescanned, bucket, expected, 2,
+ required_repairs={(item["key"], item["versionId"]) for item in obligations},
+ metadata_repairs={item["key"] for item in obligations})
+ require(result["separateRescan"]["bucketIncarnationId"] == incarnation, "F07 rescan rebound the original incarnation")
+ idle_fleet(fleet, binding)
+ fleet.stop()
+ repaired = fleet.snapshot("complete-rescan-physical-before-business-reads", quiescent=True)
+ cohorts = mrf.replica_census(repaired, bucket, expected, authorities, topology=topology)
+ result["fullShardIntegrity"] = [verify_cohort(fleet, cohort, "F07-full-shard-proof") for cohort in cohorts]
+ retained_corruption(original, observe(fleet, "retained-corrupt-MRF-after-rescan"), receipts, bucket)
+ fleet.start()
+ for endpoint in binding["endpoints"]:
+ inventory = fleet.record("all-peer-S3", {"endpoint": endpoint,
+ **Probe(binding, endpoint).inventory(bucket, unversioned_current_tags=fixture == "DB")})
+ verify_seed(expected, inventory); compare_inventories(baseline, inventory)
+ fleet.stop()
+ require(physical_content(repaired) == physical_content(fleet.snapshot("after-business-reads", quiescent=True)),
+ "F07 business reads repaired work not proved by the separate rescan")
+ result["originalResponsibilityAfterReads"] = retained_corruption(original, observe(fleet, "retained-corrupt-MRF-after-reads"), receipts, bucket)
+ result.update(verdict="PASS", originalMrfRepairClaimed=False, completePhysicalAndS3ManifestsVerified=True)
+ except (Exception, KeyboardInterrupt) as exc:
+ result.update(verdict="FAIL", failure={"type": type(exc).__name__, "message": str(exc)})
+ try:
+ fleet.stop(); observe(fleet, "original-MRF-failure-scene")
+ fleet.snapshot("original-physical-failure-scene", quiescent=True)
+ except Exception as error: result["failureEvidenceError"] = str(error)
+ finally:
+ clean = fleet.cleanup(preserve_namespaces=pending is not None)
+ result["cleanup"] = "PASS" if clean and pending is None else "FAIL"
+ if pending is not None: result["unresolvedMutation"] = pending
+ fleet.record("MRF-corruption-branch-result", result)
+ return result
diff --git a/scripts/validation_heal/persistence_native.py b/scripts/validation_heal/persistence_native.py
new file mode 100644
index 00000000..9095da55
--- /dev/null
+++ b/scripts/validation_heal/persistence_native.py
@@ -0,0 +1,153 @@
+#!/usr/bin/env python3
+"""Run existing F07 directory-fsync regressions from the verified candidate binary."""
+
+import argparse
+import json
+import os
+import re
+from pathlib import Path
+import subprocess
+import time
+
+try:
+ from . import native_regressions as native
+except ImportError:
+ import native_regressions as native
+
+
+PREFIX = "disk::local::test::"
+REGRESSIONS = {
+ "cas-update-directory-sync": (
+ "conditional_file_update_dir_fsync_failure_restores_previous_bytes",
+ "post-rename directory fsync failure is commit-ambiguous and restores the previous owner bytes"),
+ "cas-create-directory-sync": (
+ "conditional_file_update_dir_fsync_failure_removes_new_file_without_anchor",
+ "post-rename directory fsync failure removes an uncommitted first owner"),
+ "cas-delete-directory-sync": (
+ "conditional_file_delete_dir_fsync_failure_restores_previous_bytes",
+ "post-delete directory fsync failure restores the previous owner bytes"),
+ "mrf-manifest-directory-sync": (
+ "conditional_mrf_manifest_dir_fsync_failure_keeps_recovery_anchors",
+ "actual system-metadata directory fsync is required even in relaxed mode; failed replacement and first creation retain recovery anchors"),
+ "mrf-manifest-storage-full": (
+ "conditional_mrf_manifest_storage_full_keeps_recovery_anchors",
+ "StorageFull at the actual manifest directory fsync preserves the previous manifest and legacy journal"),
+ "mrf-delete-storage-full": (
+ "conditional_mrf_manifest_storage_full_delete_keeps_recovery_anchors",
+ "StorageFull at cleanup directory fsync restores the committed manifest and preserves the legacy journal"),
+}
+LIMITS = {
+ "directoryFsyncFailureTestScope": True,
+ "individualFileFsyncCovered": False,
+ "physicalPowerLossClaimed": False,
+ "requiredNativeCoverageComplete": False,
+ "canonicalCasePassClaimed": False,
+ "fleetPassClaimed": False,
+ "emittedBarrierLogsClaimed": False,
+}
+
+
+def binding(directory, evidence):
+ manifest, binary, _ = native.verify_manifest(directory)
+ native.require(manifest["candidateCommit"] == native.INCARNATION_CANDIDATE,
+ "F07 directory fsync tests are reviewed only for the exact immutable candidate")
+ native.verify_evidence(directory, evidence, {"source_sha": manifest["candidateCommit"]},
+ manifest["runId"], manifest["runAttempt"], manifest["testingCommit"])
+ listing = (directory / "test-inventory.log").read_text()
+ for suffix, _ in REGRESSIONS.values(): native.exact_listing(listing, PREFIX + suffix)
+ return manifest, binary
+
+
+def identity(manifest, testing_commit):
+ native.require(isinstance(testing_commit, str) and re.fullmatch(r"[0-9a-f]{40}", testing_commit),
+ "F07 collector requires an exact independent harness commit")
+ return {"candidateCommit": manifest["candidateCommit"], "candidateSourceTree": manifest["candidateSourceTree"],
+ "candidateCargoLockSha256": manifest["candidateCargoLockSha256"],
+ "binarySha256": manifest["binarySha256"], "runId": manifest["runId"],
+ "runAttempt": manifest["runAttempt"], "nativeTestingCommit": manifest["testingCommit"],
+ "collectorCommit": testing_commit,
+ "collectorSha256": native.sha(Path(__file__)), **LIMITS}
+
+
+def run(directory, native_evidence, out, testing_commit):
+ manifest, binary = binding(directory, native_evidence)
+ proof_identity = identity(manifest, testing_commit)
+ native.require(not out.exists(), "F07 native evidence must be fresh")
+ out.mkdir(parents=True)
+ listing = subprocess.run([str(binary), "--list"], capture_output=True, text=True, timeout=60)
+ (out / "runtime-inventory.log").write_text(listing.stdout + listing.stderr)
+ native.require(listing.returncode == 0, "F07 native runtime inventory failed")
+ rows = []
+ for key, (suffix, assertion) in REGRESSIONS.items():
+ name = PREFIX + suffix
+ native.exact_listing(listing.stdout, name)
+ native.require(not binary.is_symlink() and native.sha(binary) == manifest["binarySha256"],
+ "F07 native executable changed before exact execution")
+ command = native.exact_command(binary, name, manifest["candidateCommit"])
+ row = {**proof_identity, "regression": key, "test": name, "sourceAssertion": assertion,
+ "command": command, "runtimeEnvironment": manifest["runtimeEnvironment"]}
+ started = time.monotonic_ns()
+ try:
+ child = subprocess.run(command, capture_output=True, text=True, timeout=120,
+ env={**os.environ, **manifest["runtimeEnvironment"]})
+ output = child.stdout + child.stderr
+ (out / (key + ".log")).write_text(output)
+ row["exitCode"] = child.returncode
+ native.test_result(output, name, child.returncode)
+ row["verdict"] = "PASS"
+ except subprocess.TimeoutExpired as error:
+ parts = [part.decode(errors="replace") if isinstance(part, bytes) else part or ""
+ for part in (error.stdout, error.stderr)]
+ (out / (key + ".log")).write_text("".join(parts))
+ row.update(verdict="FAIL", reason="fixed 120-second F07 native deadline exceeded")
+ except (OSError, ValueError) as error:
+ if not (out / (key + ".log")).exists(): (out / (key + ".log")).write_text(str(error))
+ row.update(verdict="FAIL", reason=str(error))
+ row["elapsedNanoseconds"] = time.monotonic_ns() - started
+ native.save(out / (key + ".json"), row)
+ rows.append(row)
+ complete = all(row["verdict"] == "PASS" for row in rows)
+ native.save(out / "persistence-native-summary.json", {**proof_identity, "complete": complete, "results": rows})
+ native.require(complete, "F07 native fsync failures remain in original evidence")
+
+
+def verify(directory, native_evidence, evidence, testing_commit):
+ manifest, binary = binding(directory, native_evidence)
+ summary = json.loads((evidence / "persistence-native-summary.json").read_text())
+ rows = summary.get("results", [])
+ expected_identity = identity(manifest, testing_commit)
+ native.require(all(type(summary.get(key)) is type(value) and summary[key] == value
+ for key, value in expected_identity.items()) and summary.get("complete") is True
+ and len(rows) == len(REGRESSIONS) and {row.get("regression") for row in rows} == set(REGRESSIONS),
+ "F07 native summary changed source, scope, limitations or collector identity")
+ listings = [(directory / "test-inventory.log").read_text(), (evidence / "runtime-inventory.log").read_text()]
+ for row in rows:
+ key = row["regression"]
+ suffix, assertion = REGRESSIONS[key]
+ name = PREFIX + suffix
+ command = row.get("command", [])
+ native.require(row == json.loads((evidence / (key + ".json")).read_text()) and row.get("verdict") == "PASS"
+ and all(type(row.get(k)) is type(v) and row[k] == v for k, v in expected_identity.items())
+ and row.get("test") == name and row.get("sourceAssertion") == assertion
+ and len(command) == 5 and Path(command[0]).name == binary.name
+ and command[1:] == native.exact_command(binary, name, manifest["candidateCommit"])[1:]
+ and row.get("runtimeEnvironment") == manifest["runtimeEnvironment"],
+ "F07 native result changed exact test, command, source assertion or binary")
+ for listing in listings: native.exact_listing(listing, name)
+ native.test_result((evidence / (key + ".log")).read_text(), name, row.get("exitCode"))
+ return {**expected_identity, "exactTests": len(rows), "complete": True}
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("command", choices=("run", "verify"))
+ parser.add_argument("--testing-commit", required=True)
+ parser.add_argument("--directory", type=Path, required=True)
+ parser.add_argument("--native-evidence", type=Path, required=True)
+ parser.add_argument("--out", type=Path, required=True)
+ args = parser.parse_args()
+ if args.command == "run": run(args.directory, args.native_evidence, args.out, args.testing_commit)
+ else: print(json.dumps(verify(args.directory, args.native_evidence, args.out, args.testing_commit), indent=2))
+
+
+if __name__ == "__main__": main()
diff --git a/scripts/validation_heal/prepare_heal_meta_inspector.py b/scripts/validation_heal/prepare_heal_meta_inspector.py
new file mode 100644
index 00000000..47b8390c
--- /dev/null
+++ b/scripts/validation_heal/prepare_heal_meta_inspector.py
@@ -0,0 +1,143 @@
+#!/usr/bin/env python3
+"""Prepare (do not silently build) a read-only metadata inspector for one SHA."""
+
+import argparse
+import hashlib
+import json
+from pathlib import Path
+import re
+import subprocess
+
+
+def quorum_contract(source: Path, commit: str) -> dict:
+ """Freeze only recognized candidate rules; unfamiliar source stays unsupported."""
+ paths = ("rustfs/src/error.rs", "crates/ecstore/src/set_disk/metadata.rs")
+ contents = {path: (source / path).read_text() for path in paths}
+ code_only = {path: re.sub(r'"(?:\\.|[^"\\])*"', '""',
+ re.sub(r"/\*.*?\*/|//[^\n]*", "", value, flags=re.S)) for path, value in contents.items()}
+ compact = {path: re.sub(r"\s+", "", value) for path, value in code_only.items()}
+ metadata = compact[paths[1]]
+ signature = "pub(super)fnobject_quorum_from_meta("
+ start = metadata.find(signature)
+ function = ""
+ if start >= 0:
+ opening = metadata.find("{", start)
+ if opening >= 0:
+ depth = 0
+ for index in range(opening, len(metadata)):
+ depth += (metadata[index] == "{") - (metadata[index] == "}")
+ if depth == 0:
+ function = metadata[opening:index + 1]
+ break
+ errors = compact[paths[0]]
+ known_read = ("letdata_blocks=parts_metadata.len()asi32-parity_blocks;" in function
+ and function.endswith("Ok((data_blocks,write_quorum))}"))
+ known_write = known_read and function.endswith("letdata_blocks=parts_metadata.len()asi32-parity_blocks;"
+ "letwrite_quorum=ifdata_blocks==parity_blocks{data_blocks+1}else{data_blocks};Ok((data_blocks,write_quorum))}")
+ known_error = ('StorageError::ErasureReadQuorum|StorageError::InsufficientReadQuorum(_,_)=>{'
+ 'S3ErrorCode::Custom(SLOW_DOWN_READ_CODE.into())}' in errors
+ and 'S3ErrorCode::Custom(custom)if&**custom==SLOW_DOWN_READ_CODE=>Some(StatusCode::SERVICE_UNAVAILABLE)' in errors)
+ code = re.search(r'const\s+SLOW_DOWN_READ_CODE\s*:\s*&str\s*=\s*"([A-Za-z0-9]+)"\s*;', contents[paths[0]])
+ return {"candidateCommit": commit, "recognized": bool(known_read and known_error and code),
+ "readQuorumRule": "decoded-data-blocks" if known_read else None,
+ "writeQuorumRule": "data-plus-one-if-equal-parity" if known_write else None,
+ "unrecoverableGet": {"httpStatus": 503, "code": code[1]} if known_error and code else None,
+ "sourceFiles": {path: hashlib.sha256(value.encode()).hexdigest() for path, value in contents.items()},
+ "extractorSourceSha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest()}
+
+
+def compact_rust(value: str) -> str:
+ value = re.sub(r"/\*.*?\*/|//[^\n]*", "", value, flags=re.S)
+ return re.sub(r"\s+", "", value)
+
+
+def rust_body(value: str, signature: str) -> str:
+ start = value.find(signature)
+ if start < 0:
+ return ""
+ opening = value.find("{", start)
+ if opening < 0:
+ return ""
+ depth = 0
+ for index in range(opening, len(value)):
+ depth += (value[index] == "{") - (value[index] == "}")
+ if depth == 0:
+ return value[opening:index + 1]
+ return ""
+
+
+def size_contract(source: Path, commit: str) -> dict:
+ """Recognize the bounded untransformed PUT policy, preserving source digests."""
+ paths = ("crates/ecstore/src/config/storageclass.rs", "crates/ecstore/src/set_disk/mod.rs",
+ "crates/ecstore/src/set_disk/ops/object.rs", "crates/ecstore/src/erasure/coding/erasure.rs")
+ contents = {path: (source / path).read_text() for path in paths}
+ storage, admission, put, erasure = (compact_rust(contents[path]) for path in paths)
+ default = re.search(r"pubstaticDEFAULT_INLINE_BLOCK:usize=([0-9]+)\*([0-9]+);", storage)
+ should = rust_body(storage, "pubfnshould_inline(")
+ budget = rust_body(storage, "pub(crate)fneffective_inline_block(")
+ shard = rust_body(erasure, "pubfncalc_shard_size(")
+ recognized = bool(default and
+ "constDEFAULT_INLINE_OBJECT_BUDGET:usize=2*DEFAULT_INLINE_BLOCK;" in storage and
+ should == "{ifshard_size<0||data_shards==0{returnfalse;}letshard_size=shard_sizeasusize;"
+ "letinline_block=self.effective_inline_block(data_shards);ifversioned{shard_size<=inline_block/8}"
+ "else{shard_size<=inline_block}}" and
+ budget == "{ifdata_shards==0{return0;}ifself.initialized&&self.inline_block_explicit{self.inline_block}"
+ "else{DEFAULT_INLINE_OBJECT_BUDGET.div_ceil(data_shards).min(DEFAULT_INLINE_BLOCK)}}" and
+ "ifstored_size>=0{returnerasure.shard_file_size(stored_size);}" in rust_body(admission, "fninline_admission_shard_size(") and
+ "letis_inline_buffer=storage_class_config.should_inline(inline_admission_shard_size(&erasure,put_object_size,data.actual_size()),"
+ "erasure.data_shards,opts.versioned,)&&usize::try_from(inline_admission_size).is_ok_and(|size|size<=erasure.block_size);" in put and
+ shard == "{block_size.div_ceil(data_shards)}")
+ return {"candidateCommit": commit, "recognized": recognized, "scope": "known-size untransformed unversioned PUT; current shard codec",
+ "defaultInlineBlock": int(default[1]) * int(default[2]) if default else None,
+ "defaultObjectBudgetFactor": 2, "inlineEnvironment": "RUSTFS_STORAGE_CLASS_INLINE_BLOCK",
+ "inlineRule": "ceil(size/dataBlocks)<=effectiveInlineBlock AND size<=actualECBlock",
+ "effectiveDefaultRule": "min(ceil(2*defaultInlineBlock/dataBlocks),defaultInlineBlock)",
+ "sourceFiles": {path: hashlib.sha256(value.encode()).hexdigest() for path, value in contents.items()},
+ "extractorSourceSha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest()}
+
+
+def prepare(source: Path, commit: str, output: Path) -> None:
+ if not re.fullmatch(r"[0-9a-f]{40}", commit):
+ raise ValueError("full candidate commit required")
+ source = source.resolve(strict=True)
+ head = subprocess.run(["git", "-C", str(source), "rev-parse", "HEAD"], check=True,
+ text=True, capture_output=True).stdout.strip()
+ dirty = subprocess.run(["git", "-C", str(source), "status", "--porcelain", "--untracked-files=no"],
+ check=True, text=True, capture_output=True).stdout
+ if head != commit or dirty.strip():
+ raise ValueError("inspector requires an unmodified checkout of the exact candidate SHA")
+ crate = source / "crates/filemeta"
+ if not (crate / "Cargo.toml").is_file():
+ raise ValueError("candidate has no rustfs-filemeta crate")
+ output.mkdir(parents=True, exist_ok=False)
+ (output / "src").mkdir()
+ helper = Path(__file__).resolve().parent / "tools/heal_meta_inspect.rs"
+ text = helper.read_text()
+ (output / "src/main.rs").write_text(text)
+ manifest = ('[package]\nname = "heal-meta-inspect"\nversion = "0.1.0"\nedition = "2024"\n'
+ '[workspace]\n[dependencies]\nserde_json = "1"\n'
+ 'rustfs-filemeta = { path = ' + json.dumps(str(crate)) + ' }\n'
+ 'rustfs-utils = { path = ' + json.dumps(str(source / "crates/utils"))
+ + ', default-features = false, features = ["hash"] }\n')
+ (output / "Cargo.toml").write_text(manifest)
+ (output / "source.json").write_text(json.dumps({"candidateCommit": head,
+ "candidateLockSha256": hashlib.sha256((source / "Cargo.lock").read_bytes()).hexdigest(),
+ "helperSourceSha256": hashlib.sha256(text.encode()).hexdigest(),
+ "quorumContract": quorum_contract(source, head),
+ "sizeContract": size_contract(source, head),
+ "role": "read-only decoder; not the RustFS service under test",
+ "built": False}, indent=2) + "\n")
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--source", type=Path, required=True)
+ parser.add_argument("--commit", required=True)
+ parser.add_argument("--out", type=Path, required=True)
+ args = parser.parse_args()
+ prepare(args.source, args.commit, args.out)
+ print(f"Prepared {args.out}; build on an approved host, then record its Cargo.lock and binary SHA-256.")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/validation_heal/prepare_heal_shared_fixture.py b/scripts/validation_heal/prepare_heal_shared_fixture.py
new file mode 100644
index 00000000..e5a18726
--- /dev/null
+++ b/scripts/validation_heal/prepare_heal_shared_fixture.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+"""Prepare the bounded C10 metadata encoder against one unmodified candidate."""
+
+import argparse
+import hashlib
+import json
+from pathlib import Path
+
+try:
+ from .prepare_heal_meta_inspector import prepare
+except ImportError:
+ from prepare_heal_meta_inspector import prepare
+
+
+ROLE = 'controlled C10 shared-reference metadata encoder; not service under test'
+
+
+def prepare_shared(source, commit, output):
+ prepare(source, commit, output)
+ helper = Path(__file__).with_name('tools') / 'heal_shared_version_fixture.rs'
+ (output / 'src/main.rs').write_bytes(helper.read_bytes())
+ manifest = output / 'Cargo.toml'
+ manifest.write_text(manifest.read_text().replace('name = "heal-meta-inspect"', 'name = "heal-shared-fixture"'))
+ with manifest.open('a') as stream:
+ stream.write('[dev-dependencies]\ntime = "0.3"\n')
+ path = output / 'source.json'
+ proof = json.loads(path.read_text())
+ proof.update(role=ROLE, helperSourceSha256=hashlib.sha256(helper.read_bytes()).hexdigest(),
+ filesystemMutationPerformed=False, physicalByteEqualityClaimed=False)
+ path.write_text(json.dumps(proof, indent=2) + '\n')
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--source', type=Path, required=True)
+ parser.add_argument('--commit', required=True)
+ parser.add_argument('--out', type=Path, required=True)
+ args = parser.parse_args()
+ prepare_shared(args.source, args.commit, args.out)
diff --git a/scripts/validation_heal/queue_pressure.py b/scripts/validation_heal/queue_pressure.py
new file mode 100644
index 00000000..71d7a8b9
--- /dev/null
+++ b/scripts/validation_heal/queue_pressure.py
@@ -0,0 +1,476 @@
+#!/usr/bin/env python3
+"""I04: bounded multi-bucket pressure and durable responsibility through Full."""
+
+from __future__ import annotations
+
+from datetime import datetime, timedelta, timezone
+import hashlib
+import json
+import os
+import secrets
+import sys
+import time
+from urllib.parse import quote
+import xml.etree.ElementTree as ET
+
+try:
+ from .heal_probe import Probe, compare_inventories, require
+ from .heal_workload import Recorder, seed, verify_seed
+ from .healthy import BASE_OPTIONS, Fleet, physical_content, validate_topology
+ from .fleet_node import PROCESS_FIELDS, runtime_config
+ from .inspector import verify as verify_inspector
+ from .matrix import select
+ from .cursor import SIZE, map_cohorts, validate_missing_batch, verify_part_manifest, main as generic_main
+ from .shards import map_target, validate_repaired_physical, verify_cohort
+ from .heal_fault import bounded_fault_id
+ from .stop import status, idle_fleet
+ from .scanner_repair import JournalWindow, event_time
+ from .mrf_records import verify_reference, REFERENCE as MRF_REFERENCE
+ from .versions import public_version_id
+ from .mrf_replay import (acknowledge, identity, pending_bindings, read_checkpoints, retirement,
+ source_tasks, verified_receipt)
+except ImportError:
+ from heal_probe import Probe, compare_inventories, require
+ from heal_workload import Recorder, seed, verify_seed
+ from healthy import BASE_OPTIONS, Fleet, physical_content, validate_topology
+ from fleet_node import PROCESS_FIELDS, runtime_config
+ from inspector import verify as verify_inspector
+ from matrix import select
+ from cursor import SIZE, map_cohorts, validate_missing_batch, verify_part_manifest, main as generic_main
+ from shards import map_target, validate_repaired_physical, verify_cohort
+ from heal_fault import bounded_fault_id
+ from stop import status, idle_fleet
+ from scanner_repair import JournalWindow, event_time
+ from mrf_records import verify_reference, REFERENCE as MRF_REFERENCE
+ from versions import public_version_id
+ from mrf_replay import (acknowledge, identity, pending_bindings, read_checkpoints, retirement,
+ source_tasks, verified_receipt)
+
+
+CASE_ID = "heal.queue-pressure-and-admission"
+BRANCHES = ("full-queue", "dropped-admission", "merged-duplicates", "cross-bucket-fairness")
+COUNTS = (769, 129, 129, 129)
+CAPACITY = 2
+MAX_ADMIN_REQUESTS = 24
+PRESSURE_SECONDS = 45
+DRAIN_SECONDS = 600
+RESOURCE_BOUNDS = {"rssBytes": 3 * 1024**3, "fileDescriptors": 8192, "threads": 2048}
+REFERENCE = {"candidateCommit": "072b57b37559fbe626aeda8abf61617fdcbdd926",
+ "sources": {"crates/heal/src/heal/manager.rs":
+ "ba9289942fbbe8ce2632686ae86b9d1be0ae9923420e3b091d67148b4ee02094",
+ "crates/heal/src/heal/mrf_queue.rs": MRF_REFERENCE["sourceFiles"]["crates/heal/src/heal/mrf_queue.rs"]},
+ "adminPriority": "High", "partialWritePriority": "Normal", "queueCapacity": CAPACITY,
+ "maxConcurrentHeals": 1, "maxConcurrentPerSet": 1,
+ "droppedReason": "overlapping_paths", "mergedRule": "same scope returns the existing token",
+ "fullRule": "HTTP 503 SlowDown; admission=full, reason=none",
+ "fairness": "every admitted owned bucket must finish within the frozen drain budget",
+ "persistentProof": "all exact degraded-write ACKs remain committed during MRF Full; each retires only with its verified source task",
+ "nativeCriticalWindowClaimed": False}
+
+
+def validate_durable_gate(value, run_id, bucket):
+ control = value.get("systemMetadataControl") or {}
+ require(value.get("scope") == "durable-bucket" and type(value.get("disk")) is int and value["disk"] == 0
+ and value.get("dataPath") == f"/data/rustfs1/heal-validation/{run_id}/data/{bucket}"
+ and value.get("systemMetadataAccessible") is True and control.get("sameServiceIdentity") is True
+ and all(type(control.get(label, {}).get("errno")) is int and control[label]["errno"] == 0
+ and type(control[label].get("uid")) is int and control[label]["uid"] == 0
+ and type(control[label].get("gid")) is int and control[label]["gid"] == 0 for label in ("read", "write"))
+ and value.get("sameServiceIdentity") is True and value.get("privateMountNamespace") is True
+ and value.get("dacBypassRemoved") is True and value.get("name") == "EACCES"
+ and value.get("errno") == 13 and value.get("readProbe", {}).get("errno") == 13,
+ "queue pressure needs exact durable-bucket refusal and readable/writable system metadata")
+ return value
+
+
+def rejection(observed, *, admission, reason, code, http):
+ require(observed.get("httpStatus") == http and not observed.get("transportError"),
+ "queue rejection has a wrong status or an unknown transport result")
+ value = observed.get("response")
+ if isinstance(value, str):
+ value = {element.tag.rsplit("}", 1)[-1]: element.text for element in ET.fromstring(value).iter()}
+ require(isinstance(value, dict) and not value.get("clientToken")
+ and (value.get("Code") or value.get("code")) == code
+ and (value.get("Message") or value.get("message")) ==
+ f"heal request not admitted: admission={admission}, reason={reason}",
+ "queue rejection erased its actual admission/reason or invented a task")
+ return {"httpStatus": http, "code": code, "admission": admission, "reason": reason,
+ "independentRepairCount": 0}
+
+
+def queue_snapshot(value, peers):
+ require(value.get("clusterStatusComplete") is True
+ and value.get("coverage", {}).get("expected") == peers
+ and value["coverage"].get("responded") == peers and value["coverage"].get("unknown") == 0,
+ "queue pressure has incomplete actual peer coverage")
+ operations = value.get("healOperations") or {}
+ for name in ("queueLength", "activeTasks", "retryingTasks"):
+ require(type(operations.get(name)) is int and operations[name] >= 0, "queue pressure omitted typed backlog")
+ require(operations["queueLength"] <= peers * CAPACITY and operations["activeTasks"] <= peers,
+ "queue or active work exceeds its frozen capacity")
+ require(value.get("healQueueLength") == operations["queueLength"]
+ and value.get("healActiveTasks") == operations["activeTasks"], "queue counters disagree")
+ for name in ("queuedByPriority", "activeByPriority", "queuedBySource", "activeBySource"):
+ counts = operations.get(name)
+ require(isinstance(counts, dict) and counts and all(type(item) is int and item >= 0 for item in counts.values()),
+ "queue sample omitted actual source/priority counters")
+ require(sum(counts.values()) == operations["queueLength" if name.startswith("queued") else "activeTasks"],
+ "queue source/priority counters hide work")
+ telemetry = operations.get("admission") or {}
+ require(all(type(telemetry.get(name)) is int and telemetry[name] >= 0
+ for name in ("accepted", "full", "dropped", "merged")), "queue sample omitted actual admission telemetry")
+ return operations
+
+
+def resources(samples, baseline):
+ require(set(samples) == set(baseline), "resource sample dropped a current owned node")
+ for node, current in samples.items():
+ require(current.get("readOnly") is True and current.get("clockDomain") == baseline[node].get("clockDomain")
+ and type(current.get("monotonicNs")) is int and current["monotonicNs"] > baseline[node]["monotonicNs"]
+ and all(current.get("identity", {}).get(name) == baseline[node]["identity"].get(name) for name in PROCESS_FIELDS),
+ "resource curve crossed its original process birth or monotonic clock")
+ require(all(type(current.get(name)) is int and 0 <= current[name] <= limit
+ for name, limit in RESOURCE_BOUNDS.items()), "queue pressure exceeded a pre-registered resource ceiling")
+ return {"withinFrozenLimits": True, "nodes": list(samples), "limits": RESOURCE_BOUNDS}
+
+
+def active_window(current, count):
+ outcome = current.get("outcome") or {}
+ processed = outcome.get("counters", {}).get("processed")
+ return (current.get("summary") == "running" and outcome.get("execution", {}).get("state") == "running"
+ and type(processed) is int and 0 < processed < count)
+
+
+def terminal(current, bucket, expected, missing):
+ wanted = {(entry["key"], entry["versionId"]): entry for entry in expected}
+ require(len(wanted) == len(expected) and missing <= set(wanted), "queue scope escaped its frozen ACK set")
+ outcome = current.get("outcome") or {}
+ require(current.get("summary") == "finished" and outcome.get("execution") == {"state": "completed"}
+ and outcome.get("coverage") == "complete", "queue task did not complete its actual responsibility")
+ counts = {"processed": len(expected), "healed": len(missing), "unchanged": len(expected) - len(missing),
+ "failed": 0, "skipped": 0, "unknown": 0, "attemptFailures": 0, "overflowed": False}
+ counters = outcome.get("counters") or {}
+ require(all(type(counters.get(name)) is type(value) and counters[name] == value for name, value in counts.items()),
+ "queue task counters hide unresolved, false or failed repairs")
+ rows = outcome.get("objects")
+ require(isinstance(rows, list) and 0 < len(rows) <= 128 and outcome.get("objectsTruncated") is (len(expected) > 128),
+ "queue task confused bounded diagnostics with the independent complete manifest")
+ seen, incarnation = set(), None
+ for row in rows:
+ item = row.get("identity") or {}
+ version = item.get("versionId")
+ require(version is None or isinstance(version, str), "queue receipt has an untyped version identity")
+ pair = item.get("object"), public_version_id(version)
+ require(item.get("bucket") == bucket and pair in wanted and pair not in seen
+ and type(item.get("poolIndex")) is int and item["poolIndex"] == 0
+ and type(item.get("setIndex")) is int and item["setIndex"] == 0
+ and isinstance(item.get("bucketIncarnationId"), str) and item["bucketIncarnationId"],
+ "queue receipt crossed a bucket/version/incarnation/set or duplicated a result")
+ incarnation = incarnation or item["bucketIncarnationId"]
+ require(item["bucketIncarnationId"] == incarnation
+ and row.get("disposition", {}).get("state") == ("repaired" if pair in missing else "verified_healthy"),
+ "queue receipt claimed an incorrect disposition or bucket generation")
+ seen.add(pair)
+ return {"canonicalCounters": counters, "bucketIncarnationId": incarnation,
+ "diagnosticObjects": len(rows), "completePhysicalAndS3ManifestRequiredSeparately": True}
+
+
+def retained(checkpoints, obligations, bindings):
+ """A newer sequence may publish the same responsibility; a new owner may not."""
+ observed = {}
+ for obligation in obligations:
+ key = identity(obligation)
+ require(key in bindings, "retention proof lacks its original exact ACK binding")
+ for owner in bindings[key]:
+ current = checkpoints.get(owner["node"], {})
+ require(current.get("present") is True and current.get("owner") == owner["checkpointOwner"]
+ and type(current.get("sequence")) is int and current["sequence"] >= owner["sequence"],
+ "queue failure lost or replaced the original committed MRF owner")
+ matches = [item for item in current.get("intents", []) if identity(item) == key]
+ require(len(matches) == 1 and matches[0].get("intentDigest") == owner["intentDigest"]
+ and matches[0].get("lifecycle", {}).get("responsibility_id") == owner["responsibilityId"]
+ and matches[0]["lifecycle"].get("source_bucket_incarnation_id") == owner["bucketIncarnationId"]
+ and matches[0]["lifecycle"].get("state") == {"state": "active"}
+ and matches[0]["lifecycle"].get("last_operator_acceptance") is None,
+ "queue failure retired, held, operator-accepted or altered the exact durable responsibility")
+ observed[str(key)] = {"owners": len(bindings[key]), "originalResponsibilityRetained": True}
+ return observed
+
+
+def mrf_full_events(events, nodes, after):
+ return [{"node": node, "event": row} for node in nodes for row in events.get(node, [])
+ if row.get("target") == "rustfs::heal::manager" and row.get("event") == "heal_queue_admission"
+ and row.get("source") == "mrf" and row.get("priority") == "Normal"
+ and row.get("result") == "rejected_full" and row.get("queue_capacity") == CAPACITY
+ and row.get("queue_len") == CAPACITY and isinstance(row.get("request_id"), str) and row["request_id"]
+ and event_time(row) is not None and event_time(row) >= after]
+
+
+class Pressure:
+ def __init__(self, probe, fleet, binding, active):
+ self.probe, self.fleet, self.binding, self.active = probe, fleet, binding, active
+ self.requests, self.samples = 0, []
+ self.baseline = {node: fleet.record("resource-baseline", {"node": node, **fleet.remote(node, "process-metrics")})
+ for node in fleet.nodes}
+
+ def call(self, path):
+ self.requests += 1
+ require(self.requests <= MAX_ADMIN_REQUESTS, "pressure exceeded its pre-registered request ceiling")
+ begin = time.monotonic_ns()
+ observed = self.fleet.record("bounded-pressure-request", self.probe.admin(path, [],
+ json.dumps({**BASE_OPTIONS, "recursive": True}).encode()))
+ self.fleet.record("bounded-admission-latency", {"path": path, "requestNumber": self.requests,
+ "beginMonotonicNs": begin, "endMonotonicNs": time.monotonic_ns(),
+ "httpStatus": observed.get("httpStatus"), "errorsIncludedInRequestCount": True})
+ return observed
+
+ def admit(self, path):
+ observed = self.call(path)
+ token = (observed.get("response") or {}).get("clientToken") if isinstance(observed.get("response"), dict) else None
+ require(observed.get("httpStatus") == 200 and isinstance(token, str) and token, "pressure request lacks an accepted token")
+ self.active.append((path, token))
+ return token
+
+ def sample(self):
+ observed = self.fleet.record("actual-queue-sample", self.probe.admin("/rustfs/admin/v3/background-heal/status", [], b""))
+ require(observed.get("httpStatus") == 200, "pressure status failed")
+ operations = queue_snapshot(observed.get("response") or {}, len(self.fleet.nodes))
+ samples = {node: self.fleet.record("resource-curve", {"node": node, **self.fleet.remote(node, "process-metrics")})
+ for node in self.fleet.nodes}
+ self.fleet.record("bounded-resource-proof", resources(samples, self.baseline))
+ self.samples.append({"monotonicNs": time.monotonic_ns(), "operations": operations})
+ return operations
+
+
+def run_topology(args, topology, nodes, **unused):
+ directory = args.evidence / topology
+ directory.mkdir()
+ run_id = f"gh-{os.environ['GITHUB_RUN_ID']}-{os.environ.get('GITHUB_RUN_ATTEMPT', '1')}-{topology.lower()}"
+ fleet = Fleet(nodes, directory, run_id, args.tool_directory, runtime_profile="queue-pressure", resource_profile="cursor")
+ candidate = json.loads(args.candidate.read_text())
+ require(candidate["source_sha"] == REFERENCE["candidateCommit"], "unsupported candidate queue contract")
+ fleet.record("inspector-build", verify_inspector(args.tool_directory, candidate))
+ fleet.record("MRF-contract", verify_reference(candidate))
+ prefix = "rv-heal-" + hashlib.sha256(run_id.encode()).hexdigest()[:16]
+ fleet.snapshot_buckets = [prefix, *[f"{prefix}-queue-{index}" for index in range(len(COUNTS))], prefix + "-durable"]
+ access, secret = secrets.token_hex(16), secrets.token_hex(24)
+ os.environ.update(HEAL_ACCESS_KEY=access, HEAL_SECRET_KEY=secret)
+ binding = {"schemaVersion": 1, "runId": run_id, "jumpHostOnly": True, "exclusiveHealService": True,
+ "leaseExpiresAt": (datetime.now(timezone.utc) + timedelta(minutes=45)).isoformat(), "bucketPrefix": prefix,
+ "endpoints": {f"node{index}": f"http://rustfs-node{index}:9000" for index in range(1, len(nodes) + 1)},
+ "defaultEndpoint": "node1", "candidateCommit": candidate["source_sha"], "harnessCommit": args.testing_commit,
+ "candidatePackageSha256": candidate["package_sha256"], "candidateBinarySha256": args.binary_sha,
+ "rcBinarySha256": hashlib.sha256(args.rc_bin.read_bytes()).hexdigest(),
+ "maxInventoryObjects": sum(COUNTS) + 16, "maxInventoryBytes": (sum(COUNTS) + 16) * SIZE}
+ fleet.record("binding", binding)
+ fleet.record("frozen-pressure-budget", {"source": REFERENCE, "runtime": runtime_config("queue-pressure"),
+ "bucketObjectCounts": list(COUNTS), "objectBytes": SIZE, "pressureSeconds": PRESSURE_SECONDS,
+ "drainSeconds": DRAIN_SECONDS, "maxAdminRequests": MAX_ADMIN_REQUESTS, "resourceBounds": RESOURCE_BOUNDS,
+ "durableDegradedWrites": 3, "maxConcurrentClients": 1, "fourSourceBranches": list(BRANCHES)})
+ require(set(select(CASE_ID)[0]["variants"]) == set(BRANCHES), "source queue branches changed")
+ probe, active, scopes = Probe(binding), [], []
+ result = {"topology": topology, "verdict": "RUNNING", "cleanup": "PENDING", "branches": {}}
+ try:
+ fleet.deploy(args.binary_sha, prefix, access, secret)
+ for index, count in enumerate(COUNTS):
+ bucket = f"{prefix}-queue-{index}"
+ recorder = Recorder(probe, directory / f"fixture-{index}")
+ expected = seed(recorder, bucket, "DX", count, [SIZE])
+ baseline = fleet.record("full-baseline", probe.inventory(bucket, unversioned_current_tags=True))
+ fleet.record("ACK-full-manifest", {"bucket": bucket, "expected": expected, **verify_seed(expected, baseline)})
+ scopes.append({"bucket": bucket, "expected": expected, "baseline": baseline,
+ "path": "/rustfs/admin/v3/heal/" + quote(bucket, safe="") + "/objects/"})
+ durable_bucket = prefix + "-durable"
+ durable_recorder = Recorder(probe, directory / "durable-fixture")
+ durable_expected = seed(durable_recorder, durable_bucket, "DB", 4, [SIZE])
+ durable_baseline = fleet.record("durable-baseline", probe.inventory(durable_bucket, unversioned_current_tags=True))
+ verify_seed(durable_expected, durable_baseline)
+ fleet.stop()
+ original = fleet.snapshot("physical-baseline", quiescent=True)
+ fleet.record("topology-proof", validate_topology(original, topology))
+ targets = []
+ for index, scope in enumerate(scopes):
+ scope["cohorts"] = map_cohorts(original_subset(original, scope["bucket"]), scope["bucket"], scope["expected"], count=COUNTS[index])
+ verify_part_manifest(fleet, scope["cohorts"], "baseline-part-manifest", count=COUNTS[index])
+ target = scope["cohorts"][-1]
+ scope["missing"] = {(target["key"], target["versionId"])}
+ targets.append(target)
+ params = {"disk": target["disk"], "relativePath": target["relativePath"], "kind": "missing",
+ "faultId": bounded_fault_id("I04", "queue", index), "expectedSha256": target["sha256"]}
+ fleet.faults.append((target["node"], params))
+ scope["fault"] = fleet.record("exact-missing-part", {"node": target["node"], **fleet.remote(target["node"], "apply-fault", **params)})
+ faulted = fleet.snapshot("physical-fault", quiescent=True)
+ fleet.record("fault-proof", validate_missing_batch(original, faulted, targets, [scope["fault"] for scope in scopes]))
+ fleet.start()
+ require(physical_content(faulted) == physical_content(fleet.snapshot("physical-before-pressure")),
+ "ATTRIBUTION_NOT_OBSERVED: an owned fault changed before pressure admission")
+ initial_checkpoints = read_checkpoints(fleet, nodes, "MRF-empty-baseline")
+ require(all(not item["intents"] for item in initial_checkpoints.values()), "pressure fixture has unrelated durable responsibility")
+ # Deny only the dedicated durable bucket on one owned drive. System
+ # metadata must remain readable/writable for the real MRF journal.
+ # Finish SSH permission probes before admitting the finite pressure work;
+ # the actual MRF Full event after the SDK ACKs is still mandatory.
+ gate_node, gate_disk = nodes[0], 0
+ owner = fleet.record("original-gated-process", fleet.remote(gate_node, "process"))
+ fleet.drive_gates.add(gate_node)
+ fleet.record("permission-gate-armed", fleet.remote(gate_node, "drive-gate-arm-live", disk=gate_disk, expected=owner))
+ fleet.record("permission-gate-activated", fleet.remote(gate_node, "drive-gate-activate"))
+ gate_proof = fleet.record("actual-service-refusal", fleet.remote(gate_node, "drive-gate-probe"))
+ validate_durable_gate(gate_proof, run_id, durable_bucket)
+ window = JournalWindow(fleet, "pressure-admission", event_targets={"rustfs::heal::manager", "rustfs::heal::task"})
+ pressure = Pressure(probe, fleet, binding, active)
+ first = scopes[0]
+ first["token"] = pressure.admit(first["path"])
+ deadline = time.monotonic() + PRESSURE_SECONDS
+ while True:
+ running = status(probe, fleet, first["path"], first["token"], "pressure-running-window")
+ if active_window(running, COUNTS[0]): break
+ require(running.get("summary") == "running" and time.monotonic() < deadline,
+ "NOT_OBSERVED: no partial running task for queue pressure")
+ time.sleep(0.05)
+ merged = pressure.call(first["path"])
+ require(merged.get("httpStatus") == 200 and merged.get("response", {}).get("clientToken") == first["token"],
+ "duplicate admission created another task instead of merging its original token")
+ dropped = rejection(pressure.call(first["path"] + "0001.bin"), admission="dropped",
+ reason="overlapping_paths", code="OperationAborted", http=409)
+ require(active_window(status(probe, fleet, first["path"], first["token"], "after-merge-and-drop"), COUNTS[0]),
+ "NOT_OBSERVED: Merged/Dropped were not bracketed by the same active task")
+ for scope in scopes[1:3]:
+ scope["admittedMonotonicNs"] = time.monotonic_ns()
+ scope["token"] = pressure.admit(scope["path"])
+ require(len({scope["token"] for scope in scopes[:3]}) == 3, "independent buckets reused a task identity")
+ rejected = rejection(pressure.call(scopes[3]["path"]), admission="full", reason="none", code="SlowDown", http=503)
+ require(pressure.sample()["queueLength"] == CAPACITY, "NOT_OBSERVED: Full was not witnessed with the actual two-slot queue")
+ earliest_ms, ack_start = int(time.time() * 1000) - 5000, datetime.now(timezone.utc)
+ durable_expected, obligations, _ = acknowledge(durable_recorder, durable_bucket, "offline-put", durable_expected, fixture="DB")
+ fleet.record("durable-ACK-ledger", {"expected": durable_expected, "obligations": obligations})
+ deadline = time.monotonic() + PRESSURE_SECONDS
+ while True:
+ records = read_checkpoints(fleet, nodes, "committed-ACKs-during-Full")
+ try:
+ bindings = pending_bindings(records, obligations, earliest_ms)
+ except ValueError as exc:
+ require(str(exc).startswith("NOT_OBSERVED:") and time.monotonic() < deadline,
+ "durable pressure persistence failed: " + str(exc))
+ time.sleep(0.5)
+ continue
+ all_intents = {identity(item) for value in records.values() for item in value["intents"]}
+ require(all_intents == {identity(item) for item in obligations}, "MRF Full isolation contains foreign or unexplained durable ACKs")
+ full = mrf_full_events(window.capture(), nodes, ack_start)
+ if full: break
+ require(time.monotonic() < deadline, "NOT_OBSERVED: the degraded-write MRF source never encountered real Full")
+ time.sleep(0.2)
+ # Capture again AFTER the rejection event, before rejoining. Admission
+ # alone cannot discharge any of the three exact durable responsibilities.
+ fleet.record("Full-durable-retention-proof", retained(read_checkpoints(fleet, nodes, "after-MRF-Full"), obligations, bindings))
+ pressure.sample()
+ rejoin_time = datetime.now(timezone.utc)
+ released = fleet.record("same-process-drive-rejoin", fleet.remote(gate_node, "drive-gate-release-live"))
+ require(released.get("sameOriginalProcess") is True and released.get("serviceRestarted") is False,
+ "pressure release restarted the scheduler or replaced its original process")
+ fleet.drive_gates.remove(gate_node)
+ deadline, proofs, completed = time.monotonic() + DRAIN_SECONDS, {}, {}
+ while True:
+ operations = pressure.sample()
+ if "token" not in scopes[3] and operations["queueLength"] < CAPACITY:
+ retry = pressure.call(scopes[3]["path"])
+ if retry.get("httpStatus") == 200:
+ token = retry.get("response", {}).get("clientToken")
+ require(isinstance(token, str) and token and token not in {scope["token"] for scope in scopes[:3]},
+ "previously Full bucket lost its independent admitted token")
+ scopes[3].update(token=token, admittedMonotonicNs=time.monotonic_ns())
+ active.append((scopes[3]["path"], token))
+ else:
+ rejection(retry, admission="full", reason="none", code="SlowDown", http=503)
+ for scope in scopes:
+ if "token" not in scope or scope["bucket"] in completed: continue
+ current = status(probe, fleet, scope["path"], scope["token"], "per-bucket-drain")
+ if current.get("summary") != "running":
+ completed[scope["bucket"]] = terminal(current, scope["bucket"], scope["expected"], scope["missing"])
+ completed[scope["bucket"]]["completionMonotonicNs"] = time.monotonic_ns()
+ active.remove((scope["path"], scope["token"]))
+ events = window.capture()
+ for obligation in obligations:
+ key = identity(obligation)
+ if key in proofs: continue
+ for task in source_tasks(events, obligation, bindings[key], rejoin_time):
+ endpoint = "node" + str(nodes.index(task["node"]) + 1)
+ path = "/rustfs/admin/v3/heal/" + quote(durable_bucket, safe="") + "/" + quote(obligation["key"], safe="/")
+ current = status(Probe(binding, endpoint), fleet, path, task["taskId"], "MRF-exact-ACK-completion")
+ proofs[key] = {"task": task, "receipt": verified_receipt(current, obligation, bindings[key])}
+ break
+ if len(completed) == len(scopes) and len(proofs) == len(obligations):
+ try:
+ retired = retirement(read_checkpoints(fleet, nodes, "exact-retirement-after-proof"), obligations, bindings)
+ break
+ except ValueError as exc:
+ require(str(exc).startswith("NOT_OBSERVED:") and time.monotonic() < deadline,
+ "durable queue retirement failed: " + str(exc))
+ require(time.monotonic() < deadline, "queue drain exceeded its pre-registered cross-bucket completion budget")
+ time.sleep(2)
+ idle_fleet(fleet, binding)
+ fleet.stop()
+ repaired = fleet.snapshot("physical-product-result-before-GET", quiescent=True)
+ validate_repaired_physical(original_subset(original, None, excluded=durable_bucket),
+ original_subset(repaired, None, excluded=durable_bucket), targets[0], repair_targets=targets)
+ for index, scope in enumerate(scopes):
+ fleet.record("complete-external-product-proof", verify_part_manifest(fleet, scope["cohorts"], "repaired-part-manifest", count=COUNTS[index]))
+ for entry in durable_expected:
+ target = map_target(original_subset(repaired, durable_bucket), durable_bucket, entry["key"], "data", 1, entry["versionId"])
+ fleet.record("all-degraded-ACK-parts", verify_cohort(fleet, target, "durable-all-member-streaming"))
+ retirement(read_checkpoints(fleet, nodes, "quiescent-retirement", quiescent=True), obligations, bindings)
+ fleet.start()
+ for endpoint in binding["endpoints"]:
+ peer = Probe(binding, endpoint)
+ for scope in scopes:
+ inventory = fleet.record("all-peer-S3", {"endpoint": endpoint, "bucket": scope["bucket"],
+ **peer.inventory(scope["bucket"], unversioned_current_tags=True)})
+ fleet.record("all-peer-full-manifest", {**verify_seed(scope["expected"], inventory),
+ **compare_inventories(scope["baseline"], inventory)})
+ inventory = fleet.record("all-peer-durable-S3", {"endpoint": endpoint,
+ **peer.inventory(durable_bucket, unversioned_current_tags=True)})
+ fleet.record("all-peer-durable-manifest", verify_seed(durable_expected, inventory))
+ fleet.stop()
+ require(physical_content(repaired) == physical_content(fleet.snapshot("physical-after-S3", quiescent=True)),
+ "verification GET supplied repairs absent from the prior physical result")
+ result.update(verdict="PASS", branches={
+ "full-queue": {"verdict": "PASS", "adminRejection": rejected, "MRFRejections": full,
+ "durableRetirement": retired, "verifiedSourceACKs": len(proofs)},
+ "dropped-admission": {"verdict": "PASS", "typedRejection": dropped, "independentRepairCount": 0},
+ "merged-duplicates": {"verdict": "PASS", "originalTaskId": first["token"], "independentRepairCount": 0},
+ "cross-bucket-fairness": {"verdict": "PASS", "completedBuckets": completed, "drainBudgetSeconds": DRAIN_SECONDS,
+ "requestCount": pressure.requests, "boundedQueueAndResourceSamples": pressure.samples}},
+ completePhysicalAndS3ManifestsVerified=True, backupRestorationUsedAsRepair=False)
+ except (Exception, KeyboardInterrupt) as exc:
+ result.update(verdict="FAIL", failure={"type": type(exc).__name__, "message": str(exc)})
+ try:
+ if fleet.reserved:
+ fleet.stop()
+ fleet.snapshot("physical-failure", quiescent=True)
+ read_checkpoints(fleet, nodes, "retained-failure-responsibility", quiescent=True)
+ except Exception as evidence_error:
+ result["failureEvidenceError"] = str(evidence_error)
+ finally:
+ for path, token in active:
+ try:
+ fleet.record("stop-unresolved-owned-task", probe.admin(path, [("clientToken", token), ("forceStop", "true")], b""))
+ except Exception as exc:
+ fleet.record("stop-error", {"error": str(exc)})
+ result["cleanup"] = "PASS" if fleet.cleanup() else "FAIL"
+ fleet.record("topology-result", result)
+ return result
+
+
+def original_subset(snapshot, bucket, *, excluded=None):
+ return {node: {**value, "files": [entry for entry in value["files"]
+ if (bucket is None or entry["path"].startswith(bucket + "/"))
+ and (excluded is None or not entry["path"].startswith(excluded + "/"))]}
+ for node, value in snapshot.items()}
+
+
+if __name__ == "__main__":
+ try:
+ generic_main(case_id=CASE_ID, branches=BRANCHES, runner=run_topology)
+ except Exception as exc:
+ print(f"Heal queue-pressure validation failed: {exc}", file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_heal/quorum_boundary.py b/scripts/validation_heal/quorum_boundary.py
new file mode 100644
index 00000000..d191451f
--- /dev/null
+++ b/scripts/validation_heal/quorum_boundary.py
@@ -0,0 +1,366 @@
+#!/usr/bin/env python3
+"""B06: independent read, manual repair and restored-source quorum witnesses."""
+
+from __future__ import annotations
+
+import argparse
+import copy
+from datetime import UTC, datetime, timedelta
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import secrets
+import signal
+import sys
+
+try:
+ from .heal_probe import Probe, payload, require, save
+ from .heal_workload import Recorder, verify_seed
+ from .healthy import Fleet, physical_content, validate_topology
+ from .inspector import verify as verify_inspector
+ from .matrix import select
+ from .heal_fault import bounded_fault_id
+ from .shards import files, map_target, run_task, validate_fault, validate_repaired_physical, verify_cohort
+ from .versions import (validate_version_receipts, validate_unrecoverable_get,
+ verify_quorum_contract, verify_version_s3)
+except ImportError:
+ from heal_probe import Probe, payload, require, save
+ from heal_workload import Recorder, verify_seed
+ from healthy import Fleet, physical_content, validate_topology
+ from inspector import verify as verify_inspector
+ from matrix import select
+ from heal_fault import bounded_fault_id
+ from shards import files, map_target, run_task, validate_fault, validate_repaired_physical, verify_cohort
+ from versions import (validate_version_receipts, validate_unrecoverable_get,
+ verify_quorum_contract, verify_version_s3)
+
+
+CASE_ID = "heal.read-quorum-boundary"
+BRANCHES = ("exact-r-read", "exact-r-heal-write-limited", "restored-w")
+SIZE = 5 * 1024**2 + 123
+# These reviewed lock policies are deliberately candidate-specific. They do
+# not require a service checkout on the jump host or a modified service binary.
+LOCK_REFERENCE = {"candidateCommit": "072b57b37559fbe626aeda8abf61617fdcbdd926",
+ "clientDomain": "distinct nonempty pool endpoint host:port; H1 uses its local manager",
+ "exclusiveQuorumRule": "floor(clientCount/2)+1; local manager=1",
+ "sharedQuorumRule": "ceil(clientCount/2); local manager=1",
+ "sourceFiles": {
+ "crates/ecstore/src/core/sets.rs": "06543b1d083f489346171ba489b1eb1e0b7899c844f0ebf15e16ad467cf5e2f1",
+ "crates/ecstore/src/runtime/sources.rs": "7d88f56e6d06fd31850bbbdb2fd9faf20fcffbaedf380888f5f9285c309794f2",
+ "crates/ecstore/src/set_disk/ops/locking.rs": "44d50605c5538adf8fc6f25034e426011d0087b37e895f8246c79cbd711b2a42",
+ "crates/ecstore/src/set_disk/ops/heal.rs": "767c468b780cf5f4e863b078f46c63440549e75d88b6b43bb59ed83c3b4a47b2",
+ "crates/lock/src/distributed_lock.rs": "657796708fdc5f826f896147b74a9366dae875fd6b69e4b253994966ed5f6e37"}}
+
+
+LOCK_REFERENCES = {
+ LOCK_REFERENCE["candidateCommit"]: LOCK_REFERENCE,
+ "1c7fc818dd858247c45b39542abd8291b1e6e31b": {
+ **LOCK_REFERENCE,
+ "candidateCommit": "1c7fc818dd858247c45b39542abd8291b1e6e31b",
+ "rpcControlChannelPolicy": "class-aware control channel; audience and replay-scope authentication retained; live admission remains required",
+ "sourceFiles": {
+ **LOCK_REFERENCE["sourceFiles"],
+ "crates/ecstore/src/runtime/sources.rs": "fad5d30e9f76a71c41a4dd5cfa816d3ea49b0fa082ab111118e2ee5a6f8fcc90",
+ "crates/ecstore/src/set_disk/ops/heal.rs": "fc576119f455848ce279772ffca6d2943cb9f909968cae367860a9887d3e020e",
+ "crates/ecstore/src/cluster/rpc/remote_locker.rs": "506a16274efc0ac7dd2ffa4e2faf2b2a057deacfb8fb9480f4785768628b9cbf",
+ "crates/ecstore/src/cluster/rpc/client.rs": "b37e4107e898a3467520c8516608c8eaaa0c1d56a2eac82d11982eb483396291"},
+ },
+}
+
+
+LOCK_REFERENCES['85c706cac09d53d0649487b8f1865f18beb53225'] = {
+ **LOCK_REFERENCES['1c7fc818dd858247c45b39542abd8291b1e6e31b'],
+ 'candidateCommit': '85c706cac09d53d0649487b8f1865f18beb53225'}
+
+LOCK_REFERENCES['62ff96774fa4139d45f14d70b70519aae8070ac0'] = {
+ **LOCK_REFERENCES['85c706cac09d53d0649487b8f1865f18beb53225'],
+ 'candidateCommit': '62ff96774fa4139d45f14d70b70519aae8070ac0'}
+
+LOCK_REFERENCES['1a3f038c90704f9ad9693eddcf7d861c9530f13c'] = {
+ **LOCK_REFERENCES['62ff96774fa4139d45f14d70b70519aae8070ac0'],
+ 'candidateCommit': '1a3f038c90704f9ad9693eddcf7d861c9530f13c',
+ 'manualHealLockPolicy': 'ordinary manual heal retains the exclusive namespace owner through storage completion; read-repair has a separate shared data phase and exclusive commit fence',
+ 'sourceFiles': {
+ **LOCK_REFERENCES['62ff96774fa4139d45f14d70b70519aae8070ac0']['sourceFiles'],
+ 'crates/ecstore/src/set_disk/ops/locking.rs': 'b75bb680ab648a1cac84a14318e7b0c5022897be43b624281f6fa277235a4e62',
+ 'crates/ecstore/src/set_disk/ops/heal.rs': '0b706ac476e02202d5ad217a166eb523abfd3cfd123902a9fb7edf27bd2bf376',
+ 'crates/ecstore/src/cluster/rpc/client.rs': '515b1ec94c2d88e8cf77646652dc9b84405f1c5e5eb4cc103b79be21746edc47'},
+}
+
+
+def lock_reference(candidate_commit):
+ require(isinstance(candidate_commit, str) and candidate_commit in LOCK_REFERENCES,
+ "lock policy is not reviewed for this candidate")
+ return copy.deepcopy(LOCK_REFERENCES[candidate_commit])
+
+
+def seed_fixtures(recorder, bucket):
+ recorder.call("create_bucket", {"Bucket": bucket})
+ expected = []
+ for branch in BRANCHES:
+ key = "boundary/" + branch + ".bin"
+ body = payload(f"{bucket}/{key}/0", SIZE)
+ response = recorder.call("put_object", {"Bucket": bucket, "Key": key,
+ "Metadata": {"heal-fixture": "DB", "branch": branch}, "Tagging": "suite=heal&fixture=DB"}, body)
+ require(response.get("VersionId") in {None, "null"}, "B06 requires never-versioned independent objects")
+ expected.append({"key": key, "versionId": "null", "deleteMarker": False, "isLatest": True,
+ "payload": {"size": len(body), "sha256": hashlib.sha256(body).hexdigest()}})
+ return expected
+
+
+def boundary_plan(branch, snapshot, bucket, expected, contract):
+ require(branch in BRANCHES and contract.get("recognized") is True
+ and contract.get("writeQuorumRule") == "data-plus-one-if-equal-parity",
+ "B06 requires a recognized candidate read/write quorum contract")
+ selected = [entry for entry in expected if entry["key"] == "boundary/" + branch + ".bin"]
+ require(len(selected) == 1 and selected[0].get("versionId") == "null"
+ and selected[0].get("deleteMarker") is False, "B06 branch reused or lost its independent ACK")
+ target = map_target(snapshot, bucket, selected[0]["key"], "data", 1)
+ geometry = target["logical"]["erasure"]
+ read, parity = geometry["data_blocks"], geometry["parity_blocks"]
+ write, total = read + int(read == parity), read + parity
+ retained_count = read - 1 if branch == "restored-w" else read
+ ordered = sorted(target["cohort"], key=lambda item: item["index"])
+ retained, missing = ordered[:retained_count], ordered[retained_count:]
+ faults = [{**target, **shard} for shard in missing]
+ restore_count = write - retained_count if branch == "restored-w" else 0
+ require(read > 1 and len(ordered) == total and len({item["index"] for item in ordered}) == total
+ and len(retained) == retained_count and 0 <= restore_count < len(faults),
+ "B06 layout cannot leave real product repair targets after restoration to W")
+ hosts = len(snapshot)
+ require(hosts in {1, 4}, "B06 lock client domain requires actual H1/H2 topology")
+ return {"scope": {"bucket": bucket, "key": selected[0]["key"]}, "expected": selected,
+ "target": target, "faults": faults, "retained": retained, "restoreCount": restore_count,
+ "readQuorum": read, "writeQuorum": write, "sameVersionSourcesBefore": retained_count,
+ "writableTargets": total, "metadataAuthorities": total,
+ "lock": {"mode": "local" if hosts == 1 else "distributed", "clientCount": hosts,
+ "sharedQuorum": (hosts + 1) // 2, "exclusiveQuorum": hosts // 2 + 1,
+ "allEndpointsOnline": True, "successfulRepairAlsoProvesActualAdmission": True},
+ "insufficientWriteOrLockApplicable": False,
+ "nonApplicableReason": "F1 isolates payload files only; all N drives, metadata and host lock endpoints remain online. "
+ "R counts readable payload authorities, not writable drives. B08 independently injects actual write/lock loss."}
+
+
+def source_witness(snapshot, plan, missing):
+ indexed = files(snapshot)
+ identities = {(item["node"], item["disk"], item["relativePath"]) for item in missing}
+ cohort = plan["target"]["cohort"]
+ require(len(identities) == len(missing) and identities <= {
+ (item["node"], item["disk"], item["relativePath"]) for item in cohort}, "missing authority escaped its version/set")
+ for item in cohort:
+ identity = item["node"], item["disk"], item["relativePath"]
+ actual = indexed.get(identity)
+ if identity in identities:
+ require(actual is None, "ATTRIBUTION_NOT_OBSERVED: isolated source reappeared before manual repair")
+ else:
+ require(actual is not None and actual["sha256"] == item["sha256"] and actual["size"] == item["size"],
+ "retained same-version source differs from its full shard oracle")
+ metadata = indexed.get((item["node"], item["disk"], f"{plan['scope']['bucket']}/{plan['scope']['key']}/xl.meta"))
+ require(metadata and metadata["sha256"] == item["baselineMetadataSha256"],
+ "F1 changed metadata authority or used a different generation")
+ return {"sameVersionPayloadSources": len(cohort) - len(identities), "missingTargets": len(identities),
+ "allMetadataAuthoritiesOnlineAndUnchanged": True, "allRetainedShardBytesUnchanged": True}
+
+
+def read_observation(observation, expected):
+ require(observation.get("acknowledged") is True and observation.get("resultUnknown") is False
+ and observation.get("response", {}).get("ResponseMetadata", {}).get("HTTPStatusCode") == 200
+ and observation.get("payload") == expected["payload"], "GET at exactly R did not return the acknowledged bytes")
+
+
+def read_interference(before, after, plan):
+ """Permit only exact product reconstruction, and retain each observed target."""
+ left, right = files(before), files(after)
+ targets = {(item["node"], item["disk"], item["relativePath"]): item for item in plan["faults"]}
+ reconstructed = []
+ for identity, target in targets.items():
+ require(identity not in left, "exact-R GET began with an already reconstructed source")
+ if identity in right:
+ require(right[identity]["sha256"] == target["sha256"] and right[identity]["size"] == target["size"],
+ "GET produced a target with incorrect shard bytes")
+ reconstructed.append({"node": identity[0], "disk": identity[1], "relativePath": identity[2]})
+ return {"readRepairObserved": bool(reconstructed), "reconstructedTargets": reconstructed,
+ "manualRepairClaimed": False, "manualStartIssuedForReadBranch": False,
+ "note": "Any unexpected reconstruction is retained as interference; this GET never certifies the separate manual branch."}
+
+
+def recover_registered(fleet, registrations, label):
+ for registration in reversed(tuple(registrations)):
+ node, params = registration
+ receipt = fleet.record(label, fleet.remote(node, "recover-fault", **params))
+ require(receipt.get("physicalRepairPassAllowed") is False, "backup restoration must not certify product repair")
+ fleet.faults.remove(registration)
+ registrations.remove(registration)
+
+
+def run_topology(args, topology, nodes):
+ directory = args.evidence / "DB" / topology
+ directory.mkdir(parents=True)
+ run_id = f"gh-{os.environ['GITHUB_RUN_ID']}-{os.environ.get('GITHUB_RUN_ATTEMPT', '1')}-{topology.lower()}"
+ fleet = Fleet(nodes, directory, run_id, args.tool_directory)
+ candidate = json.loads(args.candidate.read_text())
+ locks = lock_reference(candidate["source_sha"])
+ contract = verify_quorum_contract(fleet.record("inspector-build", verify_inspector(args.tool_directory, candidate)), candidate)
+ require(contract.get("writeQuorumRule") == "data-plus-one-if-equal-parity", "B06 write policy is not recognized")
+ require(list(BRANCHES) == select(CASE_ID)[0]["variants"], "B06 executor omitted a source branch")
+ bucket = "rv-heal-" + hashlib.sha256(run_id.encode()).hexdigest()[:16]
+ access, secret = secrets.token_hex(16), secrets.token_hex(24)
+ os.environ.update(HEAL_ACCESS_KEY=access, HEAL_SECRET_KEY=secret)
+ binding = {"schemaVersion": 1, "runId": run_id, "jumpHostOnly": True, "exclusiveHealService": True,
+ "leaseExpiresAt": (datetime.now(UTC) + timedelta(minutes=30)).isoformat(), "bucketPrefix": bucket,
+ "endpoints": {f"node{index}": f"http://rustfs-node{index}:9000" for index in range(1, len(nodes) + 1)},
+ "defaultEndpoint": "node1", "candidateCommit": candidate["source_sha"], "harnessCommit": args.testing_commit,
+ "candidatePackageSha256": candidate["package_sha256"], "candidateBinarySha256": args.binary_sha,
+ "rcBinarySha256": hashlib.sha256(args.rc_bin.read_bytes()).hexdigest(),
+ "maxInventoryObjects": 512, "maxInventoryBytes": 512 * 1024**2}
+ fleet.record("binding", binding)
+ fleet.record("quorum-source-contract", {"payloadAndWrite": contract, "locks": locks})
+ probe, active, admissions = Probe(binding), [], []
+ result = {"topology": topology, "fixture": "DB", "verdict": "RUNNING", "branches": {}, "cleanup": "PENDING"}
+ try:
+ fleet.deploy(args.binary_sha, bucket, access, secret)
+ fleet.record("topology-proof", validate_topology(fleet.snapshot("deployed-topology"), topology))
+ expected = seed_fixtures(Recorder(probe, directory / "fixture"), bucket)
+ baseline = fleet.record("s3-baseline", probe.inventory(bucket, unversioned_current_tags=True))
+ fleet.record("fixture-manifest", {"expected": expected, **verify_seed(expected, baseline)})
+ for branch in BRANCHES:
+ registrations = []
+ try:
+ fleet.stop()
+ before = fleet.snapshot("physical-before-" + branch, quiescent=True)
+ plan = boundary_plan(branch, before, bucket, expected, contract)
+ fleet.record("quorum-plan-" + branch, plan)
+ verify_cohort(fleet, plan["target"], "baseline-bitrot-" + branch)
+ prior = before
+ for index, target in enumerate(plan["faults"]):
+ params = {"disk": target["disk"], "faultId": bounded_fault_id("B06", branch, index)}
+ registration = target["node"], params
+ fleet.faults.append(registration)
+ registrations.append(registration)
+ receipt = fleet.record("fault-" + branch, fleet.remote(target["node"], "apply-fault", **params,
+ relativePath=target["relativePath"], expectedSha256=target["sha256"], kind="missing"))
+ faulted = fleet.snapshot("physical-fault-" + branch, quiescent=True)
+ validate_fault(prior, faulted, target, receipt)
+ prior = faulted
+ source_witness(prior, plan, plan["faults"])
+ fleet.start()
+ witness = fleet.snapshot("physical-before-trigger-" + branch)
+ require(physical_content(prior) == physical_content(witness), "ATTRIBUTION_NOT_OBSERVED: B06 fault changed before trigger")
+ fleet.record("source-window-" + branch, source_witness(witness, plan, plan["faults"]))
+ proof = {"quorums": {name: plan[name] for name in ("readQuorum", "writeQuorum", "writableTargets", "lock",
+ "insufficientWriteOrLockApplicable", "nonApplicableReason")}}
+ if branch == "exact-r-read":
+ observed = fleet.record("exact-r-get", probe.s3("get_object", {"Bucket": bucket, "Key": plan["scope"]["key"]}))
+ read_observation(observed, plan["expected"][0])
+ after_get = fleet.snapshot("physical-after-exact-r-get")
+ proof["read"] = read_interference(witness, after_get, plan)
+ fleet.stop()
+ recover_registered(fleet, registrations, "get-fixture-backup-rescue")
+ repaired = fleet.snapshot("physical-after-get-rescue", quiescent=True)
+ validate_repaired_physical(before, repaired, plan["target"], repair_targets=plan["faults"])
+ proof["backupRescueNotManualRepair"] = True
+ else:
+ remaining = plan["faults"]
+ if branch == "restored-w":
+ denied = fleet.record("below-r-get", probe.s3("get_object", {"Bucket": bucket, "Key": plan["scope"]["key"]}))
+ validate_unrecoverable_get(denied, contract)
+ require(physical_content(prior) == physical_content(fleet.snapshot("physical-after-below-r-get")),
+ "below-R GET changed good shards or repaired without authority")
+ previous_token = admissions[-1]["clientToken"] if admissions else None
+ fleet.stop()
+ stopped = fleet.snapshot("physical-before-restore", quiescent=True)
+ require(physical_content(prior) == physical_content(stopped), "below-R read window changed original authority")
+ proof["belowReadWindow"] = {"frozenUnrecoverableGetMatched": True,
+ "sourceWitness": source_witness(stopped, plan, plan["faults"]), "backupRestoreHasNotRun": True}
+ count = plan["restoreCount"]
+ restored_registrations = registrations[:count]
+ recover_registered(fleet, restored_registrations, "restore-original-sources-to-w")
+ del registrations[:count]
+ remaining = plan["faults"][count:]
+ prior = fleet.snapshot("physical-at-w", quiescent=True)
+ proof["atW"] = source_witness(prior, plan, remaining)
+ require(proof["atW"]["sameVersionPayloadSources"] == plan["writeQuorum"], "restore did not reach exactly W")
+ proof["restoredOriginalSources"] = count
+ fleet.start()
+ witness = fleet.snapshot("physical-before-restored-w-trigger")
+ require(physical_content(prior) == physical_content(witness), "ATTRIBUTION_NOT_OBSERVED: restored-W fault changed before its token")
+ source_witness(witness, plan, remaining)
+ status = run_task(probe, fleet, plan["scope"], 2, active, admissions=admissions)
+ if branch == "restored-w":
+ require(previous_token is None or admissions[-1]["clientToken"] != previous_token,
+ "restored-W Heal reused another branch's manual token")
+ proof["previousIndependentToken"], proof["restoredToken"] = previous_token, admissions[-1]["clientToken"]
+ fleet.stop()
+ repaired = fleet.snapshot("physical-after-manual-" + branch, quiescent=True)
+ proof["responsibility"] = validate_version_receipts(status, bucket, plan["expected"], 2,
+ required_repairs={(plan["scope"]["key"], "null")})
+ proof["physical"] = validate_repaired_physical(before, repaired, plan["target"], repair_targets=remaining)
+ proof["productReconstructedTargets"] = len(remaining)
+ for registration in registrations:
+ fleet.faults.remove(registration)
+ registrations.clear()
+ proof["bitrot"] = verify_cohort(fleet, plan["target"], "repaired-bitrot-" + branch)
+ fleet.start()
+ proof["s3"] = verify_version_s3(probe, fleet, bucket, expected, baseline, "s3-after-" + branch, fixture="DB")
+ result["branches"][branch] = {"verdict": "PASS", **proof}
+ except (Exception, KeyboardInterrupt) as exc:
+ result["branches"][branch] = {"verdict": "FAIL", "failure": {"type": type(exc).__name__, "message": str(exc)}}
+ fleet.stop()
+ fleet.snapshot("physical-failure-" + branch, quiescent=True)
+ recover_registered(fleet, registrations, "error-backup-rescue-" + branch)
+ if isinstance(exc, KeyboardInterrupt) or active:
+ raise
+ result["verdict"] = "PASS" if len(result["branches"]) == len(BRANCHES) and all(
+ item["verdict"] == "PASS" for item in result["branches"].values()) else "FAIL"
+ except (Exception, KeyboardInterrupt) as exc:
+ result.update(verdict="FAIL", failure={"type": type(exc).__name__, "message": str(exc)})
+ finally:
+ for path, token in active:
+ try:
+ fleet.record("stop-unresolved", probe.admin(path, [("clientToken", token), ("forceStop", "true")], b""))
+ except Exception as exc:
+ fleet.record("stop-error", {"error": str(exc)})
+ result["cleanup"] = "PASS" if fleet.cleanup() else "FAIL"
+ fleet.record("topology-result", result)
+ return result
+
+
+def main():
+ def interrupted(signum, frame):
+ raise KeyboardInterrupt(f"Heal workflow interrupted by signal {signum}")
+ signal.signal(signal.SIGTERM, interrupted)
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--candidate", type=Path, required=True)
+ parser.add_argument("--case-id", choices=(CASE_ID,), required=True)
+ parser.add_argument("--testing-commit", required=True)
+ parser.add_argument("--binary-sha", required=True)
+ parser.add_argument("--rc-bin", type=Path, required=True)
+ parser.add_argument("--evidence", type=Path, required=True)
+ parser.add_argument("--tool-directory", type=Path, required=True)
+ args = parser.parse_args()
+ nodes = os.environ.get("RUSTFS_NODES", "vm000 vm001 vm002 vm003").split()
+ require(len(nodes) >= 4 and len(set(nodes)) == len(nodes) and all(
+ re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", node) for node in nodes), "four current Heal nodes required")
+ require(bool(re.fullmatch(r"[0-9a-f]{64}", args.binary_sha)), "invalid candidate binary digest")
+ require(bool(re.fullmatch(r"[1-9][0-9]*", os.environ.get("GITHUB_RUN_ID", ""))), "workflow fleet identity required")
+ results = []
+ try:
+ for topology, selected in (("H1", nodes[:1]), ("H2", nodes[:4])):
+ results.append(run_topology(args, topology, selected))
+ if results[-1]["cleanup"] != "PASS":
+ break
+ finally:
+ complete = len(results) == 2 and all(item["verdict"] == "PASS" and item["cleanup"] == "PASS" for item in results)
+ save(args.evidence / "healthy-summary.json", {"caseId": CASE_ID, "results": results,
+ "cleanup": "PASS" if results and all(item["cleanup"] == "PASS" for item in results) else "FAIL", "complete": complete})
+ require(complete, "B06 requires all independent source branches on H1/H2 and complete cleanup")
+
+
+if __name__ == "__main__":
+ try:
+ main()
+ except Exception as exc:
+ print(f"Heal read quorum validation failed: {exc}", file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_heal/quorum_gate.py b/scripts/validation_heal/quorum_gate.py
new file mode 100644
index 00000000..97a5212b
--- /dev/null
+++ b/scripts/validation_heal/quorum_gate.py
@@ -0,0 +1,143 @@
+"""Read-only gates for exact run-owned roots inside the candidate's mount namespace."""
+
+import errno
+import json
+import os
+from pathlib import Path
+import stat
+import subprocess
+
+
+MARKER = "write-quorum-gate.json"
+PROBE_SOURCE = '''import errno,hashlib,json,os,stat,sys
+folder,metadata = sys.argv[1:]
+result = {"uid": os.geteuid(), "gid": os.getegid()}
+try:
+ fd = os.open(metadata, os.O_RDONLY | os.O_NOFOLLOW)
+ digest = hashlib.sha256()
+ count = 0
+ try:
+ info = os.fstat(fd)
+ if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1 or info.st_size > 64*1024**2:
+ raise ValueError("expected bounded single-link metadata")
+ while chunk := os.read(fd, 1024**2):
+ count += len(chunk)
+ if count > 64*1024**2: raise ValueError("metadata grew")
+ digest.update(chunk)
+ finally:
+ os.close(fd)
+ result["read"] = {"errno": 0, "size": count, "sha256": digest.hexdigest()}
+except OSError as exc:
+ result["read"] = {"errno": exc.errno}
+name = folder + "/.validation-quorum-write-probe"
+try:
+ fd = os.open(name, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
+except OSError as exc:
+ result["writeErrno"] = exc.errno
+else:
+ try:
+ os.write(fd, b"quorum-probe")
+ os.fsync(fd)
+ finally:
+ os.close(fd)
+ os.unlink(name)
+ result["writeErrno"] = 0
+print(json.dumps(result))
+'''
+
+
+def require(value, message):
+ if not value:
+ raise ValueError(message)
+
+
+def checked(state, roots):
+ path = state / MARKER
+ if not path.exists():
+ return None
+ info = path.lstat()
+ require(stat.S_ISREG(info.st_mode) and info.st_uid == 0 and info.st_nlink == 1
+ and stat.S_IMODE(info.st_mode) == 0o600 and info.st_size < 4096, "invalid quorum gate marker")
+ value = json.loads(path.read_text())
+ entries = value.get("roots")
+ require(value.get("schemaVersion") == 1 and value.get("mode") == "read-only"
+ and isinstance(entries, list) and 0 < len(entries) <= len(roots), "invalid quorum gate contract")
+ disks = [row.get("disk") for row in entries]
+ require(all(type(disk) is int and 0 <= disk < len(roots) for disk in disks)
+ and disks == sorted(set(disks)), "quorum gate disks differ")
+ for row in entries:
+ folder = roots[row["disk"]] / "data"
+ info = folder.lstat()
+ require(row["path"] == str(folder) and not folder.is_symlink() and stat.S_ISDIR(info.st_mode)
+ and info.st_uid == 0 and stat.S_IMODE(info.st_mode) == 0o700
+ and row["device"] == info.st_dev and row["inode"] == info.st_ino,
+ "quorum gate root ownership/device/inode drifted; retain namespace")
+ return value
+
+
+def apply(state, roots, disks):
+ require(isinstance(disks, list) and bool(disks) and disks == sorted(set(disks))
+ and all(type(disk) is int and 0 <= disk < len(roots) for disk in disks), "invalid quorum gate disk list")
+ require(checked(state, roots) is None and not (state / "drive-gate.json").exists()
+ and not (state / "disk-replacement.json").exists(), "quorum gate cannot share another drive mutation")
+ rows = []
+ for disk in disks:
+ folder = roots[disk] / "data"
+ info = folder.lstat()
+ require(not folder.is_symlink() and stat.S_ISDIR(info.st_mode) and info.st_uid == 0
+ and stat.S_IMODE(info.st_mode) == 0o700, "expected private owned data root")
+ rows.append({"disk": disk, "path": str(folder), "device": info.st_dev, "inode": info.st_ino})
+ value = {"schemaVersion": 1, "mode": "read-only", "roots": rows}
+ fd = os.open(state / MARKER, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
+ with os.fdopen(fd, "w") as stream:
+ json.dump(value, stream)
+ stream.flush()
+ os.fsync(stream.fileno())
+ require(checked(state, roots) == value, "quorum gate intent differs")
+ return {**value, "state": "ARMED", "hostMountRemounted": False, "sharedPermissionsChanged": False}
+
+
+def properties(state, roots):
+ value = checked(state, roots)
+ return [] if value is None else ["--property=PrivateMounts=true",
+ "--property=ReadOnlyPaths=" + " ".join(row["path"] for row in value["roots"])]
+
+
+def release(state, roots):
+ value = checked(state, roots)
+ if value is not None:
+ (state / MARKER).unlink()
+ return {"state": "CLEAR", "gateWasAbsent": value is None, "hostMountRemounted": False,
+ "privateMountRemovedByStoppedService": True}
+
+
+def validate_probe(observed, expected_read, read_only):
+ require(observed.get("uid") == observed.get("gid") == 0, "quorum probe differs from root service identity")
+ require(observed.get("read") == {"errno": 0, **expected_read}, "quorum probe cannot read exact original metadata")
+ require(observed.get("writeErrno") == (errno.EROFS if read_only else 0),
+ "quorum probe did not witness the expected actual writable/refused root")
+ return {"readableMetadata": True, "writable": not read_only, "writeErrno": observed["writeErrno"],
+ "sameServiceUidAndMountNamespace": True}
+
+
+def probe(state, roots, pid, relative, expected_reads):
+ value = checked(state, roots)
+ gated = {row["disk"] for row in value["roots"]} if value else set()
+ require(type(pid) is int and pid > 1 and len(expected_reads) == len(roots), "live service and all disk expectations required")
+ namespace = Path(f"/proc/{pid}/ns/mnt")
+ if gated:
+ require(namespace.readlink() != Path('/proc/self/ns/mnt').readlink(), "read-only gate lacks private mount namespace")
+ fields = dict(line.split(":", 1) for line in Path(f"/proc/{pid}/status").read_text().splitlines() if ":" in line)
+ require(list(map(int, fields["Uid"].split()))[1] == list(map(int, fields["Gid"].split()))[1] == 0,
+ "quorum candidate effective identity changed")
+ rows = []
+ for disk, root in enumerate(roots):
+ result = subprocess.run(["nsenter", "--mount=" + str(namespace), "python3", "-c", PROBE_SOURCE,
+ str(root / "data"), str(root / "data" / relative)],
+ capture_output=True, text=True, timeout=15)
+ require(result.returncode == 0, "candidate-namespace quorum probe did not complete")
+ observed = json.loads(result.stdout)
+ rows.append({"disk": disk, "observed": observed,
+ **validate_probe(observed, expected_reads[disk], disk in gated)})
+ return {"disks": rows, "backend": "candidate-service-private-read-only-mounts",
+ "hostMountRemounted": False, "sharedPermissionsChanged": False}
diff --git a/scripts/validation_heal/race.py b/scripts/validation_heal/race.py
new file mode 100644
index 00000000..6ec68576
--- /dev/null
+++ b/scripts/validation_heal/race.py
@@ -0,0 +1,1105 @@
+#!/usr/bin/env python3
+"""G01/G03 version races; a blocked native fence never certifies the case."""
+
+from __future__ import annotations
+
+import argparse
+import copy
+from contextlib import contextmanager
+from concurrent.futures import ThreadPoolExecutor
+from datetime import datetime, timedelta, timezone
+import hashlib
+import json
+import os
+from pathlib import Path
+from queue import Empty, SimpleQueue
+import re
+import secrets
+import signal
+import sys
+import threading
+import time
+from urllib.parse import quote
+import uuid
+
+try:
+ from .heal_probe import Probe, canonical, compare_inventories, payload, require, save
+ from .heal_workload import Recorder, seed as seed_workload, verify_seed
+ from .healthy import BASE_OPTIONS, Fleet, physical_content, validate_topology
+ from .inspector import verify as verify_inspector
+ from .matrix import select
+ from .heal_fault import bounded_fault_id
+ from .shards import files, map_target, validate_repaired_physical
+ from .cursor import validate_missing_batch, verify_part_manifest
+ from .versions import public_version_id
+ from .race_native_evidence import verify as verify_native
+ from .race_io import set_fleet as set_race_io_cap
+ from .mpu_race_backups import verify_versions as verify_version_backups
+ from .status_keepalive import KeepAliveStatusProbe
+ from .fleet_node import endpoint_resolution
+except ImportError:
+ from heal_probe import Probe, canonical, compare_inventories, payload, require, save
+ from heal_workload import Recorder, seed as seed_workload, verify_seed
+ from healthy import BASE_OPTIONS, Fleet, physical_content, validate_topology
+ from inspector import verify as verify_inspector
+ from matrix import select
+ from heal_fault import bounded_fault_id
+ from shards import files, map_target, validate_repaired_physical
+ from cursor import validate_missing_batch, verify_part_manifest
+ from versions import public_version_id
+ from race_native_evidence import verify as verify_native
+ from race_io import set_fleet as set_race_io_cap
+ from mpu_race_backups import verify_versions as verify_version_backups
+ from status_keepalive import KeepAliveStatusProbe
+ from fleet_node import endpoint_resolution
+
+
+CASE_IDS = ("heal.race-unversioned-overwrite", "heal.race-versioned-put", "heal.race-version-delete", "heal.race-markers-and-batch-delete", "heal.race-multipart-complete-abort", "heal.race-copy-and-metadata-update")
+CASE_BRANCHES = {
+ "heal.race-unversioned-overwrite": ("overwrite-vs-deep", "overwrite-vs-read-repair", "native-commit-cleanup"),
+ "heal.race-versioned-put": ("new-version-vs-historical-heal", "read-all-versions-during-heal", "native-version-fence"),
+ "heal.race-version-delete": ("delete-old-version-vs-deep", "delete-current-version-vs-deep", "native-delete-fence"),
+ "heal.race-markers-and-batch-delete": ("create-marker", "delete-marker", "batch-version-delete", "unversioned-delete", "native-marker-fence"),
+ "heal.race-multipart-complete-abort": ("upload-continue", "complete", "abort", "native-mpu-fence"),
+ "heal.race-copy-and-metadata-update": ("copy-source", "copy-destination", "replace-metadata", "update-tags", "native-copy-fence"),
+}
+G01_BRANCHES = CASE_BRANCHES["heal.race-unversioned-overwrite"][:2]
+G02_BRANCHES = CASE_BRANCHES["heal.race-versioned-put"][:2]
+G03_BRANCHES = CASE_BRANCHES["heal.race-version-delete"][:2]
+G04_BRANCHES = CASE_BRANCHES["heal.race-markers-and-batch-delete"][:4]
+G05_BRANCHES = CASE_BRANCHES["heal.race-multipart-complete-abort"][:3]
+G06_BRANCHES = CASE_BRANCHES["heal.race-copy-and-metadata-update"][:4]
+OBJECTS = 257
+SIZE = 257 * 1024 + 123
+TARGET_KEY = "objects/0256.bin"
+REFERENCE_KEY = "objects/0000.bin"
+G06_TARGET_KEY = "中文/文件/0252.bin"
+G06_REFERENCE_KEY = "literal%2Fslash/0254.bin"
+REFERENCE = {
+ "candidateCommit": "072b57b37559fbe626aeda8abf61617fdcbdd926",
+ "sourceFiles": {
+ "rustfs/src/admin/handlers/heal.rs": "7e4007c29a8329ced8e48b88111400e79cffd845bd6ddc6243b79f3cd48e8199",
+ "crates/heal/src/heal/channel.rs": "7f669ca43b9559db9b97fba6b419a6c76a94d76acd8526db132d74b7c534d6b0"},
+ "requested": {**BASE_OPTIONS, "recursive": True, "nolock": False, "pool": None, "set": None},
+ "effective": {**BASE_OPTIONS, "recursive": True, "nolock": False, "pool": None, "set": None},
+ "nativeFence": "pinned release has no reviewed e2e-test-hooks artifact or exact Admin commit/cleanup barrier",
+}
+NATIVE_CANDIDATE = '6afdec9fe1b0b88007f2e5b445c19bf73d51d3ca'
+NATIVE_REFERENCE = {
+ 'candidateCommit': NATIVE_CANDIDATE,
+ 'sourceFiles': {
+ 'rustfs/src/admin/handlers/heal.rs': '6187d9331555ea8421e0d4fe1449d310dd31d0c48f4e57ff43365701d3c6621b',
+ 'crates/heal/src/heal/channel.rs': 'b806939f7101c88ec0746b1b377f25ee5e88cc4871527bd348c7ee5f3f7f195d'},
+ 'requested': {**BASE_OPTIONS, 'recursive': True, 'nolock': False, 'pool': None, 'set': None},
+ 'effective': {**BASE_OPTIONS, 'recursive': True, 'nolock': False, 'pool': None, 'set': None},
+ 'nativeFence': 'exact-source real Heal during committed overwrite cleanup plus stale read-repair final rename',
+}
+VERSION_CANDIDATE = 'bd0671ba781d8ebf509f4f03533228dfb44496f1'
+VERSION_REFERENCE = {**NATIVE_REFERENCE, 'candidateCommit': VERSION_CANDIDATE,
+ 'nativeFence': 'exact-source Heal cleanup and explicit-version commit and deletion fences'}
+MARKER_CANDIDATE = '8380466f3a27c3c3ef4115084178a0ab99a78f32'
+MARKER_REFERENCE = {**VERSION_REFERENCE, 'candidateCommit': MARKER_CANDIDATE,
+ 'nativeFence': 'exact-source Heal cleanup, version and marker/batch-delete commit fences'}
+COPY_CANDIDATE = '757b751482bb970bc321e92059690a7698554358'
+COPY_REFERENCE = {**MARKER_REFERENCE, 'candidateCommit': COPY_CANDIDATE,
+ 'nativeFence': 'exact-source streamed COPY, metadata and tag final-rename fences'}
+NATIVE_RUNTIME = {
+ NATIVE_CANDIDATE: {'reference': NATIVE_REFERENCE, 'cases': ('heal.race-unversioned-overwrite',)},
+ VERSION_CANDIDATE: {'reference': VERSION_REFERENCE,
+ 'cases': ('heal.race-unversioned-overwrite', 'heal.race-versioned-put', 'heal.race-version-delete')},
+ MARKER_CANDIDATE: {'reference': MARKER_REFERENCE,
+ 'cases': ('heal.race-unversioned-overwrite', 'heal.race-versioned-put', 'heal.race-version-delete',
+ 'heal.race-markers-and-batch-delete')},
+ COPY_CANDIDATE: {'reference': COPY_REFERENCE,
+ 'cases': ('heal.race-copy-and-metadata-update', 'heal.race-multipart-complete-abort')},
+}
+# Registry subsets do not enable a fleet case. Its reviewed runtime source and
+# complete case-specific native group must both be present before bootstrap.
+FULL_NATIVE_GROUPS = {
+ 'heal.race-unversioned-overwrite': ['committed-overwrite-cleanup', 'stale-read-repair-final-rename'],
+ 'heal.race-versioned-put': ['versioned-read-repair-lock-owner', 'explicit-version-append-final-rename'],
+ 'heal.race-version-delete': ['explicit-version-delete-final-rename'],
+ 'heal.race-markers-and-batch-delete': ['marker-and-batch-delete-final-rename'],
+ 'heal.race-copy-and-metadata-update': ['copy-and-metadata-final-rename'],
+ 'heal.race-multipart-complete-abort': ['multipart-continue-complete-abort-final-rename'],
+}
+
+
+def source_contract(candidate, case_id):
+ require(case_id in CASE_IDS, 'unregistered race case')
+ if candidate.get('source_sha') == REFERENCE['candidateCommit']:
+ require(case_id not in ('heal.race-multipart-complete-abort','heal.race-markers-and-batch-delete'),
+ 'G04/G05 require their exact-source multipart or marker/batch native group')
+ return REFERENCE
+ entry = NATIVE_RUNTIME.get(candidate.get('source_sha'))
+ require(entry is not None and case_id in entry['cases'] and case_id in FULL_NATIVE_GROUPS
+ and entry['reference']['candidateCommit'] == candidate['source_sha'],
+ 'race candidate/case contract is not reviewed')
+ return entry['reference']
+
+
+def native_preflight(candidate, testing_commit, case_id, directory):
+ reference = source_contract(candidate, case_id)
+ if reference is REFERENCE:
+ return None
+ require(bool(directory), 'race requires exact-source native proof before any fleet mutation')
+ proof = verify_native(directory, candidate, testing_commit, case_id)
+ require(proof['requiredRegressionLabels'] == FULL_NATIVE_GROUPS[case_id]
+ and proof['requiredNativeSubsetPassed'] is True and proof['wholeCasePassClaimed'] is False,
+ 'case-specific native commit group is incomplete')
+ return proof
+
+
+class WindowNotObserved(ValueError):
+ """No observable client overlap; this is not a product success."""
+
+
+def target_key(branch):
+ return G06_TARGET_KEY if branch in G06_BRANCHES else TARGET_KEY
+
+
+def reference_key(branch):
+ return G06_REFERENCE_KEY if branch in G06_BRANCHES else REFERENCE_KEY
+
+
+class DispatchProbe(Probe):
+ """Wake G06 observation from inside the SDK, immediately before sending.
+
+ No wait or delay is added to the SDK call or its response. The notification
+ schedules observation; only the unchanged recorded intervals prove overlap.
+ """
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.dispatched = SimpleQueue()
+
+ def s3_client(self):
+ client = super().s3_client()
+ client.meta.events.register("before-send.s3", lambda **_: self.dispatched.put(time.monotonic_ns()))
+ return client
+
+
+class RaceRecorder(Recorder):
+ """Keep unresolved mutations and exact SDK receipts after any assertion."""
+ def __init__(self, *args, versioned, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.versioned, self.pending, self.seen = versioned, None, set()
+
+ def call(self, operation, params, body=None, **kwargs):
+ self.pending = {"operation": operation, "params": copy.deepcopy(params)}
+ result = super().call(operation, params, body, **kwargs)
+ version = result.get("VersionId")
+ if operation == "put_object":
+ require(isinstance(result.get("ETag"), str) and result["ETag"]
+ and result.get("DeleteMarker") in {None, False}, "G01/G03 PUT ACK lacks ETag or changed kind")
+ if self.versioned:
+ require(isinstance(version, str) and str(uuid.UUID(version)) == version and uuid.UUID(version).int
+ and (params["Key"], version) not in self.seen, "G01/G03 PUT ACK has an invalid/reused VersionId")
+ self.seen.add((params["Key"], version))
+ else:
+ require(version in {None, "null"}, "G01/G03 never-versioned PUT created a real version")
+ elif operation == "delete_object":
+ require(self.versioned and params.get("VersionId") in {
+ value for key, value in self.seen if key == params["Key"]}
+ and version == params["VersionId"] and result.get("DeleteMarker") in {None, False},
+ "G01/G03 DELETE ACK changed the exact known data-version identity")
+ self.pending = None
+ return result
+
+
+def settings(observed, reference=REFERENCE):
+ require(observed.get("httpStatus") == 200 and isinstance(observed.get("response"), dict),
+ "G01/G03 status has an unknown or failed HTTP response")
+ value = observed["response"]
+ require(value.get("summary") in {"running", "finished"}, "G01/G03 original task disappeared, stopped or failed")
+ # Canonical JSON distinguishes bool/int and preserves raw null selectors.
+ require(canonical(value.get("settings")) == canonical(reference["effective"]),
+ "G01/G03 Admin nolock was not safely normalized or changed unrelated options")
+ return value["settings"]
+
+
+def sample_overlap(ack, observed, path, token, reference=REFERENCE):
+ foreground = ack.get("observation") or {}
+ settings(observed, reference)
+ value = observed["response"]
+ a, z = foreground.get("callStartedMonotonicNs"), foreground.get("callFinishedMonotonicNs")
+ b, c = observed.get("callStartedMonotonicNs"), observed.get("callFinishedMonotonicNs")
+ processed = (value.get("outcome") or {}).get("counters", {}).get("processed")
+ return (foreground.get("acknowledged") is True and foreground.get("clientCallInvoked") is True
+ and foreground.get("clockDomain") and foreground["clockDomain"] == observed.get("clockDomain")
+ and observed.get("path") == path and [tuple(row) for row in observed.get("query", [])] == [("clientToken", token)]
+ and all(type(tick) is int for tick in (a, z, b, c)) and 0 < a < b < c < z
+ and value["summary"] == "running" and type(processed) is int and processed > 0)
+
+
+def seed_fixture(recorder, bucket, branch, *, count=OBJECTS):
+ require(branch in (*G01_BRANCHES, *G02_BRANCHES, *G03_BRANCHES, *G06_BRANCHES) and type(count) is int and 3 <= count <= OBJECTS, "G01/G02/G03/G04/G05/G06 unregistered fixture")
+ if branch in G06_BRANCHES:
+ require(count == OBJECTS, "G06 requires the frozen complete 257-key DS population")
+ return seed_workload(recorder, bucket, "DS", count, [SIZE])
+ recorder.call("create_bucket", {"Bucket": bucket})
+ if branch in (*G02_BRANCHES, *G03_BRANCHES):
+ recorder.call("put_bucket_versioning", {"Bucket": bucket, "VersioningConfiguration": {"Status": "Enabled"}})
+ entries = []
+ keys = [f"objects/{index:04d}.bin" for index in range(count - 1)] + [TARGET_KEY]
+ current_target = target_key(branch)
+ for key in keys:
+ versions = 3 if key == current_target and branch in (*G02_BRANCHES, *G03_BRANCHES) else 1
+ for generation in range(versions):
+ nonce = f"{bucket}/{key}/{generation}"
+ body = payload(nonce, SIZE)
+ response = recorder.call("put_object", {"Bucket": bucket, "Key": key,
+ "ContentType": "application/octet-stream", "Metadata": {"heal-fixture": "G01/G03", "nonce": nonce},
+ "Tagging": "suite=heal&fixture=G01/G03"}, body)
+ entries.append({"key": key, "versionId": public_version_id(response.get("VersionId")),
+ "isLatest": generation == versions - 1, "deleteMarker": False,
+ "payload": {"size": len(body), "sha256": hashlib.sha256(body).hexdigest()}})
+ return entries
+
+
+def operation_plan(bucket, branch, expected):
+ require(branch in (*G01_BRANCHES, *G02_BRANCHES, *G03_BRANCHES, *G06_BRANCHES), "G01/G02/G03/G04/G05/G06 unregistered foreground branch")
+ if branch in G01_BRANCHES:
+ # Three distinct serial commits; the final independent reference was
+ # written before injection, allowing a coding-index byte comparison.
+ return [{"operation": "put_object", "params": {"Bucket": bucket, "Key": TARGET_KEY,
+ "ContentType": "application/octet-stream", "Metadata": {"nonce": str(index)},
+ "Tagging": "race=overwrite"},
+ "nonce": f"{bucket}/overwrite/{index}" if index < 2 else f"{bucket}/{REFERENCE_KEY}/0"}
+ for index in range(3)]
+ current_target = target_key(branch); current_reference = reference_key(branch)
+ target = [row for row in expected if row["key"] == current_target]
+ if branch in G02_BRANCHES:
+ require(len(target) == 3 and len({row["versionId"] for row in target}) == 3, "G02 DV lacks three ACK-ordered versions")
+ return [{"operation": "put_object", "params": {"Bucket": bucket, "Key": TARGET_KEY,
+ "ContentType": "application/octet-stream", "Metadata": {"race": "versioned-put", "nonce": str(index)},
+ "Tagging": "race=versioned-put"},
+ "nonce": f"{bucket}/objects/{index:04d}.bin/0", "referenceKey": f"objects/{index:04d}.bin",
+ }
+ for index in range(3)]
+ if branch in G06_BRANCHES:
+ if branch == "copy-source":
+ return [{"operation": "copy_object", "params": {"Bucket": bucket, "Key": current_reference,
+ "CopySource": {"Bucket": bucket, "Key": current_target, "VersionId": "null"}}, "nonce": "copy-source"}]
+ if branch == "copy-destination":
+ return [{"operation": "copy_object", "params": {"Bucket": bucket, "Key": current_target,
+ "CopySource": {"Bucket": bucket, "Key": current_reference, "VersionId": "null"}}, "nonce": "copy-destination"}]
+ if branch == "replace-metadata":
+ return [{"operation": "copy_object", "params": {"Bucket": bucket, "Key": current_target,
+ "CopySource": {"Bucket": bucket, "Key": current_target, "VersionId": "null"},
+ "MetadataDirective": "REPLACE", "Metadata": {"race": "replace-metadata"},
+ "ContentType": "application/x-heal-race"}, "nonce": "replace-metadata"}]
+ return [{"operation": "put_object_tagging", "params": {"Bucket": bucket, "Key": current_target,
+ "Tagging": {"TagSet": [{"Key": "race", "Value": f"updated-{index:02d}"}]}},
+ "nonce": f"tag-{index:02d}"} for index in range(16)]
+ require(len(target) == 3 and len({row["versionId"] for row in target}) == 3
+ and target[-1]["isLatest"], "G01/G03 DV lacks three ACK-ordered versions")
+ return [{"operation": "delete_object", "params": {"Bucket": bucket, "Key": current_target,
+ "VersionId": (target[0] if branch == G03_BRANCHES[0] else target[-1])["versionId"]},
+ "seedVersionOrder": [row['versionId'] for row in target]}]
+
+
+def version_put_entries(baseline, plan, responses):
+ """Build only received serial PUT ACKs; a missing response stays unknown."""
+ require(len(plan) == 3 and 1 <= len(responses) <= 3, 'G02 ACK prefix is incomplete or excessive')
+ added = []
+ original = {row['versionId'] for row in baseline['entries'] if row['key'] == TARGET_KEY}
+ require(len(original) == 3, 'G02 lacks its three original acknowledged versions')
+ seen = set(original)
+ for index, (step, ack) in enumerate(zip(plan[:len(responses)], responses, strict=True)):
+ version = ack.get('VersionId')
+ require(isinstance(version, str) and re.fullmatch(r'[0-9a-f-]{36}', version)
+ and str(uuid.UUID(version)) == version and uuid.UUID(version).int != 0 and version not in seen
+ and isinstance(ack.get('ETag'), str) and ack['ETag'], 'G02 PUT lacks a distinct real version ACK')
+ seen.add(version)
+ key = f'objects/{index:04d}.bin'
+ reference = [row for row in baseline['entries'] if row['key'] == key and row.get('isLatest') is True
+ and row.get('deleteMarker') is False]
+ body = payload(step['nonce'], SIZE)
+ contents = {'size': len(body), 'sha256': hashlib.sha256(body).hexdigest()}
+ require(step.get('referenceKey') == key and step['nonce'] == f'{step["params"]["Bucket"]}/{key}/0'
+ and len(reference) == 1 and contents == reference[0]['payload'],
+ 'G02 new PUT bytes differ from the original independent control')
+ added.append({'key': TARGET_KEY, 'versionId': version, 'isLatest': index == len(responses) - 1,
+ 'deleteMarker': False, 'payload': contents,
+ 'properties': {'ETag': ack['ETag'], 'ContentLength': len(body),
+ 'ContentType': step['params']['ContentType'], 'Metadata': step['params']['Metadata']},
+ 'tags': [{'Key': 'race', 'Value': 'versioned-put'}]})
+ return added
+
+
+def final_inventory(baseline, branch, plan, responses):
+ expected_length = (3 if branch in (*G01_BRANCHES, *G02_BRANCHES)
+ else 16 if branch == "update-tags" else 1)
+ require(len(plan) == len(responses) == expected_length,
+ "G01/G03/G05 foreground plan/ACK sequence is incomplete")
+ result = copy.deepcopy(baseline)
+ current_target, current_reference = target_key(branch), reference_key(branch)
+ if branch in G02_BRANCHES:
+ added = version_put_entries(baseline, plan, responses)
+ for row in result['entries']:
+ if row['key'] == current_target: row['isLatest'] = False
+ result['entries'].extend(added)
+ result['acknowledgedVersionAppendOrder'] = [row['versionId'] for row in added]
+ result['entries'].sort(key=lambda row: (row['key'], row['versionId']))
+ elif branch in G01_BRANCHES:
+ entry = next(row for row in result["entries"] if row["key"] == current_target)
+ step, ack = plan[-1], responses[-1]
+ body = payload(step["nonce"], SIZE)
+ require(isinstance(ack.get("ETag"), str) and ack["ETag"]
+ and ack.get("VersionId") in {None, "null"}, "G01/G03 last PUT ACK lost its identity")
+ entry.update(payload={"size": len(body), "sha256": hashlib.sha256(body).hexdigest()},
+ tags=[{"Key": "race", "Value": "overwrite"}])
+ entry["properties"].update(ETag=ack["ETag"], ContentLength=len(body), ContentType="application/octet-stream",
+ Metadata=step["params"]["Metadata"])
+ elif branch in G06_BRANCHES:
+ entry = next(row for row in result["entries"] if row["key"] == current_target)
+ copy_result = responses[-1].get("CopyObjectResult") or {}
+ copy_etag = copy_result.get("ETag") or responses[-1].get("ETag")
+ if branch in {"copy-source", "copy-destination"}:
+ source_key, destination_key = ((current_target, current_reference) if branch == "copy-source"
+ else (current_reference, current_target))
+ source = next(row for row in result["entries"] if row["key"] == source_key)
+ destination = next(row for row in result["entries"] if row["key"] == destination_key)
+ require(isinstance(copy_etag, str) and copy_etag, "G06 COPY ACK lacks its committed ETag")
+ for field in ("payload", "properties", "tags"):
+ destination[field] = copy.deepcopy(source[field])
+ destination["properties"]["ETag"] = copy_etag
+ elif branch == "replace-metadata":
+ require(isinstance(copy_etag, str) and copy_etag, "G06 metadata replacement ACK lacks its committed ETag")
+ entry["properties"].update(Metadata=plan[0]["params"]["Metadata"], ContentType=plan[0]["params"]["ContentType"])
+ entry["properties"]["ETag"] = copy_etag
+ else:
+ entry["tags"] = copy.deepcopy(plan[-1]["params"]["Tagging"]["TagSet"])
+ else:
+ version = plan[0]["params"]["VersionId"]
+ require(responses[0].get("VersionId") == version, "G01/G03 DELETE response changed the selected VersionId")
+ order = plan[0].get('seedVersionOrder')
+ target_rows = [row for row in result['entries'] if row['key'] == current_target]
+ require(branch in G03_BRANCHES and isinstance(order, list) and len(order) == len(set(order)) == 3
+ and set(order) == {row['versionId'] for row in target_rows}
+ and all(row['deleteMarker'] is False and row['isLatest'] is (row['versionId'] == order[-1])
+ for row in target_rows)
+ and version == order[0 if branch == G03_BRANCHES[0] else -1],
+ 'G03 DELETE lost its original serial seed ACK order or selected version')
+ result["entries"] = [row for row in result["entries"]
+ if (row["key"], row["versionId"]) != (current_target, version)]
+ require(len(result["entries"]) + 1 == len(baseline["entries"]), "G01/G03 deleted a foreign/missing version")
+ latest = next(item for item in reversed(order) if item != version)
+ for row in result['entries']:
+ if row['key'] == current_target:
+ row['isLatest'] = row['versionId'] == latest
+ return result
+
+
+def object_scene(snapshot, bucket, key):
+ return {node: {**value, "files": [row for row in value["files"]
+ if row["path"].startswith(f"{bucket}/{key}/")]}
+ for node, value in snapshot.items()}
+
+
+def map_fixture(snapshot, bucket, expected):
+ live_expected = [row for row in expected if not row.get("deleteMarker")]
+ mapped = {}
+ for row in live_expected:
+ target = map_target(object_scene(snapshot, bucket, row["key"]), bucket, row["key"], "data", 1,
+ row["versionId"], latest_required=False)
+ # Frozen H1 removes the fourth physical drive's shard, regardless of
+ # shuffled coding role. H2 uses the same local slot on its first host.
+ selected = next((part for part in target["cohort"]
+ if part["node"] == sorted(snapshot)[0] and part["disk"] == 3), None)
+ require(selected is not None, "race fixture lacks its original fourth-drive member")
+ mapped[row["key"], row["versionId"]] = {**target, **selected}
+ require(len(mapped) == len(live_expected), "G01/G04 physical map omitted/duplicated an ACK")
+ return mapped
+
+
+def physical_transition(before, after, bucket, mapped, final, branch):
+ """Prove current ACK authority before allowing any GET or backup rescue."""
+ require(all(before[node]["formats"] == after[node]["formats"] for node in before),
+ "G01/G03 race changed an original disk format")
+ known = {row["key"] for row in final["entries"]}
+ require(all(any(row["path"].startswith(f"{bucket}/{key}/") for key in known)
+ for value in after.values() for row in value["files"]), "G01/G03 created an unacknowledged physical key")
+ current_target, current_reference = target_key(branch), reference_key(branch)
+ changed_key = (current_reference if branch == "copy-source" else current_target)
+ for (key, version), target in mapped.items():
+ if key == (changed_key if branch in G06_BRANCHES else current_target): continue
+ validate_repaired_physical(object_scene(before, bucket, key), object_scene(after, bucket, key), target)
+ survivors = map_fixture(after, bucket, final["entries"])
+ original_target = [value for (key, _), value in mapped.items() if key == current_target]
+ current_target_values = [value for (key, _), value in survivors.items() if key == current_target]
+ if branch in G01_BRANCHES:
+ require(len(original_target) == len(current_target_values) == 1
+ and original_target[0]["dataDir"] != current_target_values[0]["dataDir"],
+ "G01/G03 overwrite retained the old physical generation")
+ reference = next(target for (key, _), target in mapped.items() if key == current_reference)
+ require({part["index"]: (part["sha256"], part["size"]) for part in reference["cohort"]}
+ == {part["index"]: (part["sha256"], part["size"]) for part in current_target_values[0]["cohort"]},
+ "G01/G03 last ACK shard bytes/coding indices differ from the independent payload reference")
+ elif branch in G02_BRANCHES:
+ require(len(original_target) == 3 and len(current_target_values) == 6,
+ "G02 version race lost an acknowledged historical or new version")
+ old_dirs = {target["dataDir"] for target in original_target}
+ new_dirs = {target["dataDir"] for target in current_target_values} - old_dirs
+ require(len(old_dirs) == len(new_dirs) == 3 and old_dirs.isdisjoint(new_dirs),
+ "G02 versions share or resurrect a physical data directory")
+ old_versions = {target['versionId'] for target in original_target}
+ entries = [row for row in final['entries'] if row['key'] == current_target]
+ added = {row['versionId']: row for row in entries if row['versionId'] not in old_versions}
+ order = final.get('acknowledgedVersionAppendOrder', [])
+ require(len(order) == len(set(order)) == 3 and set(order) == set(added),
+ 'G02 physical transition lost the explicit serial ACK append order')
+ appended = [added[version] for version in order]
+ require(len(appended) == 3 and len({row['versionId'] for row in entries}) == len(entries) == 6,
+ 'G02 physical transition lacks all three serial new version ACKs')
+ def coding(target):
+ return {part['index']: (part['sha256'], part['size']) for part in target['cohort']}
+ for target in current_target_values:
+ if target['versionId'] in old_versions:
+ original = mapped[current_target, target['versionId']]
+ require({key: value for key, value in target['logical'].items() if key != 'isLatest'}
+ == {key: value for key, value in original['logical'].items() if key != 'isLatest'}
+ and coding(target) == coding(original), 'G02 append changed a retained historical version')
+ else:
+ index = next(i for i, row in enumerate(appended) if row['versionId'] == target['versionId'])
+ control_key = f'objects/{index:04d}.bin'
+ controls = [row for row in final['entries'] if row['key'] == control_key and row.get('isLatest') is True]
+ references = [value for (key, _), value in mapped.items() if key == control_key]
+ require(len(controls) == len(references) == 1 and appended[index]['payload'] == controls[0]['payload'],
+ 'G02 new ACK bytes lack their independent pre-fault control')
+ reference = references[0]
+ require(target['logical']['size'] == SIZE and len(target['logical']['parts']) == 1
+ and target['logical']['parts'][0]['size'] == target['logical']['parts'][0]['actualSize'] == SIZE
+ and coding(target) == coding(reference)
+ and all(target['logical']['erasure'][key] == reference['logical']['erasure'][key]
+ for key in ('data_blocks', 'parity_blocks', 'block_size', 'algorithm')),
+ 'G02 new version logical size or coding-index bytes differ from its immutable reference')
+ expected_versions = sorted((row['versionId'], row['isLatest'], row['deleteMarker']) for row in entries)
+ for value in after.values():
+ for row in value['files']:
+ if row['path'] != f'{bucket}/{current_target}/xl.meta': continue
+ versions = row['metadata']['versions']
+ require(all(type(v.get('isLatest')) is bool and type(v.get('deleted')) is bool for v in versions)
+ and sorted((public_version_id(v.get('versionId')), v['isLatest'], v['deleted']) for v in versions)
+ == expected_versions, 'G02 physical version population differs from all original and new ACKs')
+ elif branch in G06_BRANCHES:
+ target_after = next(value for (key, _), value in survivors.items() if key == changed_key)
+ target_before = next(value for (key, _), value in mapped.items() if key == changed_key)
+ if branch in {"copy-source", "copy-destination"}:
+ source_key = current_target if branch == "copy-source" else current_reference
+ source = next(value for (key, _), value in mapped.items() if key == source_key)
+ require(target_after["dataDir"] != target_before["dataDir"]
+ and {part["index"]: (part["sha256"], part["size"]) for part in target_after["cohort"]}
+ == {part["index"]: (part["sha256"], part["size"]) for part in source["cohort"]},
+ "G06 copy did not publish a distinct target generation with source shard bytes")
+ else:
+ # The frozen metadata/tag oracle permits a new current directory;
+ # every coding index must still contain the original payload bytes.
+ require({part["index"]: (part["sha256"], part["size"]) for part in target_after["cohort"]}
+ == {part["index"]: (part["sha256"], part["size"]) for part in target_before["cohort"]},
+ "G06 metadata/tag update changed the original coding bytes")
+ else:
+ remaining = {(row["key"], row["versionId"]) for row in final["entries"]}
+ removed = [value for pair, value in mapped.items() if pair not in remaining]
+ require(len(removed) == 1, "G01/G03 delete transition lacks one exact deleted version")
+ for target in current_target_values:
+ original = mapped[current_target, target["versionId"]]
+ require({name: value for name, value in target['logical'].items() if name != 'isLatest'}
+ == {name: value for name, value in original['logical'].items() if name != 'isLatest'}
+ and [(row["index"], row["sha256"], row["size"]) for row in target["cohort"]]
+ == [(row["index"], row["sha256"], row["size"]) for row in original["cohort"]],
+ "G01/G03 delete changed another retained ACK generation")
+ wanted = sorted((row['versionId'], row['isLatest'], row['deleteMarker'])
+ for row in final['entries'] if row['key'] == current_target)
+ indexed = files(after)
+ for node, value in after.items():
+ for drive in value['formats']:
+ versions = indexed[node, drive['disk'], f'{bucket}/{current_target}/xl.meta']['metadata']['versions']
+ require(all(type(row.get('isLatest')) is bool and type(row.get('deleted')) is bool for row in versions)
+ and sorted((row['versionId'], row['isLatest'], row['deleted']) for row in versions) == wanted,
+ 'G03 physical version/latest population differs from retained serial ACKs')
+ inspected_keys = {current_target, changed_key} if branch in G06_BRANCHES else {current_target}
+ for value in after.values():
+ for row in value["files"]:
+ key = next((key for key in inspected_keys if row["path"].startswith(f"{bucket}/{key}/")), None)
+ if key is None: continue
+ prefix = f"{bucket}/{key}/"
+ current_dirs = {target["dataDir"] for (name, _), target in survivors.items() if name == key}
+ suffix = row["path"][len(prefix):]
+ require(suffix == "xl.meta" or suffix.split("/")[0] in current_dirs,
+ "G01/G03 resurrected an old/deleted data directory or left an unbound target leaf")
+ if branch in G02_BRANCHES and suffix != 'xl.meta':
+ require(re.fullmatch(r'[0-9a-f-]{36}/(?:part\.1|part\.1\.integrity\.[0-9a-f-]{36}|xl\.meta\.bkp)', suffix),
+ 'G02 retained an unacknowledged physical target leaf')
+ return survivors, {"allControlShardsRestored": True, "lastACKGenerationVerified": True,
+ "physicalProofBeforeGETOrRescue": True, "oldVersionResurrected": False}
+
+
+def terminal_proof(status, bucket, before, final):
+ outcome = status.get("outcome") or {}
+ require(status.get("summary") == "finished" and outcome.get("execution", {}).get("state") == "completed"
+ and outcome.get("coverage") == "complete", "G01/G03 task lacks complete successful terminal coverage")
+ old = {(row["key"], row["versionId"]) for row in before["entries"]}
+ new = {(row["key"], row["versionId"]) for row in final["entries"]}
+ counters = outcome.get("counters") or {}
+ require(all(type(counters.get(key)) is int and counters[key] >= 0
+ for key in ("processed", "healed", "unchanged", "failed", "unknown", "skipped", "attemptFailures"))
+ and all(counters[key] == 0 for key in ("failed", "unknown", "skipped", "attemptFailures"))
+ and counters.get("overflowed") is False and counters["processed"] == counters["healed"] + counters["unchanged"]
+ and len(old & new) <= counters["processed"] <= len(old | new),
+ "G01/G03 terminal responsibility exceeds ACK bounds or hides unknown/failed work")
+ receipts = outcome.get("objects")
+ require(isinstance(receipts, list) and 0 < len(receipts) <= 128
+ and outcome.get("objectsTruncated") is (counters["processed"] > 128),
+ "G01/G03 confused truncated diagnostics with its independent complete ACK manifest")
+ seen, incarnation = set(), None
+ for receipt in receipts:
+ identity = receipt.get("identity") or {}
+ pair = identity.get("object"), public_version_id(identity.get("versionId"))
+ require(identity.get("bucket") == bucket and pair in old | new and pair not in seen
+ and type(identity.get("poolIndex")) is int and identity["poolIndex"] == 0
+ and type(identity.get("setIndex")) is int and identity["setIndex"] == 0
+ and isinstance(identity.get("bucketIncarnationId"), str) and identity["bucketIncarnationId"]
+ and receipt.get("disposition", {}).get("state") in {"repaired", "verified_healthy"},
+ "G01/G03 diagnostics changed version/incarnation/scope or claimed unknown/false health")
+ incarnation = incarnation or identity["bucketIncarnationId"]
+ require(incarnation == identity["bucketIncarnationId"], "G01/G03 diagnostics mix incarnations")
+ seen.add(pair)
+ return {"minimumACKResponsibilities": len(old & new), "maximumACKResponsibilities": len(old | new),
+ "counters": counters, "exactRaceEnumerationClaimed": False}
+
+
+def observe(probe, fleet, path, token, reference=REFERENCE):
+ value = fleet.record("original-token-status", probe.admin(path, [("clientToken", token)], b""))
+ settings(value, reference)
+ return value
+
+
+def verify_endpoint_resolution(fleet):
+ """Fail before allocation/start if an existing fleet alias cannot resolve."""
+ request = {'runId': fleet.run_id, 'action': 'endpoint-resolution'}
+ runner = endpoint_resolution(request)
+ with ThreadPoolExecutor(max_workers=len(fleet.nodes)) as pool:
+ peers = dict(zip(fleet.nodes, pool.map(lambda node: fleet.remote(node, 'endpoint-resolution'), fleet.nodes), strict=True))
+ observed = fleet.record('endpoint-resolution', {'runner': runner, 'peers': peers})
+ for entry in [observed['runner'], *observed['peers'].values()]:
+ require(entry.get('readOnly') is True and entry.get('mutated') is False
+ and entry.get('resolved') is True,
+ 'Heal endpoint name resolution failed before allocation/start; inspect endpoint-resolution evidence')
+
+
+@contextmanager
+def foreground_status_session(probe, fleet, branch, path, token):
+ if branch not in G06_BRANCHES:
+ yield
+ return
+ with probe.status_session(token, path) as transport:
+ try:
+ yield
+ finally:
+ fleet.record('G06-status-transport', transport)
+
+
+def wait_running(probe, fleet, path, token, total, reference=REFERENCE):
+ deadline = time.monotonic() + 30
+ while True:
+ observed = observe(probe, fleet, path, token, reference)
+ value = observed["response"]
+ processed = (value.get("outcome") or {}).get("counters", {}).get("processed")
+ if value["summary"] == "finished":
+ raise WindowNotObserved("G01/G03 task finished before its partial-progress race window")
+ if type(processed) is int and 0 < processed < total: return observed
+ if time.monotonic() >= deadline:
+ raise WindowNotObserved("G01/G03 nonzero partial progress was not observed within 30 seconds")
+ time.sleep(.05)
+
+
+def wait_terminal(probe, fleet, path, token, reference=REFERENCE):
+ deadline = time.monotonic() + 240
+ while True:
+ current = observe(probe, fleet, path, token, reference)
+ if current["response"]["summary"] == "finished": return current["response"]
+ require(time.monotonic() < deadline, "G01/G03 original task exceeded its terminal budget")
+ time.sleep(.2)
+
+
+def wait_version_window(probe, fleet, path, token, reference, before, targets, bucket):
+ """Frozen G02 window: repaired controls and at least 32 physical targets pending."""
+ deadline = time.monotonic() + 120
+ while time.monotonic() < deadline:
+ observed = observe(probe, fleet, path, token, reference)
+ snapshot = fleet.snapshot('G02-live-partial-physical-window')
+ complete, pending = [], []
+ for target in targets:
+ try:
+ validate_repaired_physical(object_scene(before, bucket, target['key']),
+ object_scene(snapshot, bucket, target['key']), target)
+ except ValueError:
+ pending.append(target['key'])
+ else:
+ complete.append(target['key'])
+ fleet.record('G02-partial-window', {'status': observed, 'completeKeys': complete, 'pendingKeys': pending})
+ value = observed['response']; processed = (value.get('outcome') or {}).get('counters', {}).get('processed')
+ if value['summary'] == 'running' and type(processed) is int and processed > 0 and complete and len(pending) >= 32:
+ if TARGET_KEY not in pending:
+ raise WindowNotObserved('G02 selected historical target repaired before the source partial window')
+ return snapshot
+ if not pending:
+ raise WindowNotObserved('G02 all physical targets repaired before the source partial window')
+ require(value['summary'] == 'running', 'G02 original root stopped before physical partial repair')
+ time.sleep(.25)
+ raise WindowNotObserved('G02 nonzero physical partial window was not observed within its source 120-second budget')
+
+
+def foreground(recorder, plan, *, committed=None):
+ responses = []
+ for step in plan:
+ require(step['operation'] not in {'create_multipart_upload', 'upload_part', 'complete_multipart_upload',
+ 'abort_multipart_upload'}, 'G05 requires its same-key ACK model and schedule')
+ body = payload(step['nonce'], SIZE) if step['operation'] == 'put_object' else None
+ response = recorder.call(step['operation'], copy.deepcopy(step['params']), body)
+ responses.append(response)
+ if committed is not None:
+ committed(responses)
+ return responses, {}
+
+
+def read_versions(recorder, ledger, lock, done):
+ """Read each ACK-known version, including a final round after the writer drains."""
+ proofs = []
+ deadline = time.monotonic() + 60
+ for cycle in range(16):
+ final_round = done.is_set()
+ with lock:
+ entries = copy.deepcopy(ledger['entries'])
+ for entry in entries:
+ if time.monotonic() >= deadline:
+ return proofs
+ params = {'Bucket': ledger['bucket'], 'Key': entry['key'], 'VersionId': entry['versionId']}
+ recorder.call('get_object', params, identity={'readCycle': cycle})
+ ack = json.loads((recorder.output / f'{recorder.sequence:06d}-ack.json').read_text())
+ observed = ack['observation']; response = observed.get('response') or {}
+ require(observed.get('acknowledged') is True and observed.get('resultUnknown') is False
+ and not observed.get('clientCleanupError')
+ and response.get('ResponseMetadata', {}).get('HTTPStatusCode') == 200
+ and observed.get('params') == params and response.get('VersionId') == entry['versionId']
+ and observed.get('payload') == entry['payload']
+ and {key: response[key] for key in entry['properties'] if key in response} == entry['properties'],
+ 'G02 historical GET did not preserve its exact acknowledged version bytes/properties')
+ proofs.append(ack)
+ if final_round:
+ break
+ return proofs
+
+
+def read_unversioned(recorder, params, done):
+ """Retain every actual G01 GET request/ACK independently of the PUT worker."""
+ proofs = []
+ deadline = time.monotonic() + 90
+ for cycle in range(512):
+ if done.is_set() or time.monotonic() >= deadline:
+ break
+ recorder.call('get_object', params, identity={'readCycle': cycle})
+ proofs.append(json.loads((recorder.output / f'{recorder.sequence:06d}-ack.json').read_text()))
+ time.sleep(.01)
+ return proofs
+
+
+def verify_unversioned_reads(readers, baseline, plan, responses):
+ """A concurrent unversioned GET may see only one complete ACK-known generation."""
+ require(baseline is not None and len(plan) == len(responses) == 3 and len(readers) <= 512,
+ 'G01 reader lacks its complete bounded original/foreground ACK model')
+ params = {'Bucket': plan[0]['params']['Bucket'], 'Key': plan[0]['params']['Key']}
+ originals = [entry for entry in baseline['entries'] if entry['key'] == params['Key']]
+ require(len(originals) == 1 and originals[0]['versionId'] == 'null'
+ and originals[0]['deleteMarker'] is False and originals[0]['isLatest'] is True,
+ 'G01 reader lost its exact original unversioned generation')
+ known = [{'payload': originals[0]['payload'], 'properties': {
+ key: originals[0]['properties'][key] for key in ('ETag', 'ContentLength', 'ContentType', 'Metadata')}}]
+ for step, response in zip(plan, responses, strict=True):
+ body = payload(step['nonce'], SIZE)
+ require(step['operation'] == 'put_object' and step['params']['Bucket'] == params['Bucket']
+ and step['params']['Key'] == params['Key'] and response.get('ETag')
+ and response.get('VersionId') in {None, 'null'}, 'G01 reader has a foreign or unresolved PUT generation')
+ known.append({'payload': {'size': len(body), 'sha256': hashlib.sha256(body).hexdigest()},
+ 'properties': {'ETag': response['ETag'], 'ContentLength': len(body),
+ 'ContentType': step['params']['ContentType'], 'Metadata': step['params']['Metadata']}})
+ for ack in readers:
+ observed = ack.get('observation') or {}; response = observed.get('response') or {}
+ require(ack.get('operationCheck') == 'PASS' and observed.get('operation') == 'get_object'
+ and observed.get('params') == params and observed.get('acknowledged') is True
+ and observed.get('resultUnknown') is False and not observed.get('clientCleanupError')
+ and response.get('ResponseMetadata', {}).get('HTTPStatusCode') == 200
+ and response.get('VersionId') in {None, 'null'}
+ and any(observed.get('payload') == entry['payload']
+ and all(response.get(key) == value for key, value in entry['properties'].items()) for entry in known),
+ 'G01 read-repair GET lost its full ACK-known body or mixed generation properties')
+ return bool(readers)
+
+
+def race(recorder, plan, probe, fleet, path, token, reference=REFERENCE, *, baseline=None):
+ samples = []
+ dispatch_times = []
+ dispatch = recorder.probe.dispatched if plan and plan[0].get('branch') in G06_BRANCHES else None
+ # Only the worker mutates Recorder state. All Fleet.record writes run on
+ # the controller, so evidence sequence allocation never races.
+ readers = []
+ read_branch = bool(plan and plan[0].get("branch") in {"overwrite-vs-read-repair", "read-all-versions-during-heal"})
+ version_reads = bool(plan and plan[0].get('branch') == G02_BRANCHES[1])
+ version_race = bool(plan and plan[0].get('branch') in G02_BRANCHES)
+ done = threading.Event(); lock = threading.Lock()
+ ledger = {'bucket': plan[0]['params']['Bucket'], 'entries': []}
+ read_recorder = None
+ if read_branch:
+ require(baseline is not None, 'concurrent reader needs its original complete S3 baseline')
+ if version_reads:
+ ledger['entries'] = [copy.deepcopy(row) for row in baseline['entries'] if row['key'] == TARGET_KEY]
+ require(len(ledger['entries']) == 3, 'G02 historical reader lacks three original ACK-known versions')
+ if read_branch:
+ read_recorder = Recorder(probe, recorder.output.parent / (
+ 'historical-read-ACKs' if version_reads else 'read-repair-ACKs'))
+ def committed(responses):
+ added = version_put_entries(baseline, plan, responses)
+ with lock:
+ originals = [copy.deepcopy(row) for row in baseline['entries'] if row['key'] == TARGET_KEY]
+ for entry in originals: entry['isLatest'] = False
+ ledger['entries'] = originals + added
+ def writing():
+ try:
+ return foreground(recorder, plan, committed=committed if version_reads else None)
+ finally:
+ done.set()
+ with ThreadPoolExecutor(max_workers=2 if read_branch else 1) as pool:
+ worker = pool.submit(writing)
+ if version_reads:
+ read_future = pool.submit(read_versions, read_recorder, ledger, lock, done)
+ elif read_branch:
+ params = {'Bucket': plan[0]['params']['Bucket'], 'Key': plan[0]['params']['Key']}
+ read_future = pool.submit(read_unversioned, read_recorder, params, done)
+ else:
+ read_future = None
+ deadline = time.monotonic() + (90 if version_race else 120)
+ try:
+ while (not worker.done() or read_branch and read_future is not None and not read_future.done()) and time.monotonic() < deadline:
+ if dispatch is not None:
+ try:
+ dispatch_times.append(dispatch.get(timeout=.05))
+ except Empty:
+ continue
+ samples.append(observe(probe, fleet, path, token, reference))
+ if dispatch is None:
+ time.sleep(.025 if version_race else .01)
+ except BaseException:
+ recorder.mutations.poison("status observation failed; drain in-flight mutations only")
+ raise
+ if not worker.done(): recorder.mutations.poison("foreground deadline; drain only")
+ responses, runtime = worker.result(timeout=60)
+ if plan and runtime:
+ plan[0]["runtime"] = runtime
+ if read_future is not None:
+ value = read_future.result(timeout=60)
+ readers = value
+ acks = [json.loads(path.read_text()) for path in sorted(recorder.output.glob("*-ack.json"))]
+ require(len(acks) == len(plan) and all(row.get("operationCheck") == "PASS" for row in acks),
+ "G01/G03 lacks every durable foreground ACK")
+ overlaps = [any(sample_overlap(ack, sample, path, token, reference) for sample in samples) for ack in acks]
+ # The frozen controllers require one nested sample per workload role,
+ # while every serial mutation still needs its own durable ACK.
+ tag_updates = bool(plan and plan[0].get('branch') == 'update-tags')
+ overlap_complete = any(overlaps)
+ if dispatch is not None:
+ fleet.record('G06-request-dispatch-observation', {'requestSendMonotonicNs': dispatch_times,
+ 'statusSamples': len(samples), 'notificationScope': 'botocore before-send.s3; inside SDK call',
+ 'SDKCallDelayed': False, 'publicOverlapProved': False, 'nativeFenceProved': False,
+ 'canonicalCasePassClaimed': False})
+ read_complete = True
+ reader_overlap = True
+ if version_reads:
+ required = {row['versionId'] for row in ledger['entries']}
+ actual = {row['observation']['params']['VersionId'] for row in readers}
+ read_complete = len(required) == 6 and actual == required
+ reader_overlap = any(sample_overlap(ack, sample, path, token, reference) for ack in readers for sample in samples)
+ fleet.record('G02-historical-read-coverage', {'requiredVersionIds': sorted(required),
+ 'observedVersionIds': sorted(actual), 'complete': read_complete, 'readerCallOverlapObserved': reader_overlap,
+ 'allReadsBoundToDurableACKs': True, 'nativeFenceProved': False, 'canonicalCasePassClaimed': False})
+ elif read_branch:
+ read_complete = verify_unversioned_reads(readers, baseline, plan, responses)
+ reader_overlap = any(sample_overlap(ack, sample, path, token, reference) for ack in readers for sample in samples)
+ fleet.record('G01-read-repair-coverage', {'readCalls': len(readers), 'maxReadCalls': 512,
+ 'complete': read_complete, 'readerCallOverlapObserved': reader_overlap,
+ 'allReadsBoundToDurableACKs': True, 'fullACKKnownBodiesAndPropertiesVerified': read_complete,
+ 'nativeFenceProved': False, 'canonicalCasePassClaimed': False})
+ fleet.record("read-repair-overlap-proof", {"branch": plan[0].get("branch") if plan else None,
+ "readCalls": len(readers), "allReadsObserved": bool(readers) if read_branch else True,
+ "nativeFenceProved": False})
+ fleet.record("public-call-overlap-proof", {"everyMutationOverlapped": all(overlaps), "mutations": overlaps,
+ "writerCallOverlapObserved": overlap_complete,
+ "readerCallOverlapObserved": reader_overlap if read_branch else None,
+ "requiredCallOverlapObserved": overlap_complete and reader_overlap,
+ "allRequiredReadContentVerified": read_complete,
+ "overlapRequirement": "at-least-one-of-frozen-16-tag-updates" if tag_updates else "at-least-one-per-required-workload-role",
+ "nativeFenceProved": False, "serverCommitOverlapProved": False})
+ return responses, overlap_complete and read_complete and reader_overlap
+
+
+def run_branch(args, topology, nodes, branch):
+ if args.case_id == 'heal.race-markers-and-batch-delete':
+ try:
+ from .version_race_runner import run_branch as run_marker_branch
+ except ImportError:
+ from version_race_runner import run_branch as run_marker_branch
+ source_contract(json.loads(args.candidate.read_text()), args.case_id)
+ return run_marker_branch(args, topology, nodes, branch)
+
+ if args.case_id == 'heal.race-multipart-complete-abort':
+ # Lazy import keeps the generic controller and G05's model acyclic.
+ try:
+ from .mpu_race_runner import run_branch as run_mpu_branch
+ except ImportError:
+ from mpu_race_runner import run_branch as run_mpu_branch
+ source_contract(json.loads(args.candidate.read_text()), args.case_id)
+ return run_mpu_branch(args, topology, nodes, branch)
+ directory = args.evidence / topology / branch
+ directory.mkdir(parents=True, exist_ok=False)
+ run_id = f"gh-{os.environ['GITHUB_RUN_ID']}-{os.environ.get('GITHUB_RUN_ATTEMPT', '1')}-{topology.lower()}"
+ fleet = Fleet(nodes, directory, run_id, args.tool_directory, runtime_profile="manual")
+ candidate = json.loads(args.candidate.read_text())
+ reference = source_contract(candidate, args.case_id)
+ inspector = fleet.record("inspector-build", verify_inspector(args.tool_directory, candidate))
+ bucket = "rv-heal-" + hashlib.sha256(run_id.encode()).hexdigest()[:16]
+ access, secret = secrets.token_hex(16), secrets.token_hex(24)
+ os.environ.update(HEAL_ACCESS_KEY=access, HEAL_SECRET_KEY=secret)
+ binding = {"schemaVersion": 1, "runId": run_id, "jumpHostOnly": True, "exclusiveHealService": True,
+ "leaseExpiresAt": (datetime.now(timezone.utc) + timedelta(minutes=45)).isoformat(), "bucketPrefix": bucket,
+ "endpoints": {f"node{index}": f"http://rustfs-node{index}:9000" for index in range(1, len(nodes) + 1)},
+ "defaultEndpoint": "node1", "candidateCommit": candidate["source_sha"], "harnessCommit": args.testing_commit,
+ "candidatePackageSha256": candidate["package_sha256"], "candidateBinarySha256": args.binary_sha,
+ "rcBinarySha256": hashlib.sha256(args.rc_bin.read_bytes()).hexdigest(),
+ "maxInventoryObjects": 512, "maxInventoryBytes": 512 * 1024**2}
+ fleet.record("binding", binding)
+ current_target_key, current_reference_key = target_key(branch), reference_key(branch)
+ fleet.record("source-contract", {**reference, "branch": branch, "objectCount": OBJECTS, "objectSize": SIZE,
+ "requiredPUTs": 3 if branch in (*G01_BRANCHES, *G02_BRANCHES) else 0, "serverCommitOverlapProved": False})
+ probe = KeepAliveStatusProbe(binding) if branch in G06_BRANCHES else Probe(binding)
+ active, recorder, writer = [], None, None
+ result = {"topology": topology, "branch": branch,
+ "fixture": "DL" if branch in G01_BRANCHES else "DS" if branch in G06_BRANCHES else "DV",
+ "verdict": "RUNNING", "cleanup": "PENDING", "canonicalCasePassClaimed": False}
+ changed_target_registered, target_authority = None, False
+ try:
+ verify_endpoint_resolution(fleet)
+ fleet.deploy(args.binary_sha, bucket, access, secret)
+ fleet.record("topology-proof", validate_topology(fleet.snapshot("deployed-topology"), topology))
+ recorder = RaceRecorder(probe, directory / "seed-ACKs",
+ versioned=branch in (*G02_BRANCHES, *G03_BRANCHES))
+ expected = seed_fixture(recorder, bucket, branch)
+ baseline = fleet.record("complete-baseline-S3", probe.inventory(
+ bucket, unversioned_current_tags=branch in (*G01_BRANCHES, *G06_BRANCHES)))
+ verify_seed(expected, baseline)
+ plan = operation_plan(bucket, branch, expected)
+ for step in plan:
+ step["branch"] = branch
+ fleet.record("frozen-foreground-plan", plan)
+ fleet.stop()
+ before = fleet.snapshot("complete-baseline-physical", quiescent=True)
+ mapped = map_fixture(before, bucket, expected)
+ verify_part_manifest(fleet, list(mapped.values()), "baseline-streaming", count=len(mapped))
+ target_version = (next(entry for entry in expected if entry["key"] == current_target_key and entry["isLatest"])
+ if branch == G03_BRANCHES[1]
+ else next(entry for entry in expected if entry["key"] == current_target_key))
+ targets = [mapped[row["key"], row["versionId"]] for row in expected
+ if row["key"] != current_target_key or row["versionId"] == target_version["versionId"]]
+ require(len(targets) == OBJECTS, "G01/G03 fault plan omitted a key or changed its source dimension")
+ receipts = []
+ case_code = select(args.case_id)[0]["code"]
+ for index, target in enumerate(targets):
+ params = {"disk": target["disk"], "faultId": bounded_fault_id(case_code, branch, index)}
+ fleet.faults.append((target["node"], params))
+ if target["key"] == current_target_key: changed_target_registered = target["node"], params
+ receipts.append(fleet.record("exact-missing-part", {"node": target["node"], **fleet.remote(
+ target["node"], "apply-fault", **params, relativePath=target["relativePath"],
+ expectedSha256=target["sha256"], kind="missing")}))
+ faulted = fleet.snapshot("complete-fault-physical", quiescent=True)
+ fleet.record("fault-batch-proof", validate_missing_batch(before, faulted, targets, receipts))
+ verify_endpoint_resolution(fleet)
+ fleet.start()
+ set_race_io_cap(fleet, args.case_id, branch, True, nodes=sorted({target['node'] for target in targets}))
+ require(physical_content(faulted) == physical_content(fleet.snapshot("attribution-before-START")),
+ "NOT_OBSERVED: G01/G03 missing shards changed before manual admission")
+ path = "/rustfs/admin/v3/heal/" + quote(bucket, safe="")
+ fleet.record("prepared-Admin-request", {"path": path, "query": [], "options": reference["requested"]})
+ start = fleet.record("original-Admin-start", probe.admin(path, [], json.dumps(reference["requested"]).encode()))
+ token = start.get("response", {}).get("clientToken") if isinstance(start.get("response"), dict) else None
+ require(start.get("httpStatus") == 200 and isinstance(token, str) and token, "G01/G03 admission lacks an original token")
+ active.append((path, token))
+ with foreground_status_session(probe, fleet, branch, path, token):
+ if branch in G02_BRANCHES:
+ witness = wait_version_window(probe, fleet, path, token, reference, before, targets, bucket)
+ else:
+ wait_running(probe, fleet, path, token, len(expected), reference)
+ witness = fleet.snapshot("target-missing-before-foreground")
+ target = next(target for target in targets if target["key"] == current_target_key)
+ if (target["node"], target["disk"], target["relativePath"]) in files(witness):
+ raise WindowNotObserved("G01/G03 old target already repaired before foreground mutations")
+ writer_probe = DispatchProbe(binding) if branch in G06_BRANCHES else probe
+ writer = RaceRecorder(writer_probe, directory / "foreground-ACKs",
+ versioned=branch in (*G02_BRANCHES, *G03_BRANCHES))
+ writer.seen.update((row["key"], row["versionId"]) for row in expected)
+ responses, overlapped = race(writer, plan, probe, fleet, path, token, reference, baseline=baseline)
+ set_race_io_cap(fleet, args.case_id, branch, False, nodes=sorted({target['node'] for target in targets}))
+ final = fleet.record("ACK-derived-final-manifest", final_inventory(baseline, branch, plan, responses))
+ terminal = wait_terminal(probe, fleet, path, token, reference)
+ active.remove((path, token))
+ for _ in range(2):
+ repeated = observe(probe, fleet, path, token, reference)["response"]
+ require(canonical(repeated.get("outcome")) == canonical(terminal.get("outcome"))
+ and repeated["summary"] == terminal["summary"], "G01/G03 terminal responsibility changed on repetition")
+ # Collect physical evidence even if terminal counters fail. Old target
+ # bytes are never rescued over a successful overwrite or exact delete.
+ fleet.stop()
+ after = fleet.snapshot("product-physical-before-GET-or-rescue", quiescent=True)
+ survivors, physical = physical_transition(before, after, bucket, mapped, final, branch)
+ if branch in G02_BRANCHES:
+ backups = {node: fleet.remote(node, 'version-race-backups', bucket=bucket,
+ expectedBinarySha256=args.binary_sha, expectedInspectorSha256=inspector['binarySha256']) for node in nodes}
+ fleet.record('G02-stopped-backup-decoding', backups)
+ original_order = [row['versionId'] for row in expected if row['key'] == current_target_key]
+ fleet.record('G02-serial-ACK-backup-prefix-proof', verify_version_backups(
+ before, after, backups, nodes, bucket, current_target_key, original_order,
+ final['acknowledgedVersionAppendOrder'], args.binary_sha, inspector['binarySha256']))
+ fleet.record("last-ACK-physical-proof", physical)
+ verify_part_manifest(fleet, list(survivors.values()), "final-streaming", count=len(survivors))
+ target_authority = True
+ # Every missing-part registration is now superseded by product output;
+ # leaving it in Fleet.faults would make cleanup attempt a blind backup
+ # rescue over the new ACK generation.
+ fleet.faults.clear()
+ fleet.record("obsolete-target-fault-retired", {"fault": changed_target_registered,
+ "reason": "complete last-ACK physical authority proves the original generation obsolete",
+ "oldBytesRestored": False, "backupRetainedUntilCleanup": True})
+ verify_endpoint_resolution(fleet)
+ fleet.start()
+ for endpoint in binding["endpoints"]:
+ current = fleet.record("all-peer-final-S3", {"endpoint": endpoint, **Probe(binding, endpoint).inventory(
+ bucket, unversioned_current_tags=branch in (*G01_BRANCHES, *G06_BRANCHES))})
+ compare_inventories(final, current)
+ if branch in G03_BRANCHES:
+ negative = fleet.record("all-peer-deleted-version-GET", Probe(binding, endpoint).s3("get_object", plan[0]["params"]))
+ response = negative.get("response") or {}
+ require(negative.get("acknowledged") is False and negative.get("resultUnknown") is False
+ and response.get("ResponseMetadata", {}).get("HTTPStatusCode") == 404
+ and response.get("Error", {}).get("Code") == "NoSuchVersion",
+ "G01/G03 deleted exact VersionId revived or failed for another reason")
+ fleet.stop()
+ require(physical_content(after) == physical_content(fleet.snapshot("physical-after-S3", quiescent=True)),
+ "G01/G03 final GET changed the already-proven physical result")
+ result["terminal"] = terminal_proof(terminal, bucket, baseline, final)
+ if not overlapped: raise WindowNotObserved("race did not observe a nested SDK call for every required workload role")
+ result.update(verdict="PASS", physical=physical, publicCallOverlapObserved=True,
+ nativeFenceProved=False, serverCommitOverlapProved=False)
+ except (Exception, KeyboardInterrupt) as exc:
+ result.update(verdict="NOT_OBSERVED" if isinstance(exc, WindowNotObserved) else "FAIL",
+ failure={"type": type(exc).__name__, "message": str(exc)})
+ try:
+ fleet.stop()
+ fleet.snapshot("retained-failure-scene", quiescent=True)
+ except Exception as capture: fleet.record("failure-scene-error", {"error": str(capture)})
+ finally:
+ for path, token in active:
+ try: fleet.record("cancel-original-token", probe.admin(path, [("clientToken", token), ("forceStop", "true")], b""))
+ except Exception as exc: fleet.record("cancel-error", {"error": str(exc)})
+ unresolved = any(item is not None and item.pending is not None for item in (recorder, writer))
+ mutated = writer is not None and writer.sequence > 0
+ if unresolved or mutated and not target_authority:
+ try: fleet.stop()
+ except Exception as exc: fleet.record("retain-stop-error", {"error": str(exc)})
+ fleet.record("last-ACK-rescue-required", {"state": "DIRTY", "namespacesRetained": True,
+ "unresolvedMutation": unresolved, "lastACKPhysicalAuthorityVerified": target_authority,
+ "originalGenerationBlindRestorationProhibited": True})
+ result["cleanup"] = "FAIL"
+ else:
+ cleanup_ok = fleet.cleanup()
+ result["cleanup"] = "FAIL" if result.get("cleanup") == "FAIL" or not cleanup_ok else "PASS"
+ fleet.record("branch-result", result)
+ return result
+
+
+def native_blocked(candidate, topology, case_id):
+ require(topology in {"H1", "H2"}, "G01/G03 native prerequisite lost its source topology")
+ return {"caseId": case_id, "topology": topology, "branch": CASE_BRANCHES[case_id][-1], "verdict": "BLOCKED", "cleanup": "PASS",
+ "candidateCommit": candidate["source_sha"], "canonicalCasePassClaimed": False,
+ "reason": REFERENCE["nativeFence"], "nativeFenceProved": False, "fleetExecutionClaimed": False}
+
+
+def complete_results(results, case_id):
+ branches = CASE_BRANCHES[case_id]
+ wanted = {(topology, branch) for topology in ("H1", "H2") for branch in branches}
+ actual = {(row["topology"], row["branch"]) for row in results}
+ require(len(actual) == len(results) and actual <= wanted, "G01/G03 duplicate or foreign topology/branch")
+ return actual == wanted and all(row["verdict"] == row["cleanup"] == "PASS" for row in results) and all(
+ row.get("nativeFenceProved") is True for row in results if row["branch"] == branches[-1])
+
+
+def main():
+ if sys.argv[1:2] == ['preflight']:
+ parser = argparse.ArgumentParser(description='Read-only native admission before fleet bootstrap')
+ parser.add_argument('--candidate', type=Path, required=True)
+ parser.add_argument('--case-id', choices=list(CASE_BRANCHES), required=True)
+ parser.add_argument('--testing-commit', required=True)
+ args = parser.parse_args(sys.argv[2:])
+ candidate = json.loads(args.candidate.read_text())
+ proof = native_preflight(candidate, args.testing_commit, args.case_id,
+ os.environ.get('VALIDATION_HEAL_NATIVE_DIRECTORY'))
+ print(json.dumps({'nativeProof': proof, 'fleetMutationRequested': False, 'canonicalCasePassClaimed': False}))
+ return
+ def interrupted(signum, frame): raise KeyboardInterrupt(f"G01/G03 interrupted by signal {signum}")
+ signal.signal(signal.SIGTERM, interrupted)
+ parser = argparse.ArgumentParser(description=__doc__)
+ for name in ("candidate", "rc-bin", "evidence", "tool-directory"):
+ parser.add_argument("--" + name, type=Path, required=True)
+ parser.add_argument("--case-id", choices=list(CASE_BRANCHES), required=True)
+ parser.add_argument("--testing-commit", required=True)
+ parser.add_argument("--binary-sha", required=True)
+ args = parser.parse_args()
+ case_id = args.case_id; branches = CASE_BRANCHES[case_id]
+ require(select(case_id)[0]["variants"] == list(branches), "G01/G03 executor omitted a source branch")
+ require(re.fullmatch(r"[0-9a-f]{64}", args.binary_sha) and re.fullmatch(r"[0-9a-f]{40}", args.testing_commit)
+ and re.fullmatch(r"[1-9][0-9]*", os.environ.get("GITHUB_RUN_ID", "")), "G01/G03 immutable identities required")
+ nodes = os.environ.get("RUSTFS_NODES", "vm000 vm001 vm002 vm003").split()
+ require(len(nodes) >= 4 and len(nodes) == len(set(nodes))
+ and all(re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", node) for node in nodes),
+ "G01/G03 requires four distinct current Heal nodes")
+ candidate = json.loads(args.candidate.read_text())
+ native = native_preflight(candidate, args.testing_commit, case_id,
+ os.environ.get('VALIDATION_HEAL_NATIVE_DIRECTORY'))
+ if native is None:
+ results = [native_blocked(candidate, topology, case_id) for topology in ("H1", "H2")]
+ else:
+ save(args.evidence / 'independent-native-race-subset.json', native)
+ results = [{'caseId': case_id, 'topology': topology, 'branch': branches[-1], 'verdict': 'PASS',
+ 'cleanup': 'PASS', 'candidateCommit': candidate['source_sha'], 'nativeFenceProved': True,
+ 'nativeProof': native, 'nativePhysicalShapes': ['4/2', '16/4'],
+ 'distributedFleetExecutionClaimed': False, 'fleetExecutionClaimed': False,
+ 'canonicalCasePassClaimed': False} for topology in ('H1', 'H2')]
+ try:
+ stop = False
+ for topology in ("H1", "H2"):
+ for branch in branches[:-1]:
+ row = run_branch(args, topology, nodes[:1] if topology == "H1" else nodes[:4], branch)
+ row["caseId"] = case_id
+ results.append(row)
+ if row["cleanup"] != "PASS": stop = True; break
+ if stop: break
+ finally:
+ complete = complete_results(results, case_id)
+ save(args.evidence / "healthy-summary.json", {"caseId": case_id, "results": results, "complete": complete,
+ "cleanup": "PASS" if all(row["cleanup"] == "PASS" for row in results) else "FAIL",
+ "canonicalCasePassClaimed": False, "nativePrerequisitesBlocked": native is None})
+ require(complete, "race coverage or native prerequisites remain incomplete; every H1/H2 branch stays in the denominator")
+
+
+if __name__ == "__main__":
+ try: main()
+ except Exception as exc:
+ print(f"G01/G03 validation failed: {exc}", file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_heal/race_io.py b/scripts/validation_heal/race_io.py
new file mode 100644
index 00000000..2bba553d
--- /dev/null
+++ b/scripts/validation_heal/race_io.py
@@ -0,0 +1,154 @@
+"""Frozen race write bandwidth control on one owned unit and its fourth disk."""
+from pathlib import Path
+import re
+
+RATE = '65536'
+CAP_BRANCHES = {
+ 'heal.admin-nolock-safety': ('overwrite-nolock', 'delete-nolock'),
+ 'heal.race-unversioned-overwrite': ('overwrite-vs-deep', 'overwrite-vs-read-repair'),
+ 'heal.race-versioned-put': ('new-version-vs-historical-heal', 'read-all-versions-during-heal'),
+ 'heal.race-version-delete': ('delete-old-version-vs-deep', 'delete-current-version-vs-deep'),
+ 'heal.race-markers-and-batch-delete': ('create-marker', 'delete-marker', 'batch-version-delete', 'unversioned-delete'),
+ 'heal.race-copy-and-metadata-update': ('copy-source', 'copy-destination', 'replace-metadata', 'update-tags'),
+}
+
+
+def require(value, message):
+ if not value:
+ raise ValueError(message)
+
+
+def limits(raw):
+ require(isinstance(raw, str) and len(raw) <= 65536, 'bounded cgroup IO evidence required')
+ out = {}
+ for line in raw.splitlines():
+ parts = line.split()
+ require(len(out) < 64 and len(parts) == 5 and re.fullmatch(r'[0-9]+:[0-9]+', parts[0])
+ and parts[0] not in out, 'invalid or duplicate cgroup IO device')
+ values = {}
+ for item in parts[1:]:
+ pair = item.split('=')
+ require(len(pair) == 2 and pair[0] in {'rbps', 'wbps', 'riops', 'wiops'} and pair[0] not in values
+ and (pair[1] == 'max' or re.fullmatch(r'[1-9][0-9]{0,19}', pair[1])),
+ 'invalid or duplicate cgroup IO limit')
+ values[pair[0]] = pair[1]
+ out[parts[0]] = values
+ return out
+
+
+def run(request, node):
+ observing = request.get('action') == 'race-io-observe'
+ fields = {'runId', 'action', 'caseId', 'branch', 'expectedProcess'}
+ require((observing and set(request) == fields
+ and request['caseId'] == 'heal.race-multipart-complete-abort'
+ and request['branch'] in ('upload-continue', 'complete', 'abort'))
+ or (not observing and set(request) == fields | {'enabled'}
+ and request['action'] == 'race-io-cap' and type(request['enabled']) is bool
+ and request['branch'] in CAP_BRANCHES.get(request['caseId'], ())),
+ 'write cap requires a registered frozen public race; G05 forbids this cap')
+ state, roots, owner = node['owned'](request['runId'])
+ legacy_case = (observing or request['caseId'] == 'heal.race-markers-and-batch-delete')
+ require((owner.get('runtimeProfile') == 'manual' or
+ owner.get('runtimeProfile') == 'frozen-race-legacy' and legacy_case)
+ and len(roots) == len(node['MOUNTS']) == 4,
+ 'write cap requires the original four-drive race owner')
+ expected = request['expectedProcess']
+ def process():
+ actual = node['process'](request)
+ require(isinstance(expected, dict) and type(actual.get('pid')) is int and actual['pid'] > 1
+ and all(k in expected and actual.get(k) == expected[k] for k in node['PROCESS_FIELDS']),
+ 'write cap refuses a changed process birth or candidate')
+ require(actual['unit'] == owner['unit'] and actual['binarySha256'] == owner['binarySha256'],
+ 'write cap process escaped its original owner')
+ return actual
+ original = process()
+ require(node['mounts']() == owner['mounts'], 'write cap original mount identities changed')
+ target = owner['mounts'][3]
+ require(target['target'] == str(node['MOUNTS'][3]) and re.fullmatch(r'[0-9]+:[0-9]+', target['maj:min']),
+ 'write cap must target the original fourth physical disk')
+ group = node['command']('systemctl', 'show', '--value', '-p', 'ControlGroup', owner['unit']).stdout.strip()
+ require(group == '/system.slice/' + owner['unit'] and '/' not in owner['unit'],
+ 'write cap requires the exact owned systemd cgroup')
+ cgroup = Path('/sys/fs/cgroup') / group.lstrip('/')
+ def scope():
+ systemd_group = node['command']('systemctl', 'show', '--value', '-p', 'ControlGroup', owner['unit']).stdout.strip()
+ membership = node['command']('cat', f'/proc/{original["pid"]}/cgroup').stdout
+ require(systemd_group == group and
+ [line for line in membership.splitlines() if line.startswith('0:')] == ['0::' + group],
+ 'write cap process or unit moved to another cgroup')
+ return {'systemdControlGroup': systemd_group, 'processCgroup': membership}
+ scope()
+ def evidence(directory=cgroup):
+ value = node['command']('cat', str(directory / 'io.max'), check=False)
+ if value.returncode:
+ # A fresh unthrottled unit need not have its IO controller enabled.
+ # Verify the group still exists; any other cat error remains fatal.
+ require(value.returncode == 1 and 'No such file or directory' in value.stderr
+ and node['command']('test', '-d', str(directory), check=False).returncode == 0,
+ 'owned cgroup IO limits could not be read')
+ return '', {}
+ return value.stdout, limits(value.stdout)
+ if observing:
+ def sample():
+ result = []
+ # io.max is hierarchical; an empty owned unit does not rule out an
+ # inherited cap on system.slice. These paths follow the fixed group.
+ for directory in (cgroup, cgroup.parent, cgroup.parent.parent):
+ raw, parsed = evidence(directory)
+ require(all(all(value == 'max' for value in device.values()) for device in parsed.values()),
+ 'G05 functional race has an owned or inherited kernel IO cap')
+ result.append({'cgroup': str(directory), 'ioMax': raw})
+ return result
+ before_scope = scope()
+ before_sample = sample()
+ process()
+ after_scope = scope()
+ after_sample = sample()
+ process()
+ require(scope() == after_scope == before_scope,
+ 'G05 IO observation process or unit moved during sampling')
+ require(node['owned'](request['runId']) == (state, roots, owner) and node['mounts']() == owner['mounts'],
+ 'G05 IO observation owner or mount changed during sampling')
+ return {'process': original, 'controlGroup': group, 'beforeScope': before_scope, 'afterScope': after_scope,
+ 'beforeIoScopes': before_sample, 'afterIoScopes': after_sample,
+ 'actualKernelNoLimitVerifiedAtSamples': True, 'continuousNoLimitProved': False,
+ 'inspectionOnly': True, 'mutated': False, 'ioCapRequested': False,
+ 'publicOverlapProved': False, 'canonicalCasePassClaimed': False}
+ raw_before, before = evidence()
+ device = target['maj:min']
+ defaults = dict.fromkeys(('rbps','wbps','riops','wiops'), 'max')
+ prior = before.get(device, defaults)
+ require(all(v == {**defaults, 'wbps': RATE if d == device and not request['enabled'] else 'max'}
+ for d,v in before.items()) and (request['enabled'] or prior['wbps'] == RATE),
+ 'write cap refuses an existing foreign limit or an unbound release')
+ process()
+ scope_before = scope()
+ node['command']('systemctl', 'set-property', '--runtime', owner['unit'],
+ 'IOWriteBandwidthMax=' + (target['target'] + ' ' + RATE if request['enabled'] else ''))
+ raw_after, after = evidence()
+ wanted = {**defaults, 'wbps': RATE if request['enabled'] else 'max'}
+ require(after.get(device, defaults) == wanted and
+ all(v == (wanted if d == device else defaults) for d,v in after.items()),
+ 'kernel cgroup IO write cap did not match the exact original device/rate')
+ process()
+ scope_after = scope()
+ require(node['owned'](request['runId']) == (state, roots, owner) and node['mounts']() == owner['mounts'],
+ 'write cap owner or mount changed during control')
+ return {'enabled': request['enabled'], 'writeBytesPerSecond': int(RATE) if request['enabled'] else None,
+ 'disk': 3, 'device': device, 'mount': target['target'], 'controlGroup': group,
+ 'beforeIoMax': raw_before, 'afterIoMax': raw_after, 'process': original,
+ 'beforeScope': scope_before, 'afterScope': scope_after,
+ 'actualKernelLimitVerified': True, 'publicOverlapProved': False, 'canonicalCasePassClaimed': False}
+
+
+def set_fleet(fleet, case_id, branch, enabled, *, nodes):
+ require(type(enabled) is bool and branch in CAP_BRANCHES.get(case_id, ())
+ and isinstance(nodes, list) and nodes and len(set(nodes)) == len(nodes)
+ and set(nodes) <= set(fleet.nodes),
+ 'write cap is not part of this frozen public race')
+ for host in nodes:
+ process = fleet.record('race-io-process', {'node': host, **fleet.remote(host, 'process')})
+ expected = {key: value for key, value in process.items() if key != 'node'}
+ request = {'caseId': case_id, 'branch': branch, 'enabled': enabled, 'expectedProcess': expected}
+ fleet.record('race-io-cap-intent', {'node': host, **request})
+ fleet.record('race-io-cap-applied', {'node': host, **fleet.remote(host, 'race-io-cap', **request)})
diff --git a/scripts/validation_heal/race_native.py b/scripts/validation_heal/race_native.py
new file mode 100644
index 00000000..ef3d95ce
--- /dev/null
+++ b/scripts/validation_heal/race_native.py
@@ -0,0 +1,298 @@
+#!/usr/bin/env python3
+"""Exact-source race fences, separate from retained legacy native artifacts."""
+import argparse
+import hashlib
+import json
+import os
+from pathlib import Path, PurePosixPath
+import re
+import shutil
+import subprocess
+import sys
+
+CONTRACTS = Path(__file__).with_name('race_native_contracts.json')
+BUILD_ENV = {'CARGO_BUILD_JOBS': '2', 'CARGO_INCREMENTAL': '0',
+ 'CARGO_PROFILE_RELEASE_DEBUG': '0', 'CARGO_PROFILE_RELEASE_STRIP': 'debuginfo'}
+BUILD_COMMAND = ['cargo', 'test', '--locked', '--release', '-p', 'rustfs-ecstore',
+ '--lib', '--no-run', '--message-format=json']
+RUNTIME_ENV = {'RUST_MIN_STACK': '33554432'}
+BINARY = 'ecstore-race-native-tests'
+KIND = 'exact-source-race-native-not-release-service'
+CASE_IDS = {'heal.admin-nolock-safety', 'heal.race-unversioned-overwrite', 'heal.race-versioned-put',
+ 'heal.race-version-delete', 'heal.race-markers-and-batch-delete',
+ 'heal.race-multipart-complete-abort', 'heal.race-copy-and-metadata-update',
+ 'heal.lock-timeout-and-caller-abort'}
+
+
+def require(value, message):
+ if not value:
+ raise ValueError(message)
+
+
+def sha(path):
+ value = hashlib.sha256()
+ with path.open('rb') as stream:
+ while chunk := stream.read(1024**2):
+ value.update(chunk)
+ return value.hexdigest()
+
+
+def save(path, value):
+ with path.open('x') as out:
+ json.dump(value, out, indent=2)
+ out.write('\n')
+
+
+def canonical(value):
+ return json.dumps(value, sort_keys=True, separators=(',', ':'), allow_nan=False).encode()
+
+
+def registry():
+ require(CONTRACTS.is_file() and not CONTRACTS.is_symlink() and CONTRACTS.stat().st_size < 1024**2,
+ 'bounded regular race native registry required')
+ value = json.loads(CONTRACTS.read_text())
+ require(isinstance(value, dict) and type(value.get('schemaVersion')) is int and value['schemaVersion'] == 1
+ and set(value) == {'schemaVersion', 'candidates'} and isinstance(value['candidates'], dict)
+ and len(value['candidates']) <= 256
+ and all(re.fullmatch(r'[0-9a-f]{40}', key) for key in value['candidates']),
+ 'race native registry schema or candidate identity differs')
+ return value['candidates']
+
+
+def contract_for(commit):
+ require(isinstance(commit, str) and re.fullmatch(r'[0-9a-f]{40}', commit),
+ 'native source must be an exact lowercase commit SHA')
+ contract = registry().get(commit)
+ require(contract is not None, 'race native producer source is not reviewed')
+ require(isinstance(contract, dict) and set(contract) == {'sourceTree', 'cargoLockSha256', 'sourceFiles', 'regressions', 'cases'},
+ 'race native contract fields differ')
+ require(re.fullmatch(r'[0-9a-f]{40}', contract['sourceTree'])
+ and re.fullmatch(r'[0-9a-f]{64}', contract['cargoLockSha256'])
+ and isinstance(contract['sourceFiles'], dict) and 1 <= len(contract['sourceFiles']) <= 64
+ and isinstance(contract['regressions'], dict) and 1 <= len(contract['regressions']) <= 16
+ and isinstance(contract['cases'], dict) and 1 <= len(contract['cases']) <= len(CASE_IDS),
+ 'incomplete reviewed race native contract')
+ for path, digest in contract['sourceFiles'].items():
+ relative = PurePosixPath(path)
+ require(len(path) <= 256 and not relative.is_absolute() and '..' not in relative.parts
+ and '\\' not in path and path.startswith('crates/') and path.endswith('.rs')
+ and re.fullmatch(r'[0-9a-f]{64}', digest), 'native source path or digest differs')
+ for label, spec in contract['regressions'].items():
+ require(re.fullmatch(r'[a-z][a-z0-9-]{0,63}', label) and isinstance(spec, dict)
+ and set(spec) == {'test', 'sourcePath', 'sourceAssertions', 'witnessKind', 'milestonePrefix', 'milestones'},
+ 'native regression fields differ')
+ require(isinstance(spec['test'], str) and len(spec['test']) <= 512
+ and re.fullmatch(r'(set_disk|store)(::[A-Za-z_][A-Za-z0-9_]*)+', spec['test'])
+ and spec['sourcePath'] in contract['sourceFiles']
+ and isinstance(spec['sourceAssertions'], list) and 1 <= len(spec['sourceAssertions']) <= 32
+ and all(isinstance(item, str) and 0 < len(item) <= 512 for item in spec['sourceAssertions']),
+ 'native test identity or source assertions differ')
+ require(isinstance(spec['milestones'], list) and len(spec['milestones']) <= 128
+ and all(isinstance(item, str) and 0 < len(item) <= 512 and '\n' not in item and '\r' not in item
+ for item in spec['milestones'])
+ and len(set(spec['milestones'])) == len(spec['milestones']), 'native milestone contract is malformed')
+ if spec['witnessKind'] == 'ordered-barrier-milestones':
+ require(isinstance(spec['milestonePrefix'], str)
+ and re.fullmatch(r'native_(a09|g0[1-6]|i05): ', spec['milestonePrefix'])
+ and spec['milestones'] and all(item.startswith(spec['milestonePrefix']) for item in spec['milestones']),
+ 'native ordered milestone namespace differs')
+ else:
+ require(spec['witnessKind'] == 'reviewed-in-test-assertions'
+ and spec['milestonePrefix'] is None and spec['milestones'] == [], 'native witness kind differs')
+ used = set()
+ for case, labels in contract['cases'].items():
+ require(case in CASE_IDS and isinstance(labels, list) and labels
+ and all(isinstance(label, str) and label in contract['regressions'] for label in labels)
+ and len(labels) == len(set(labels)), 'native case lost its exact required regression group')
+ used.update(labels)
+ require(used == set(contract['regressions']), 'native regressions contain an unbound case subset')
+ return contract
+
+
+def exact_result(output, name, code):
+ require(type(code) is int and code == 0
+ and re.findall(r'(?m)^running .*', output) == ['running 1 test']
+ and re.findall(r'(?m)^test (?!result:).*', output) == [f'test {name} ... ok']
+ and len(re.findall(r'(?m)^test result:.*', output)) == 1
+ and re.search(r'(?m)^test result: ok\. 1 passed; 0 failed; 0 ignored;', output),
+ 'native execution must run one exact nonignored passing test')
+
+
+def milestones(output, spec):
+ if spec.get('witnessKind') == 'reviewed-in-test-assertions':
+ require(spec.get('milestones') == [] and spec.get('milestonePrefix') is None
+ and bool(spec.get('sourceAssertions')), 'native in-test assertion contract is incomplete')
+ return []
+ require(spec.get('witnessKind') == 'ordered-barrier-milestones', 'unreviewed native witness kind')
+ prefix = spec['milestonePrefix']
+ require(re.fullmatch(r'native_(a09|g0[1-6]|i05): ', prefix), 'unreviewed native milestone namespace')
+ observed = [line for line in output.splitlines() if line.startswith(prefix)]
+ require(bool(spec['milestones']) and observed == spec['milestones'],
+ 'actual native milestones are absent, duplicated or out of order')
+ return observed
+
+
+def verify(directory):
+ require(directory.is_dir() and not directory.is_symlink(), 'native artifact directory is not regular')
+ path = directory / 'race-native.json'
+ require(path.is_file() and not path.is_symlink() and path.stat().st_size < 1024**2,
+ 'bounded regular race native manifest required')
+ value = json.loads(path.read_text())
+ contract = contract_for(value.get('candidateCommit'))
+ require(type(value.get('schemaVersion')) is int and value['schemaVersion'] == 1 and value.get('artifactKind') == KIND
+ and value.get('candidateSourceTree') == contract['sourceTree']
+ and value.get('candidateCargoLockSha256') == contract['cargoLockSha256']
+ and value.get('sourceFiles') == contract['sourceFiles']
+ and value.get('regressions') == contract['regressions']
+ and value.get('cases') == contract['cases']
+ and value.get('buildEnvironment') == BUILD_ENV and value.get('buildCommand') == BUILD_COMMAND
+ and value.get('runtimeEnvironment') == RUNTIME_ENV and value.get('optimized') is True
+ and value.get('sourceUnmodified') is True and value.get('binaryName') == BINARY
+ and value.get('collectorSha256') == sha(Path(__file__))
+ and value.get('selectedContractSha256') == hashlib.sha256(canonical(contract)).hexdigest()
+ and value.get('canonicalCasePassClaimed') is False and value.get('fleetExecutionClaimed') is False,
+ 'race native artifact identity or reviewed contract differs')
+ require(isinstance(value.get('testingCommit'), str)
+ and re.fullmatch(r'[0-9a-f]{40}', value['testingCommit']), 'native helper identity is absent')
+ require(isinstance(value.get('driverCommit'), str) and re.fullmatch(r'[0-9a-f]{40}', value['driverCommit'])
+ and isinstance(value.get('registrySourceSha256'), str)
+ and re.fullmatch(r'[0-9a-f]{64}', value['registrySourceSha256']), 'native driver or registry source identity is absent')
+ require(all(isinstance(value.get(key), str) and re.fullmatch(r'[1-9][0-9]*', value[key])
+ for key in ('runId', 'runAttempt')), 'native build attempt identity is absent')
+ binary = directory / BINARY
+ require(binary.is_file() and not binary.is_symlink() and sha(binary) == value.get('binarySha256'),
+ 'exact race native executable changed')
+ return value, binary
+
+
+def build(source, commit, testing_commit, out):
+ contract = contract_for(commit)
+ helper_root = Path(__file__).resolve().parents[2]
+ git = lambda *args: subprocess.check_output(['git', '-C', str(source), *args], text=True).strip()
+ require(git('rev-parse', 'HEAD') == commit and git('rev-parse', 'HEAD^{tree}') == contract['sourceTree'],
+ 'candidate checkout differs from reviewed source')
+ require(not git('status', '--porcelain', '--untracked-files=all'), 'native source has modified or untracked files')
+ require(all(re.fullmatch(r'[1-9][0-9]*', os.environ.get(key, ''))
+ for key in ('GITHUB_RUN_ID', 'GITHUB_RUN_ATTEMPT'))
+ and re.fullmatch(r'[0-9a-f]{40}', os.environ.get('GITHUB_SHA', '')),
+ 'native producer requires an exact workflow build attempt')
+ require(re.fullmatch(r'[0-9a-f]{40}', testing_commit)
+ and subprocess.check_output(['git', '-C', str(helper_root), 'rev-parse', 'HEAD'], text=True).strip() == testing_commit,
+ 'native helper checkout differs from exact requested commit')
+ require(sha(source / 'Cargo.lock') == contract['cargoLockSha256']
+ and all((source / path).is_file() and not (source / path).is_symlink()
+ and (source / path).resolve() == source / path and sha(source / path) == digest
+ for path, digest in contract['sourceFiles'].items()),
+ 'candidate lock or runtime-fence source differs')
+ collector_digest, registry_digest = sha(Path(__file__)), sha(CONTRACTS)
+ for path in [Path(__file__), CONTRACTS]:
+ relative = path.relative_to(helper_root).as_posix()
+ committed = subprocess.check_output(['git', '-C', str(helper_root), 'show', testing_commit + ':' + relative])
+ require(committed == path.read_bytes(), 'native helper registry or collector has tracked changes')
+ for key in ('RUSTFLAGS', 'CARGO_ENCODED_RUSTFLAGS', 'RUSTC_WRAPPER', 'RUSTC_WORKSPACE_WRAPPER',
+ 'CARGO_BUILD_RUSTFLAGS', 'CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS'):
+ require(not os.environ.get(key), 'native build has an unbound compiler override')
+ require(not out.exists() and shutil.disk_usage(source).free >= 12 * 1024**3,
+ 'fresh native output and build disk space required')
+ out.mkdir(parents=True)
+ target = out / 'target'
+ request = {'candidateCommit': commit, 'candidateSourceTree': contract['sourceTree'],
+ 'candidateCargoLockSha256': contract['cargoLockSha256'], 'testingCommit': testing_commit,
+ 'sourceFiles': contract['sourceFiles'], 'regressions': contract['regressions'],
+ 'cases': contract['cases'],
+ 'runId': os.environ.get('GITHUB_RUN_ID'), 'runAttempt': os.environ.get('GITHUB_RUN_ATTEMPT'),
+ 'artifactKind': KIND, 'canonicalCasePassClaimed': False, 'fleetExecutionClaimed': False}
+ request.update(selectedContractSha256=hashlib.sha256(canonical(contract)).hexdigest(),
+ registrySourceSha256=registry_digest, driverCommit=os.environ.get('GITHUB_SHA'))
+ save(out / 'race-native-build-request.json', request)
+ env = {**os.environ, **BUILD_ENV, 'CARGO_TARGET_DIR': str(target)}
+ with (out / 'cargo.jsonl').open('x') as stdout, (out / 'cargo.stderr').open('x') as stderr:
+ result = subprocess.run(BUILD_COMMAND, cwd=source, env=env, stdout=stdout, stderr=stderr, timeout=9000)
+ require(result.returncode == 0, 'native build failed; preserve original diagnostics')
+ require(not git('status', '--porcelain', '--untracked-files=all'), 'native build changed candidate source')
+ require(sha(Path(__file__)) == collector_digest and sha(CONTRACTS) == registry_digest,
+ 'native build changed its helper source or registry')
+ executables = []
+ for line in (out / 'cargo.jsonl').read_text().splitlines():
+ try:
+ row = json.loads(line)
+ except json.JSONDecodeError:
+ continue
+ if (row.get('reason') == 'compiler-artifact' and row.get('profile', {}).get('test') is True
+ and row.get('target', {}).get('name') == 'rustfs_ecstore' and row.get('executable')):
+ executables.append(Path(row['executable']))
+ require(len(executables) == 1 and not executables[0].is_symlink()
+ and executables[0].resolve().is_relative_to(target.resolve()), 'native build executable escaped its target')
+ binary = out / BINARY
+ shutil.copyfile(executables[0], binary)
+ binary.chmod(0o500)
+ listing = subprocess.run([str(binary), '--list'], capture_output=True, text=True, timeout=60)
+ (out / 'test-inventory.log').write_text(listing.stdout + listing.stderr)
+ require(listing.returncode == 0, 'native test inventory failed')
+ names = [line.removesuffix(': test') for line in listing.stdout.splitlines() if line.endswith(': test')]
+ require(len(names) == len(set(names)), 'native test inventory duplicates an identity')
+ require(all(names.count(spec['test']) == 1 for spec in contract['regressions'].values()),
+ 'required exact native test is absent or repeated')
+ save(out / 'race-native.json', {**request, 'schemaVersion': 1, 'optimized': True, 'sourceUnmodified': True,
+ 'binaryName': BINARY, 'binarySha256': sha(binary), 'collectorSha256': sha(Path(__file__)),
+ 'buildEnvironment': BUILD_ENV, 'buildCommand': BUILD_COMMAND, 'runtimeEnvironment': RUNTIME_ENV})
+
+
+def run(directory, out):
+ value, binary = verify(directory)
+ require(not out.exists(), 'retain original native run; no automatic retries')
+ out.mkdir(parents=True)
+ rows = []
+ for label, spec in value['regressions'].items():
+ require(verify(directory)[0] == value, 'native identity changed before execution')
+ require(re.fullmatch(r'[a-z][a-z0-9-]*', label), 'invalid reviewed native label')
+ command = [str(binary), spec['test'], '--exact', '--show-output', '--test-threads=1']
+ result = subprocess.run(command, env={**os.environ, **RUNTIME_ENV}, capture_output=True, text=True, timeout=300)
+ log = out / (label + '.log')
+ log.write_text(result.stdout + result.stderr)
+ require(verify(directory)[0] == value, 'native identity changed during execution')
+ exact_result(result.stdout, spec['test'], result.returncode)
+ observed = milestones(result.stdout, spec)
+ rows.append({'label': label, 'test': spec['test'], 'command': command, 'nativeRegressionPassed': True,
+ 'exitCode': result.returncode, 'runtimeEnvironment': RUNTIME_ENV,
+ 'milestones': observed, 'originalLogSha256': sha(log)})
+ save(out / 'race-native-results.json', {'schemaVersion': 1, 'candidateCommit': value['candidateCommit'],
+ 'candidateSourceTree': value['candidateSourceTree'], 'binarySha256': value['binarySha256'],
+ 'nativeManifestSha256': sha(directory / 'race-native.json'), 'results': rows,
+ 'requiredNativeSubsetsComplete': True, 'canonicalCasePassClaimed': False, 'fleetExecutionClaimed': False})
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ sub = parser.add_subparsers(dest='action', required=True)
+ supports_parser = sub.add_parser('supports')
+ supports_parser.add_argument('--commit', required=True)
+ supports_parser.add_argument('--case-id', choices=sorted(CASE_IDS))
+ build_parser = sub.add_parser('build')
+ build_parser.add_argument('--source', type=Path, required=True)
+ build_parser.add_argument('--commit', required=True)
+ build_parser.add_argument('--testing-commit', required=True)
+ build_parser.add_argument('--out', type=Path, required=True)
+ run_parser = sub.add_parser('run')
+ run_parser.add_argument('--directory', type=Path, required=True)
+ run_parser.add_argument('--out', type=Path, required=True)
+ args = parser.parse_args()
+ if args.action == 'supports':
+ require(re.fullmatch(r'[0-9a-f]{40}', args.commit), 'exact native source SHA required')
+ if args.commit not in registry():
+ sys.exit(2)
+ contract = contract_for(args.commit)
+ if args.case_id and args.case_id not in contract['cases']:
+ sys.exit(2)
+ elif args.action == 'build':
+ build(args.source.resolve(), args.commit, args.testing_commit, args.out.resolve())
+ else:
+ run(args.directory.absolute(), args.out.resolve())
+
+
+if __name__ == '__main__':
+ try:
+ main()
+ except (ValueError, OSError, subprocess.SubprocessError, KeyError, TypeError, json.JSONDecodeError) as error:
+ print(f'race native refusal: {error}', file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_heal/race_native_contracts.json b/scripts/validation_heal/race_native_contracts.json
new file mode 100644
index 00000000..2955367a
--- /dev/null
+++ b/scripts/validation_heal/race_native_contracts.json
@@ -0,0 +1,875 @@
+{
+ "schemaVersion": 1,
+ "candidates": {
+ "6afdec9fe1b0b88007f2e5b445c19bf73d51d3ca": {
+ "sourceTree": "65abe5d45aa4d632499960cff01bbf8d80f8a26a",
+ "cargoLockSha256": "b4d959f964c13d8ed507657fa4b52752769f96e5cce932845d3d8cec69a5cd56",
+ "sourceFiles": {
+ "crates/ecstore/src/set_disk/ops/object.rs": "9af09ff2c969975dd5196bda8006745d13c5cc6a8f520f6e1b67cc1b2dd0ca3a",
+ "crates/ecstore/src/set_disk/ops/heal.rs": "4029248422352e178b92cf26f6b96cc88b6a521eefd7c57f2985f9da1daab2e8",
+ "crates/ecstore/src/set_disk/core/io_primitives.rs": "6e50c8ea8b374b8a2d73da740321af25fed6e26c34e5117feca3e5f5891782ab"
+ },
+ "regressions": {
+ "committed-overwrite-cleanup": {
+ "test": "set_disk::ops::object::put_object_tmp_cleanup_tests::deep_heal_preserves_committed_generation_during_old_data_cleanup",
+ "sourcePath": "crates/ecstore/src/set_disk/ops/object.rs",
+ "sourceAssertions": [
+ "Actual PHASE_CLEANUP pause after new physical generation commit while TailDrained overwrite ACK remains pending.",
+ "Real Deep and shared read-repair each reconstruct missing new-generation encoded part before any GET, across 4/2 and 16/4 geometries.",
+ "All unchanged replica metadata, original overwrite ACK and final new-generation physical/full-body identity are preserved."
+ ],
+ "witnessKind": "ordered-barrier-milestones",
+ "milestonePrefix": "native_g01: ",
+ "milestones": [
+ "native_g01: geometry=4/2 read_repair=false old_cleanup_paused new_generation_committed target_part_missing",
+ "native_g01: geometry=4/2 read_repair=false locked_heal_completed new_generation_part_rebuilt_before_get",
+ "native_g01: geometry=4/2 read_repair=false overwrite_ack_preserved final_physical_generation_and_full_body_verified",
+ "native_g01: geometry=4/2 read_repair=true old_cleanup_paused new_generation_committed target_part_missing",
+ "native_g01: geometry=4/2 read_repair=true locked_heal_completed new_generation_part_rebuilt_before_get",
+ "native_g01: geometry=4/2 read_repair=true overwrite_ack_preserved final_physical_generation_and_full_body_verified",
+ "native_g01: geometry=16/4 read_repair=false old_cleanup_paused new_generation_committed target_part_missing",
+ "native_g01: geometry=16/4 read_repair=false locked_heal_completed new_generation_part_rebuilt_before_get",
+ "native_g01: geometry=16/4 read_repair=false overwrite_ack_preserved final_physical_generation_and_full_body_verified",
+ "native_g01: geometry=16/4 read_repair=true old_cleanup_paused new_generation_committed target_part_missing",
+ "native_g01: geometry=16/4 read_repair=true locked_heal_completed new_generation_part_rebuilt_before_get",
+ "native_g01: geometry=16/4 read_repair=true overwrite_ack_preserved final_physical_generation_and_full_body_verified"
+ ]
+ },
+ "stale-read-repair-final-rename": {
+ "test": "set_disk::ops::heal::heal_result_report_tests::read_repair_commit_aborts_when_object_changes_before_final_rename",
+ "sourcePath": "crates/ecstore/src/set_disk/ops/heal.rs",
+ "sourceAssertions": [
+ "Real missing external shard forces actual shared read-repair data reconstruction.",
+ "Pause immediately before final commit fence; a successor overwrite ACK precedes release.",
+ "Obsolete read-repair returns exact DiskError::Timeout and final current ETag equals acknowledged overwrite."
+ ],
+ "witnessKind": "reviewed-in-test-assertions",
+ "milestonePrefix": null,
+ "milestones": []
+ },
+ "versioned-read-repair-lock-owner": {
+ "test": "set_disk::ops::heal::heal_result_report_tests::object_lock_diagnostics_distinguish_read_repair_from_queued_put",
+ "sourcePath": "crates/ecstore/src/set_disk/ops/heal.rs",
+ "sourceAssertions": [
+ "Historical missing external shard is repaired under actual shared Data-phase lock.",
+ "Queued PUT cannot acquire namespace and later historical GET times out while version-specific owner remains held.",
+ "After release, successor PUT ACK and every original/new version full body remain correct."
+ ],
+ "witnessKind": "reviewed-in-test-assertions",
+ "milestonePrefix": null,
+ "milestones": []
+ },
+ "cancelled-physical-unlink-owner": {
+ "test": "set_disk::ops::heal::heal_result_report_tests::cancelled_heal_backup_cleanup_retains_key_owner_until_unlink_finishes",
+ "sourcePath": "crates/ecstore/src/set_disk/ops/heal.rs",
+ "sourceAssertions": [
+ "Actual external Heal backup unlink pauses in outer, inner and shared read-repair modes.",
+ "Caller abort does not retire namespace commit owner; successor write lock remains pending until physical unlink drains.",
+ "After physical completion, successor obtains lock, owned backup is absent and committed full body is preserved."
+ ],
+ "witnessKind": "reviewed-in-test-assertions",
+ "milestonePrefix": null,
+ "milestones": []
+ }
+ },
+ "cases": {
+ "heal.race-unversioned-overwrite": [
+ "committed-overwrite-cleanup",
+ "stale-read-repair-final-rename"
+ ],
+ "heal.race-versioned-put": [
+ "versioned-read-repair-lock-owner"
+ ],
+ "heal.lock-timeout-and-caller-abort": [
+ "versioned-read-repair-lock-owner",
+ "cancelled-physical-unlink-owner",
+ "stale-read-repair-final-rename"
+ ]
+ }
+ },
+ "bd0671ba781d8ebf509f4f03533228dfb44496f1": {
+ "sourceTree": "d1ee045065b69ab9298fb7f3d5d7bba0bd0b5a42",
+ "cargoLockSha256": "b4d959f964c13d8ed507657fa4b52752769f96e5cce932845d3d8cec69a5cd56",
+ "sourceFiles": {
+ "crates/ecstore/src/set_disk/ops/object.rs": "d033df47054f054413c007d5e5b94212676411fbb3670403e08358f9152ef443",
+ "crates/ecstore/src/set_disk/ops/heal.rs": "7693b55f5417991f6f5ef7d6a234f6e645d5dbbf39921e722b2825301b24b747",
+ "crates/ecstore/src/set_disk/core/io_primitives.rs": "6e50c8ea8b374b8a2d73da740321af25fed6e26c34e5117feca3e5f5891782ab"
+ },
+ "regressions": {
+ "committed-overwrite-cleanup": {
+ "test": "set_disk::ops::object::put_object_tmp_cleanup_tests::deep_heal_preserves_committed_generation_during_old_data_cleanup",
+ "sourcePath": "crates/ecstore/src/set_disk/ops/object.rs",
+ "sourceAssertions": [
+ "Every actual shuffled cleanup slot pauses after new physical generation commit while TailDrained overwrite ACK remains pending.",
+ "Real Deep and shared read-repair reconstruct a missing new-generation encoded part before GET in4/2 and16/4 geometries.",
+ "All old physical generations disappear after cleanup; every new encoded shard, unchanged metadata, overwrite ACK and complete final body remain exact."
+ ],
+ "witnessKind": "ordered-barrier-milestones",
+ "milestonePrefix": "native_g01: ",
+ "milestones": [
+ "native_g01: geometry=4/2 read_repair=false old_cleanup_paused new_generation_committed target_part_missing",
+ "native_g01: geometry=4/2 read_repair=false locked_heal_completed new_generation_part_rebuilt_before_get",
+ "native_g01: geometry=4/2 read_repair=false overwrite_ack_preserved final_physical_generation_and_full_body_verified",
+ "native_g01: geometry=4/2 read_repair=true old_cleanup_paused new_generation_committed target_part_missing",
+ "native_g01: geometry=4/2 read_repair=true locked_heal_completed new_generation_part_rebuilt_before_get",
+ "native_g01: geometry=4/2 read_repair=true overwrite_ack_preserved final_physical_generation_and_full_body_verified",
+ "native_g01: geometry=16/4 read_repair=false old_cleanup_paused new_generation_committed target_part_missing",
+ "native_g01: geometry=16/4 read_repair=false locked_heal_completed new_generation_part_rebuilt_before_get",
+ "native_g01: geometry=16/4 read_repair=false overwrite_ack_preserved final_physical_generation_and_full_body_verified",
+ "native_g01: geometry=16/4 read_repair=true old_cleanup_paused new_generation_committed target_part_missing",
+ "native_g01: geometry=16/4 read_repair=true locked_heal_completed new_generation_part_rebuilt_before_get",
+ "native_g01: geometry=16/4 read_repair=true overwrite_ack_preserved final_physical_generation_and_full_body_verified"
+ ]
+ },
+ "stale-read-repair-final-rename": {
+ "test": "set_disk::ops::heal::heal_result_report_tests::read_repair_commit_aborts_when_object_changes_before_final_rename",
+ "sourcePath": "crates/ecstore/src/set_disk/ops/heal.rs",
+ "sourceAssertions": [
+ "Real missing external shard forces actual shared read-repair data reconstruction.",
+ "Pause immediately before final commit fence; a successor overwrite ACK precedes release.",
+ "Obsolete read-repair returns exact DiskError::Timeout and final current ETag equals acknowledged overwrite."
+ ],
+ "witnessKind": "reviewed-in-test-assertions",
+ "milestonePrefix": null,
+ "milestones": []
+ },
+ "versioned-read-repair-lock-owner": {
+ "test": "set_disk::ops::heal::heal_result_report_tests::object_lock_diagnostics_distinguish_read_repair_from_queued_put",
+ "sourcePath": "crates/ecstore/src/set_disk/ops/heal.rs",
+ "sourceAssertions": [
+ "Historical missing external shard is repaired under actual shared Data-phase lock.",
+ "Queued PUT cannot acquire namespace and later historical GET times out while version-specific owner remains held.",
+ "After release, successor PUT ACK and every original/new version full body remain correct."
+ ],
+ "witnessKind": "reviewed-in-test-assertions",
+ "milestonePrefix": null,
+ "milestones": []
+ },
+ "cancelled-physical-unlink-owner": {
+ "test": "set_disk::ops::heal::heal_result_report_tests::cancelled_heal_backup_cleanup_retains_key_owner_until_unlink_finishes",
+ "sourcePath": "crates/ecstore/src/set_disk/ops/heal.rs",
+ "sourceAssertions": [
+ "Actual external Heal backup unlink pauses in outer, inner and shared read-repair modes.",
+ "Caller abort does not retire namespace commit owner; successor write lock remains pending until physical unlink drains.",
+ "After physical completion, successor obtains lock, owned backup is absent and committed full body is preserved."
+ ],
+ "witnessKind": "reviewed-in-test-assertions",
+ "milestonePrefix": null,
+ "milestones": []
+ },
+ "explicit-version-append-final-rename": {
+ "test": "set_disk::ops::heal::heal_result_report_tests::native_explicit_version_commit_preserves_acknowledged_new_version",
+ "sourcePath": "crates/ecstore/src/set_disk/ops/heal.rs",
+ "sourceAssertions": [
+ "Actual selected-version shared Deep reconstruction pauses before its final rename after releasing the read guard.",
+ "Foreground version mutation ACK and same-key physical write-tail drain precede releasing stale Heal; append succeeds or deleted version remains typed FileVersionNotFound.",
+ "All surviving acknowledged version UUIDs, data dirs, original encoded shards and complete exact/current bodies remain intact in4/2 and16/4; deleted generations do not reappear."
+ ],
+ "witnessKind": "ordered-barrier-milestones",
+ "milestonePrefix": "native_g02: ",
+ "milestones": [
+ "native_g02: geometry=4/2 mutation=append reconstruction_paused_before_final_rename",
+ "native_g02: geometry=4/2 mutation=append foreground_ack_and_physical_tail_verified_before_release",
+ "native_g02: geometry=4/2 mutation=append exact_version_outcome_physical_generations_and_full_bodies_verified",
+ "native_g02: geometry=16/4 mutation=append reconstruction_paused_before_final_rename",
+ "native_g02: geometry=16/4 mutation=append foreground_ack_and_physical_tail_verified_before_release",
+ "native_g02: geometry=16/4 mutation=append exact_version_outcome_physical_generations_and_full_bodies_verified"
+ ]
+ },
+ "explicit-version-delete-final-rename": {
+ "test": "set_disk::ops::heal::heal_result_report_tests::native_explicit_version_commit_refuses_deleted_historical_and_current_versions",
+ "sourcePath": "crates/ecstore/src/set_disk/ops/heal.rs",
+ "sourceAssertions": [
+ "Actual selected-version shared Deep reconstruction pauses before its final rename after releasing the read guard.",
+ "Foreground version mutation ACK and same-key physical write-tail drain precede releasing stale Heal; append succeeds or deleted version remains typed FileVersionNotFound.",
+ "All surviving acknowledged version UUIDs, data dirs, original encoded shards and complete exact/current bodies remain intact in4/2 and16/4; deleted generations do not reappear."
+ ],
+ "witnessKind": "ordered-barrier-milestones",
+ "milestonePrefix": "native_g03: ",
+ "milestones": [
+ "native_g03: geometry=4/2 mutation=delete-historical reconstruction_paused_before_final_rename",
+ "native_g03: geometry=4/2 mutation=delete-historical foreground_ack_and_physical_tail_verified_before_release",
+ "native_g03: geometry=4/2 mutation=delete-historical exact_version_outcome_physical_generations_and_full_bodies_verified",
+ "native_g03: geometry=16/4 mutation=delete-historical reconstruction_paused_before_final_rename",
+ "native_g03: geometry=16/4 mutation=delete-historical foreground_ack_and_physical_tail_verified_before_release",
+ "native_g03: geometry=16/4 mutation=delete-historical exact_version_outcome_physical_generations_and_full_bodies_verified",
+ "native_g03: geometry=4/2 mutation=delete-current reconstruction_paused_before_final_rename",
+ "native_g03: geometry=4/2 mutation=delete-current foreground_ack_and_physical_tail_verified_before_release",
+ "native_g03: geometry=4/2 mutation=delete-current exact_version_outcome_physical_generations_and_full_bodies_verified",
+ "native_g03: geometry=16/4 mutation=delete-current reconstruction_paused_before_final_rename",
+ "native_g03: geometry=16/4 mutation=delete-current foreground_ack_and_physical_tail_verified_before_release",
+ "native_g03: geometry=16/4 mutation=delete-current exact_version_outcome_physical_generations_and_full_bodies_verified"
+ ]
+ }
+ },
+ "cases": {
+ "heal.race-unversioned-overwrite": [
+ "committed-overwrite-cleanup",
+ "stale-read-repair-final-rename"
+ ],
+ "heal.race-versioned-put": [
+ "versioned-read-repair-lock-owner",
+ "explicit-version-append-final-rename"
+ ],
+ "heal.lock-timeout-and-caller-abort": [
+ "versioned-read-repair-lock-owner",
+ "cancelled-physical-unlink-owner",
+ "stale-read-repair-final-rename"
+ ],
+ "heal.race-version-delete": [
+ "explicit-version-delete-final-rename"
+ ]
+ }
+ },
+ "8380466f3a27c3c3ef4115084178a0ab99a78f32": {
+ "sourceTree": "48a90bf754f51d1f743b2809d484dd0b4c0575d1",
+ "cargoLockSha256": "b4d959f964c13d8ed507657fa4b52752769f96e5cce932845d3d8cec69a5cd56",
+ "sourceFiles": {
+ "crates/ecstore/src/set_disk/ops/object.rs": "d033df47054f054413c007d5e5b94212676411fbb3670403e08358f9152ef443",
+ "crates/ecstore/src/set_disk/ops/heal.rs": "b606f01db029cf6eb909a809094f5696b8eb92b2c90fdf5a23cc116185c45583",
+ "crates/ecstore/src/set_disk/core/io_primitives.rs": "6e50c8ea8b374b8a2d73da740321af25fed6e26c34e5117feca3e5f5891782ab"
+ },
+ "regressions": {
+ "committed-overwrite-cleanup": {
+ "test": "set_disk::ops::object::put_object_tmp_cleanup_tests::deep_heal_preserves_committed_generation_during_old_data_cleanup",
+ "sourcePath": "crates/ecstore/src/set_disk/ops/object.rs",
+ "sourceAssertions": [
+ "Every actual shuffled cleanup slot pauses after new physical generation commit while TailDrained overwrite ACK remains pending.",
+ "Real Deep and shared read-repair reconstruct a missing new-generation encoded part before GET in4/2 and16/4 geometries.",
+ "All old physical generations disappear after cleanup; every new encoded shard, unchanged metadata, overwrite ACK and complete final body remain exact."
+ ],
+ "witnessKind": "ordered-barrier-milestones",
+ "milestonePrefix": "native_g01: ",
+ "milestones": [
+ "native_g01: geometry=4/2 read_repair=false old_cleanup_paused new_generation_committed target_part_missing",
+ "native_g01: geometry=4/2 read_repair=false locked_heal_completed new_generation_part_rebuilt_before_get",
+ "native_g01: geometry=4/2 read_repair=false overwrite_ack_preserved final_physical_generation_and_full_body_verified",
+ "native_g01: geometry=4/2 read_repair=true old_cleanup_paused new_generation_committed target_part_missing",
+ "native_g01: geometry=4/2 read_repair=true locked_heal_completed new_generation_part_rebuilt_before_get",
+ "native_g01: geometry=4/2 read_repair=true overwrite_ack_preserved final_physical_generation_and_full_body_verified",
+ "native_g01: geometry=16/4 read_repair=false old_cleanup_paused new_generation_committed target_part_missing",
+ "native_g01: geometry=16/4 read_repair=false locked_heal_completed new_generation_part_rebuilt_before_get",
+ "native_g01: geometry=16/4 read_repair=false overwrite_ack_preserved final_physical_generation_and_full_body_verified",
+ "native_g01: geometry=16/4 read_repair=true old_cleanup_paused new_generation_committed target_part_missing",
+ "native_g01: geometry=16/4 read_repair=true locked_heal_completed new_generation_part_rebuilt_before_get",
+ "native_g01: geometry=16/4 read_repair=true overwrite_ack_preserved final_physical_generation_and_full_body_verified"
+ ]
+ },
+ "stale-read-repair-final-rename": {
+ "test": "set_disk::ops::heal::heal_result_report_tests::read_repair_commit_aborts_when_object_changes_before_final_rename",
+ "sourcePath": "crates/ecstore/src/set_disk/ops/heal.rs",
+ "sourceAssertions": [
+ "Real missing external shard forces actual shared read-repair data reconstruction.",
+ "Pause immediately before final commit fence; a successor overwrite ACK precedes release.",
+ "Obsolete read-repair returns exact DiskError::Timeout and final current ETag equals acknowledged overwrite."
+ ],
+ "witnessKind": "reviewed-in-test-assertions",
+ "milestonePrefix": null,
+ "milestones": []
+ },
+ "versioned-read-repair-lock-owner": {
+ "test": "set_disk::ops::heal::heal_result_report_tests::object_lock_diagnostics_distinguish_read_repair_from_queued_put",
+ "sourcePath": "crates/ecstore/src/set_disk/ops/heal.rs",
+ "sourceAssertions": [
+ "Historical missing external shard is repaired under actual shared Data-phase lock.",
+ "Queued PUT cannot acquire namespace and later historical GET times out while version-specific owner remains held.",
+ "After release, successor PUT ACK and every original/new version full body remain correct."
+ ],
+ "witnessKind": "reviewed-in-test-assertions",
+ "milestonePrefix": null,
+ "milestones": []
+ },
+ "cancelled-physical-unlink-owner": {
+ "test": "set_disk::ops::heal::heal_result_report_tests::cancelled_heal_backup_cleanup_retains_key_owner_until_unlink_finishes",
+ "sourcePath": "crates/ecstore/src/set_disk/ops/heal.rs",
+ "sourceAssertions": [
+ "Actual external Heal backup unlink pauses in outer, inner and shared read-repair modes.",
+ "Caller abort does not retire namespace commit owner; successor write lock remains pending until physical unlink drains.",
+ "After physical completion, successor obtains lock, owned backup is absent and committed full body is preserved."
+ ],
+ "witnessKind": "reviewed-in-test-assertions",
+ "milestonePrefix": null,
+ "milestones": []
+ },
+ "explicit-version-append-final-rename": {
+ "test": "set_disk::ops::heal::heal_result_report_tests::native_explicit_version_commit_preserves_acknowledged_new_version",
+ "sourcePath": "crates/ecstore/src/set_disk/ops/heal.rs",
+ "sourceAssertions": [
+ "Actual selected-version shared Deep reconstruction pauses before its final rename after releasing the read guard.",
+ "Foreground version mutation ACK and same-key physical write-tail drain precede releasing stale Heal; append succeeds or deleted version remains typed FileVersionNotFound.",
+ "All surviving acknowledged version UUIDs, data dirs, original encoded shards and complete exact/current bodies remain intact in4/2 and16/4; deleted generations do not reappear."
+ ],
+ "witnessKind": "ordered-barrier-milestones",
+ "milestonePrefix": "native_g02: ",
+ "milestones": [
+ "native_g02: geometry=4/2 mutation=append reconstruction_paused_before_final_rename",
+ "native_g02: geometry=4/2 mutation=append foreground_ack_and_physical_tail_verified_before_release",
+ "native_g02: geometry=4/2 mutation=append exact_version_outcome_physical_generations_and_full_bodies_verified",
+ "native_g02: geometry=16/4 mutation=append reconstruction_paused_before_final_rename",
+ "native_g02: geometry=16/4 mutation=append foreground_ack_and_physical_tail_verified_before_release",
+ "native_g02: geometry=16/4 mutation=append exact_version_outcome_physical_generations_and_full_bodies_verified"
+ ]
+ },
+ "explicit-version-delete-final-rename": {
+ "test": "set_disk::ops::heal::heal_result_report_tests::native_explicit_version_commit_refuses_deleted_historical_and_current_versions",
+ "sourcePath": "crates/ecstore/src/set_disk/ops/heal.rs",
+ "sourceAssertions": [
+ "Actual selected-version shared Deep reconstruction pauses before its final rename after releasing the read guard.",
+ "Foreground version mutation ACK and same-key physical write-tail drain precede releasing stale Heal; append succeeds or deleted version remains typed FileVersionNotFound.",
+ "All surviving acknowledged version UUIDs, data dirs, original encoded shards and complete exact/current bodies remain intact in4/2 and16/4; deleted generations do not reappear."
+ ],
+ "witnessKind": "ordered-barrier-milestones",
+ "milestonePrefix": "native_g03: ",
+ "milestones": [
+ "native_g03: geometry=4/2 mutation=delete-historical reconstruction_paused_before_final_rename",
+ "native_g03: geometry=4/2 mutation=delete-historical foreground_ack_and_physical_tail_verified_before_release",
+ "native_g03: geometry=4/2 mutation=delete-historical exact_version_outcome_physical_generations_and_full_bodies_verified",
+ "native_g03: geometry=16/4 mutation=delete-historical reconstruction_paused_before_final_rename",
+ "native_g03: geometry=16/4 mutation=delete-historical foreground_ack_and_physical_tail_verified_before_release",
+ "native_g03: geometry=16/4 mutation=delete-historical exact_version_outcome_physical_generations_and_full_bodies_verified",
+ "native_g03: geometry=4/2 mutation=delete-current reconstruction_paused_before_final_rename",
+ "native_g03: geometry=4/2 mutation=delete-current foreground_ack_and_physical_tail_verified_before_release",
+ "native_g03: geometry=4/2 mutation=delete-current exact_version_outcome_physical_generations_and_full_bodies_verified",
+ "native_g03: geometry=16/4 mutation=delete-current reconstruction_paused_before_final_rename",
+ "native_g03: geometry=16/4 mutation=delete-current foreground_ack_and_physical_tail_verified_before_release",
+ "native_g03: geometry=16/4 mutation=delete-current exact_version_outcome_physical_generations_and_full_bodies_verified"
+ ]
+ },
+ "marker-and-batch-delete-final-rename": {
+ "test": "set_disk::ops::heal::heal_result_report_tests::native_marker_and_batch_delete_fence_preserves_acknowledged_version_set",
+ "sourcePath": "crates/ecstore/src/set_disk/ops/heal.rs",
+ "sourceAssertions": [
+ "Real protected external-data reconstruction pauses before final metadata rename in4/2 and16/4 geometries for four independent marker/delete mutations.",
+ "Foreground create-marker, exact marker removal, two-item version deletion with exact per-item ACKs or unversioned deletion and same-key physical tail drain all precede releasing stale Heal.",
+ "Every member retains the exact acknowledged marker/data version set without duplicates; surviving encoded shards and full exact/current bodies remain unchanged, and deleted directories or markers cannot resurrect."
+ ],
+ "witnessKind": "ordered-barrier-milestones",
+ "milestonePrefix": "native_g04: ",
+ "milestones": [
+ "native_g04: geometry=4/2 mutation=create-marker reconstruction_paused_before_final_rename",
+ "native_g04: geometry=4/2 mutation=create-marker exact_foreground_acks_and_physical_tail_verified_before_release",
+ "native_g04: geometry=4/2 mutation=create-marker marker_set_deleted_directories_surviving_shards_and_full_bodies_verified",
+ "native_g04: geometry=4/2 mutation=delete-marker reconstruction_paused_before_final_rename",
+ "native_g04: geometry=4/2 mutation=delete-marker exact_foreground_acks_and_physical_tail_verified_before_release",
+ "native_g04: geometry=4/2 mutation=delete-marker marker_set_deleted_directories_surviving_shards_and_full_bodies_verified",
+ "native_g04: geometry=4/2 mutation=batch-version-delete reconstruction_paused_before_final_rename",
+ "native_g04: geometry=4/2 mutation=batch-version-delete exact_foreground_acks_and_physical_tail_verified_before_release",
+ "native_g04: geometry=4/2 mutation=batch-version-delete marker_set_deleted_directories_surviving_shards_and_full_bodies_verified",
+ "native_g04: geometry=4/2 mutation=unversioned-delete reconstruction_paused_before_final_rename",
+ "native_g04: geometry=4/2 mutation=unversioned-delete exact_foreground_acks_and_physical_tail_verified_before_release",
+ "native_g04: geometry=4/2 mutation=unversioned-delete marker_set_deleted_directories_surviving_shards_and_full_bodies_verified",
+ "native_g04: geometry=16/4 mutation=create-marker reconstruction_paused_before_final_rename",
+ "native_g04: geometry=16/4 mutation=create-marker exact_foreground_acks_and_physical_tail_verified_before_release",
+ "native_g04: geometry=16/4 mutation=create-marker marker_set_deleted_directories_surviving_shards_and_full_bodies_verified",
+ "native_g04: geometry=16/4 mutation=delete-marker reconstruction_paused_before_final_rename",
+ "native_g04: geometry=16/4 mutation=delete-marker exact_foreground_acks_and_physical_tail_verified_before_release",
+ "native_g04: geometry=16/4 mutation=delete-marker marker_set_deleted_directories_surviving_shards_and_full_bodies_verified",
+ "native_g04: geometry=16/4 mutation=batch-version-delete reconstruction_paused_before_final_rename",
+ "native_g04: geometry=16/4 mutation=batch-version-delete exact_foreground_acks_and_physical_tail_verified_before_release",
+ "native_g04: geometry=16/4 mutation=batch-version-delete marker_set_deleted_directories_surviving_shards_and_full_bodies_verified",
+ "native_g04: geometry=16/4 mutation=unversioned-delete reconstruction_paused_before_final_rename",
+ "native_g04: geometry=16/4 mutation=unversioned-delete exact_foreground_acks_and_physical_tail_verified_before_release",
+ "native_g04: geometry=16/4 mutation=unversioned-delete marker_set_deleted_directories_surviving_shards_and_full_bodies_verified"
+ ]
+ }
+ },
+ "cases": {
+ "heal.race-unversioned-overwrite": [
+ "committed-overwrite-cleanup",
+ "stale-read-repair-final-rename"
+ ],
+ "heal.race-versioned-put": [
+ "versioned-read-repair-lock-owner",
+ "explicit-version-append-final-rename"
+ ],
+ "heal.lock-timeout-and-caller-abort": [
+ "versioned-read-repair-lock-owner",
+ "cancelled-physical-unlink-owner",
+ "stale-read-repair-final-rename"
+ ],
+ "heal.race-version-delete": [
+ "explicit-version-delete-final-rename"
+ ],
+ "heal.race-markers-and-batch-delete": [
+ "marker-and-batch-delete-final-rename"
+ ]
+ }
+ },
+ "09fb813a003e7964bd1b893efb7446a3c41bd3bf": {
+ "sourceTree": "754738c3500a70ecd560994a414343072094caf9",
+ "cargoLockSha256": "b4d959f964c13d8ed507657fa4b52752769f96e5cce932845d3d8cec69a5cd56",
+ "sourceFiles": {
+ "crates/ecstore/src/set_disk/ops/object.rs": "d033df47054f054413c007d5e5b94212676411fbb3670403e08358f9152ef443",
+ "crates/ecstore/src/set_disk/ops/heal.rs": "381b63e6206fee996260e2d0a213114140957a5fb4fc26feb0f4533d2be9ec68",
+ "crates/ecstore/src/set_disk/core/io_primitives.rs": "6e50c8ea8b374b8a2d73da740321af25fed6e26c34e5117feca3e5f5891782ab"
+ },
+ "regressions": {
+ "committed-overwrite-cleanup": {
+ "test": "set_disk::ops::object::put_object_tmp_cleanup_tests::deep_heal_preserves_committed_generation_during_old_data_cleanup",
+ "sourcePath": "crates/ecstore/src/set_disk/ops/object.rs",
+ "sourceAssertions": [
+ "Every actual shuffled cleanup slot pauses after new physical generation commit while TailDrained overwrite ACK remains pending.",
+ "Real Deep and shared read-repair reconstruct a missing new-generation encoded part before GET in4/2 and16/4 geometries.",
+ "All old physical generations disappear after cleanup; every new encoded shard, unchanged metadata, overwrite ACK and complete final body remain exact."
+ ],
+ "witnessKind": "ordered-barrier-milestones",
+ "milestonePrefix": "native_g01: ",
+ "milestones": [
+ "native_g01: geometry=4/2 read_repair=false old_cleanup_paused new_generation_committed target_part_missing",
+ "native_g01: geometry=4/2 read_repair=false locked_heal_completed new_generation_part_rebuilt_before_get",
+ "native_g01: geometry=4/2 read_repair=false overwrite_ack_preserved final_physical_generation_and_full_body_verified",
+ "native_g01: geometry=4/2 read_repair=true old_cleanup_paused new_generation_committed target_part_missing",
+ "native_g01: geometry=4/2 read_repair=true locked_heal_completed new_generation_part_rebuilt_before_get",
+ "native_g01: geometry=4/2 read_repair=true overwrite_ack_preserved final_physical_generation_and_full_body_verified",
+ "native_g01: geometry=16/4 read_repair=false old_cleanup_paused new_generation_committed target_part_missing",
+ "native_g01: geometry=16/4 read_repair=false locked_heal_completed new_generation_part_rebuilt_before_get",
+ "native_g01: geometry=16/4 read_repair=false overwrite_ack_preserved final_physical_generation_and_full_body_verified",
+ "native_g01: geometry=16/4 read_repair=true old_cleanup_paused new_generation_committed target_part_missing",
+ "native_g01: geometry=16/4 read_repair=true locked_heal_completed new_generation_part_rebuilt_before_get",
+ "native_g01: geometry=16/4 read_repair=true overwrite_ack_preserved final_physical_generation_and_full_body_verified"
+ ]
+ },
+ "stale-read-repair-final-rename": {
+ "test": "set_disk::ops::heal::heal_result_report_tests::read_repair_commit_aborts_when_object_changes_before_final_rename",
+ "sourcePath": "crates/ecstore/src/set_disk/ops/heal.rs",
+ "sourceAssertions": [
+ "Real missing external shard forces actual shared read-repair data reconstruction.",
+ "Pause immediately before final commit fence; a successor overwrite ACK precedes release.",
+ "Obsolete read-repair returns exact DiskError::Timeout and final current ETag equals acknowledged overwrite."
+ ],
+ "witnessKind": "reviewed-in-test-assertions",
+ "milestonePrefix": null,
+ "milestones": []
+ },
+ "versioned-read-repair-lock-owner": {
+ "test": "set_disk::ops::heal::heal_result_report_tests::object_lock_diagnostics_distinguish_read_repair_from_queued_put",
+ "sourcePath": "crates/ecstore/src/set_disk/ops/heal.rs",
+ "sourceAssertions": [
+ "Historical missing external shard is repaired under actual shared Data-phase lock.",
+ "Queued PUT cannot acquire namespace and later historical GET times out while version-specific owner remains held.",
+ "After release, successor PUT ACK and every original/new version full body remain correct."
+ ],
+ "witnessKind": "reviewed-in-test-assertions",
+ "milestonePrefix": null,
+ "milestones": []
+ },
+ "cancelled-physical-unlink-owner": {
+ "test": "set_disk::ops::heal::heal_result_report_tests::cancelled_heal_backup_cleanup_retains_key_owner_until_unlink_finishes",
+ "sourcePath": "crates/ecstore/src/set_disk/ops/heal.rs",
+ "sourceAssertions": [
+ "Actual external Heal backup unlink pauses in outer, inner and shared read-repair modes.",
+ "Caller abort does not retire namespace commit owner; successor write lock remains pending until physical unlink drains.",
+ "After physical completion, successor obtains lock, owned backup is absent and committed full body is preserved."
+ ],
+ "witnessKind": "reviewed-in-test-assertions",
+ "milestonePrefix": null,
+ "milestones": []
+ },
+ "explicit-version-append-final-rename": {
+ "test": "set_disk::ops::heal::heal_result_report_tests::native_explicit_version_commit_preserves_acknowledged_new_version",
+ "sourcePath": "crates/ecstore/src/set_disk/ops/heal.rs",
+ "sourceAssertions": [
+ "Actual selected-version shared Deep reconstruction pauses before its final rename after releasing the read guard.",
+ "Foreground version mutation ACK and same-key physical write-tail drain precede releasing stale Heal; append succeeds or deleted version remains typed FileVersionNotFound.",
+ "All surviving acknowledged version UUIDs, data dirs, original encoded shards and complete exact/current bodies remain intact in4/2 and16/4; deleted generations do not reappear."
+ ],
+ "witnessKind": "ordered-barrier-milestones",
+ "milestonePrefix": "native_g02: ",
+ "milestones": [
+ "native_g02: geometry=4/2 mutation=append reconstruction_paused_before_final_rename",
+ "native_g02: geometry=4/2 mutation=append foreground_ack_and_physical_tail_verified_before_release",
+ "native_g02: geometry=4/2 mutation=append exact_version_outcome_physical_generations_and_full_bodies_verified",
+ "native_g02: geometry=16/4 mutation=append reconstruction_paused_before_final_rename",
+ "native_g02: geometry=16/4 mutation=append foreground_ack_and_physical_tail_verified_before_release",
+ "native_g02: geometry=16/4 mutation=append exact_version_outcome_physical_generations_and_full_bodies_verified"
+ ]
+ },
+ "explicit-version-delete-final-rename": {
+ "test": "set_disk::ops::heal::heal_result_report_tests::native_explicit_version_commit_refuses_deleted_historical_and_current_versions",
+ "sourcePath": "crates/ecstore/src/set_disk/ops/heal.rs",
+ "sourceAssertions": [
+ "Actual selected-version shared Deep reconstruction pauses before its final rename after releasing the read guard.",
+ "Foreground version mutation ACK and same-key physical write-tail drain precede releasing stale Heal; append succeeds or deleted version remains typed FileVersionNotFound.",
+ "All surviving acknowledged version UUIDs, data dirs, original encoded shards and complete exact/current bodies remain intact in4/2 and16/4; deleted generations do not reappear."
+ ],
+ "witnessKind": "ordered-barrier-milestones",
+ "milestonePrefix": "native_g03: ",
+ "milestones": [
+ "native_g03: geometry=4/2 mutation=delete-historical reconstruction_paused_before_final_rename",
+ "native_g03: geometry=4/2 mutation=delete-historical foreground_ack_and_physical_tail_verified_before_release",
+ "native_g03: geometry=4/2 mutation=delete-historical exact_version_outcome_physical_generations_and_full_bodies_verified",
+ "native_g03: geometry=16/4 mutation=delete-historical reconstruction_paused_before_final_rename",
+ "native_g03: geometry=16/4 mutation=delete-historical foreground_ack_and_physical_tail_verified_before_release",
+ "native_g03: geometry=16/4 mutation=delete-historical exact_version_outcome_physical_generations_and_full_bodies_verified",
+ "native_g03: geometry=4/2 mutation=delete-current reconstruction_paused_before_final_rename",
+ "native_g03: geometry=4/2 mutation=delete-current foreground_ack_and_physical_tail_verified_before_release",
+ "native_g03: geometry=4/2 mutation=delete-current exact_version_outcome_physical_generations_and_full_bodies_verified",
+ "native_g03: geometry=16/4 mutation=delete-current reconstruction_paused_before_final_rename",
+ "native_g03: geometry=16/4 mutation=delete-current foreground_ack_and_physical_tail_verified_before_release",
+ "native_g03: geometry=16/4 mutation=delete-current exact_version_outcome_physical_generations_and_full_bodies_verified"
+ ]
+ },
+ "marker-and-batch-delete-final-rename": {
+ "test": "set_disk::ops::heal::heal_result_report_tests::native_marker_and_batch_delete_fence_preserves_acknowledged_version_set",
+ "sourcePath": "crates/ecstore/src/set_disk/ops/heal.rs",
+ "sourceAssertions": [
+ "Real protected external-data reconstruction pauses before final metadata rename in4/2 and16/4 geometries for four independent marker/delete mutations.",
+ "Foreground create-marker, exact marker removal, two-item version deletion with exact per-item ACKs or unversioned deletion and same-key physical tail drain all precede releasing stale Heal.",
+ "Every member retains the exact acknowledged marker/data version set without duplicates; surviving encoded shards and full exact/current bodies remain unchanged, and deleted directories or markers cannot resurrect."
+ ],
+ "witnessKind": "ordered-barrier-milestones",
+ "milestonePrefix": "native_g04: ",
+ "milestones": [
+ "native_g04: geometry=4/2 mutation=create-marker reconstruction_paused_before_final_rename",
+ "native_g04: geometry=4/2 mutation=create-marker exact_foreground_acks_and_physical_tail_verified_before_release",
+ "native_g04: geometry=4/2 mutation=create-marker marker_set_deleted_directories_surviving_shards_and_full_bodies_verified",
+ "native_g04: geometry=4/2 mutation=delete-marker reconstruction_paused_before_final_rename",
+ "native_g04: geometry=4/2 mutation=delete-marker exact_foreground_acks_and_physical_tail_verified_before_release",
+ "native_g04: geometry=4/2 mutation=delete-marker marker_set_deleted_directories_surviving_shards_and_full_bodies_verified",
+ "native_g04: geometry=4/2 mutation=batch-version-delete reconstruction_paused_before_final_rename",
+ "native_g04: geometry=4/2 mutation=batch-version-delete exact_foreground_acks_and_physical_tail_verified_before_release",
+ "native_g04: geometry=4/2 mutation=batch-version-delete marker_set_deleted_directories_surviving_shards_and_full_bodies_verified",
+ "native_g04: geometry=4/2 mutation=unversioned-delete reconstruction_paused_before_final_rename",
+ "native_g04: geometry=4/2 mutation=unversioned-delete exact_foreground_acks_and_physical_tail_verified_before_release",
+ "native_g04: geometry=4/2 mutation=unversioned-delete marker_set_deleted_directories_surviving_shards_and_full_bodies_verified",
+ "native_g04: geometry=16/4 mutation=create-marker reconstruction_paused_before_final_rename",
+ "native_g04: geometry=16/4 mutation=create-marker exact_foreground_acks_and_physical_tail_verified_before_release",
+ "native_g04: geometry=16/4 mutation=create-marker marker_set_deleted_directories_surviving_shards_and_full_bodies_verified",
+ "native_g04: geometry=16/4 mutation=delete-marker reconstruction_paused_before_final_rename",
+ "native_g04: geometry=16/4 mutation=delete-marker exact_foreground_acks_and_physical_tail_verified_before_release",
+ "native_g04: geometry=16/4 mutation=delete-marker marker_set_deleted_directories_surviving_shards_and_full_bodies_verified",
+ "native_g04: geometry=16/4 mutation=batch-version-delete reconstruction_paused_before_final_rename",
+ "native_g04: geometry=16/4 mutation=batch-version-delete exact_foreground_acks_and_physical_tail_verified_before_release",
+ "native_g04: geometry=16/4 mutation=batch-version-delete marker_set_deleted_directories_surviving_shards_and_full_bodies_verified",
+ "native_g04: geometry=16/4 mutation=unversioned-delete reconstruction_paused_before_final_rename",
+ "native_g04: geometry=16/4 mutation=unversioned-delete exact_foreground_acks_and_physical_tail_verified_before_release",
+ "native_g04: geometry=16/4 mutation=unversioned-delete marker_set_deleted_directories_surviving_shards_and_full_bodies_verified"
+ ]
+ },
+ "multipart-continue-complete-abort-final-rename": {
+ "test": "set_disk::ops::heal::heal_result_report_tests::native_multipart_continue_complete_abort_fences_stale_heal",
+ "sourcePath": "crates/ecstore/src/set_disk/ops/heal.rs",
+ "sourceAssertions": [
+ "Real three-part committed unversioned object and distinct same-key A/B pending uploads: A parts1/2, B parts1/2/3, independent payloads and ACK identities.",
+ "Actual missing committed part forces shared Deep Heal to pause before final rename in4/2 and16/4 geometries; A part3 and B Abort ACK preserve all other committed and A pending shard bytes.",
+ "A Complete ACK and namespace physical tail drain precede release; stale Heal returns exact DiskError::Timeout. All new metadata/parts remain byte exact, old generation and both staging directories absent, final full body equals ordered A ACK payloads.",
+ "Native logical/unversioned scope does not prove full versioned SDK H1/H2 overlap, protocol or fleet execution."
+ ],
+ "witnessKind": "ordered-barrier-milestones",
+ "milestonePrefix": "native_g05: ",
+ "milestones": [
+ "native_g05: geometry=4/2 committed_multipart_and_two_distinct_pending_uploads_seeded",
+ "native_g05: geometry=4/2 real_committed_object_heal_paused_before_final_rename",
+ "native_g05: geometry=4/2 continue_ack_and_other_abort_preserve_pending_parts_and_original_commit",
+ "native_g05: geometry=4/2 complete_ack_and_all_physical_tails_verified_before_release",
+ "native_g05: geometry=4/2 stale_heal_rejected_exact_completed_parts_full_body_and_upload_retirement_verified",
+ "native_g05: geometry=16/4 committed_multipart_and_two_distinct_pending_uploads_seeded",
+ "native_g05: geometry=16/4 real_committed_object_heal_paused_before_final_rename",
+ "native_g05: geometry=16/4 continue_ack_and_other_abort_preserve_pending_parts_and_original_commit",
+ "native_g05: geometry=16/4 complete_ack_and_all_physical_tails_verified_before_release",
+ "native_g05: geometry=16/4 stale_heal_rejected_exact_completed_parts_full_body_and_upload_retirement_verified"
+ ]
+ }
+ },
+ "cases": {
+ "heal.race-unversioned-overwrite": [
+ "committed-overwrite-cleanup",
+ "stale-read-repair-final-rename"
+ ],
+ "heal.race-versioned-put": [
+ "versioned-read-repair-lock-owner",
+ "explicit-version-append-final-rename"
+ ],
+ "heal.lock-timeout-and-caller-abort": [
+ "versioned-read-repair-lock-owner",
+ "cancelled-physical-unlink-owner",
+ "stale-read-repair-final-rename"
+ ],
+ "heal.race-version-delete": [
+ "explicit-version-delete-final-rename"
+ ],
+ "heal.race-markers-and-batch-delete": [
+ "marker-and-batch-delete-final-rename"
+ ],
+ "heal.race-multipart-complete-abort": [
+ "multipart-continue-complete-abort-final-rename"
+ ]
+ }
+ },
+ "757b751482bb970bc321e92059690a7698554358": {
+ "sourceTree": "069b7f321084c52d00ce873d932b8e7f6ca17319",
+ "cargoLockSha256": "b4d959f964c13d8ed507657fa4b52752769f96e5cce932845d3d8cec69a5cd56",
+ "sourceFiles": {
+ "crates/ecstore/src/set_disk/ops/object.rs": "38f3fc4ab7562d4c14202cafb75f36d3e6ea63c376526702042f1139eebc9fa0",
+ "crates/ecstore/src/set_disk/ops/heal.rs": "0b706ac476e02202d5ad217a166eb523abfd3cfd123902a9fb7edf27bd2bf376",
+ "crates/ecstore/src/set_disk/core/io_primitives.rs": "6e50c8ea8b374b8a2d73da740321af25fed6e26c34e5117feca3e5f5891782ab",
+ "crates/ecstore/src/bucket/metadata_sys.rs": "35313244379c20bcfea44fae79c8b4f74fa7cecd60c7ea33cae47399951e8de3",
+ "crates/ecstore/src/set_disk/mod.rs": "4a20398f08dab92f83b2e6fd649de31c5a57c395c9184e2a896cf8990a986de8"
+ },
+ "regressions": {
+ "committed-overwrite-cleanup": {
+ "test": "set_disk::ops::object::put_object_tmp_cleanup_tests::deep_heal_preserves_committed_generation_during_old_data_cleanup",
+ "sourcePath": "crates/ecstore/src/set_disk/ops/object.rs",
+ "sourceAssertions": [
+ "Every actual shuffled cleanup slot pauses after new physical generation commit while TailDrained overwrite ACK remains pending.",
+ "Real Deep and shared read-repair reconstruct a missing new-generation encoded part before GET in4/2 and16/4 geometries.",
+ "All old physical generations disappear after cleanup; every new encoded shard, unchanged metadata, overwrite ACK and complete final body remain exact."
+ ],
+ "witnessKind": "ordered-barrier-milestones",
+ "milestonePrefix": "native_g01: ",
+ "milestones": [
+ "native_g01: geometry=4/2 read_repair=false old_cleanup_paused new_generation_committed target_part_missing",
+ "native_g01: geometry=4/2 read_repair=false locked_heal_completed new_generation_part_rebuilt_before_get",
+ "native_g01: geometry=4/2 read_repair=false overwrite_ack_preserved final_physical_generation_and_full_body_verified",
+ "native_g01: geometry=4/2 read_repair=true old_cleanup_paused new_generation_committed target_part_missing",
+ "native_g01: geometry=4/2 read_repair=true locked_heal_completed new_generation_part_rebuilt_before_get",
+ "native_g01: geometry=4/2 read_repair=true overwrite_ack_preserved final_physical_generation_and_full_body_verified",
+ "native_g01: geometry=16/4 read_repair=false old_cleanup_paused new_generation_committed target_part_missing",
+ "native_g01: geometry=16/4 read_repair=false locked_heal_completed new_generation_part_rebuilt_before_get",
+ "native_g01: geometry=16/4 read_repair=false overwrite_ack_preserved final_physical_generation_and_full_body_verified",
+ "native_g01: geometry=16/4 read_repair=true old_cleanup_paused new_generation_committed target_part_missing",
+ "native_g01: geometry=16/4 read_repair=true locked_heal_completed new_generation_part_rebuilt_before_get",
+ "native_g01: geometry=16/4 read_repair=true overwrite_ack_preserved final_physical_generation_and_full_body_verified"
+ ]
+ },
+ "stale-read-repair-final-rename": {
+ "test": "set_disk::ops::heal::heal_result_report_tests::read_repair_commit_aborts_when_object_changes_before_final_rename",
+ "sourcePath": "crates/ecstore/src/set_disk/ops/heal.rs",
+ "sourceAssertions": [
+ "Real missing external shard forces actual shared read-repair data reconstruction.",
+ "Pause immediately before final commit fence; a successor overwrite ACK precedes release.",
+ "Obsolete read-repair returns exact DiskError::Timeout and final current ETag equals acknowledged overwrite."
+ ],
+ "witnessKind": "reviewed-in-test-assertions",
+ "milestonePrefix": null,
+ "milestones": []
+ },
+ "versioned-read-repair-lock-owner": {
+ "test": "set_disk::ops::heal::heal_result_report_tests::object_lock_diagnostics_distinguish_read_repair_from_queued_put",
+ "sourcePath": "crates/ecstore/src/set_disk/ops/heal.rs",
+ "sourceAssertions": [
+ "Historical missing external shard is repaired under actual shared Data-phase lock.",
+ "Queued PUT cannot acquire namespace and later historical GET times out while version-specific owner remains held.",
+ "After release, successor PUT ACK and every original/new version full body remain correct."
+ ],
+ "witnessKind": "reviewed-in-test-assertions",
+ "milestonePrefix": null,
+ "milestones": []
+ },
+ "cancelled-physical-unlink-owner": {
+ "test": "set_disk::ops::heal::heal_result_report_tests::cancelled_heal_backup_cleanup_retains_key_owner_until_unlink_finishes",
+ "sourcePath": "crates/ecstore/src/set_disk/ops/heal.rs",
+ "sourceAssertions": [
+ "Actual external Heal backup unlink pauses in outer, inner and shared read-repair modes.",
+ "Caller abort does not retire namespace commit owner; successor write lock remains pending until physical unlink drains.",
+ "After physical completion, successor obtains lock, owned backup is absent and committed full body is preserved."
+ ],
+ "witnessKind": "reviewed-in-test-assertions",
+ "milestonePrefix": null,
+ "milestones": []
+ },
+ "explicit-version-append-final-rename": {
+ "test": "set_disk::ops::heal::heal_result_report_tests::native_explicit_version_commit_preserves_acknowledged_new_version",
+ "sourcePath": "crates/ecstore/src/set_disk/ops/heal.rs",
+ "sourceAssertions": [
+ "Actual selected-version shared Deep reconstruction pauses before its final rename after releasing the read guard.",
+ "Foreground version mutation ACK and same-key physical write-tail drain precede releasing stale Heal; append succeeds or deleted version remains typed FileVersionNotFound.",
+ "All surviving acknowledged version UUIDs, data dirs, original encoded shards and complete exact/current bodies remain intact in4/2 and16/4; deleted generations do not reappear."
+ ],
+ "witnessKind": "ordered-barrier-milestones",
+ "milestonePrefix": "native_g02: ",
+ "milestones": [
+ "native_g02: geometry=4/2 mutation=append reconstruction_paused_before_final_rename",
+ "native_g02: geometry=4/2 mutation=append foreground_ack_and_physical_tail_verified_before_release",
+ "native_g02: geometry=4/2 mutation=append exact_version_outcome_physical_generations_and_full_bodies_verified",
+ "native_g02: geometry=16/4 mutation=append reconstruction_paused_before_final_rename",
+ "native_g02: geometry=16/4 mutation=append foreground_ack_and_physical_tail_verified_before_release",
+ "native_g02: geometry=16/4 mutation=append exact_version_outcome_physical_generations_and_full_bodies_verified"
+ ]
+ },
+ "explicit-version-delete-final-rename": {
+ "test": "set_disk::ops::heal::heal_result_report_tests::native_explicit_version_commit_refuses_deleted_historical_and_current_versions",
+ "sourcePath": "crates/ecstore/src/set_disk/ops/heal.rs",
+ "sourceAssertions": [
+ "Actual selected-version shared Deep reconstruction pauses before its final rename after releasing the read guard.",
+ "Foreground version mutation ACK and same-key physical write-tail drain precede releasing stale Heal; append succeeds or deleted version remains typed FileVersionNotFound.",
+ "All surviving acknowledged version UUIDs, data dirs, original encoded shards and complete exact/current bodies remain intact in4/2 and16/4; deleted generations do not reappear."
+ ],
+ "witnessKind": "ordered-barrier-milestones",
+ "milestonePrefix": "native_g03: ",
+ "milestones": [
+ "native_g03: geometry=4/2 mutation=delete-historical reconstruction_paused_before_final_rename",
+ "native_g03: geometry=4/2 mutation=delete-historical foreground_ack_and_physical_tail_verified_before_release",
+ "native_g03: geometry=4/2 mutation=delete-historical exact_version_outcome_physical_generations_and_full_bodies_verified",
+ "native_g03: geometry=16/4 mutation=delete-historical reconstruction_paused_before_final_rename",
+ "native_g03: geometry=16/4 mutation=delete-historical foreground_ack_and_physical_tail_verified_before_release",
+ "native_g03: geometry=16/4 mutation=delete-historical exact_version_outcome_physical_generations_and_full_bodies_verified",
+ "native_g03: geometry=4/2 mutation=delete-current reconstruction_paused_before_final_rename",
+ "native_g03: geometry=4/2 mutation=delete-current foreground_ack_and_physical_tail_verified_before_release",
+ "native_g03: geometry=4/2 mutation=delete-current exact_version_outcome_physical_generations_and_full_bodies_verified",
+ "native_g03: geometry=16/4 mutation=delete-current reconstruction_paused_before_final_rename",
+ "native_g03: geometry=16/4 mutation=delete-current foreground_ack_and_physical_tail_verified_before_release",
+ "native_g03: geometry=16/4 mutation=delete-current exact_version_outcome_physical_generations_and_full_bodies_verified"
+ ]
+ },
+ "marker-and-batch-delete-final-rename": {
+ "test": "set_disk::ops::heal::heal_result_report_tests::native_marker_and_batch_delete_fence_preserves_acknowledged_version_set",
+ "sourcePath": "crates/ecstore/src/set_disk/ops/heal.rs",
+ "sourceAssertions": [
+ "Real protected external-data reconstruction pauses before final metadata rename in4/2 and16/4 geometries for four independent marker/delete mutations.",
+ "Foreground create-marker, exact marker removal, two-item version deletion with exact per-item ACKs or unversioned deletion and same-key physical tail drain all precede releasing stale Heal.",
+ "Every member retains the exact acknowledged marker/data version set without duplicates; surviving encoded shards and full exact/current bodies remain unchanged, and deleted directories or markers cannot resurrect."
+ ],
+ "witnessKind": "ordered-barrier-milestones",
+ "milestonePrefix": "native_g04: ",
+ "milestones": [
+ "native_g04: geometry=4/2 mutation=create-marker reconstruction_paused_before_final_rename",
+ "native_g04: geometry=4/2 mutation=create-marker exact_foreground_acks_and_physical_tail_verified_before_release",
+ "native_g04: geometry=4/2 mutation=create-marker marker_set_deleted_directories_surviving_shards_and_full_bodies_verified",
+ "native_g04: geometry=4/2 mutation=delete-marker reconstruction_paused_before_final_rename",
+ "native_g04: geometry=4/2 mutation=delete-marker exact_foreground_acks_and_physical_tail_verified_before_release",
+ "native_g04: geometry=4/2 mutation=delete-marker marker_set_deleted_directories_surviving_shards_and_full_bodies_verified",
+ "native_g04: geometry=4/2 mutation=batch-version-delete reconstruction_paused_before_final_rename",
+ "native_g04: geometry=4/2 mutation=batch-version-delete exact_foreground_acks_and_physical_tail_verified_before_release",
+ "native_g04: geometry=4/2 mutation=batch-version-delete marker_set_deleted_directories_surviving_shards_and_full_bodies_verified",
+ "native_g04: geometry=4/2 mutation=unversioned-delete reconstruction_paused_before_final_rename",
+ "native_g04: geometry=4/2 mutation=unversioned-delete exact_foreground_acks_and_physical_tail_verified_before_release",
+ "native_g04: geometry=4/2 mutation=unversioned-delete marker_set_deleted_directories_surviving_shards_and_full_bodies_verified",
+ "native_g04: geometry=16/4 mutation=create-marker reconstruction_paused_before_final_rename",
+ "native_g04: geometry=16/4 mutation=create-marker exact_foreground_acks_and_physical_tail_verified_before_release",
+ "native_g04: geometry=16/4 mutation=create-marker marker_set_deleted_directories_surviving_shards_and_full_bodies_verified",
+ "native_g04: geometry=16/4 mutation=delete-marker reconstruction_paused_before_final_rename",
+ "native_g04: geometry=16/4 mutation=delete-marker exact_foreground_acks_and_physical_tail_verified_before_release",
+ "native_g04: geometry=16/4 mutation=delete-marker marker_set_deleted_directories_surviving_shards_and_full_bodies_verified",
+ "native_g04: geometry=16/4 mutation=batch-version-delete reconstruction_paused_before_final_rename",
+ "native_g04: geometry=16/4 mutation=batch-version-delete exact_foreground_acks_and_physical_tail_verified_before_release",
+ "native_g04: geometry=16/4 mutation=batch-version-delete marker_set_deleted_directories_surviving_shards_and_full_bodies_verified",
+ "native_g04: geometry=16/4 mutation=unversioned-delete reconstruction_paused_before_final_rename",
+ "native_g04: geometry=16/4 mutation=unversioned-delete exact_foreground_acks_and_physical_tail_verified_before_release",
+ "native_g04: geometry=16/4 mutation=unversioned-delete marker_set_deleted_directories_surviving_shards_and_full_bodies_verified"
+ ]
+ },
+ "multipart-continue-complete-abort-final-rename": {
+ "test": "set_disk::ops::heal::heal_result_report_tests::native_multipart_continue_complete_abort_fences_stale_heal",
+ "sourcePath": "crates/ecstore/src/set_disk/ops/heal.rs",
+ "sourceAssertions": [
+ "Real three-part committed unversioned object and distinct same-key A/B pending uploads: A parts1/2, B parts1/2/3, independent payloads and ACK identities.",
+ "Actual missing committed part forces shared Deep Heal to pause before final rename in4/2 and16/4 geometries; A part3 and B Abort ACK preserve all other committed and A pending shard bytes.",
+ "A Complete ACK and namespace physical tail drain precede release; stale Heal returns exact DiskError::Timeout. All new metadata/parts remain byte exact, old generation and both staging directories absent, final full body equals ordered A ACK payloads.",
+ "Native logical/unversioned scope does not prove full versioned SDK H1/H2 overlap, protocol or fleet execution."
+ ],
+ "witnessKind": "ordered-barrier-milestones",
+ "milestonePrefix": "native_g05: ",
+ "milestones": [
+ "native_g05: geometry=4/2 committed_multipart_and_two_distinct_pending_uploads_seeded",
+ "native_g05: geometry=4/2 real_committed_object_heal_paused_before_final_rename",
+ "native_g05: geometry=4/2 continue_ack_and_other_abort_preserve_pending_parts_and_original_commit",
+ "native_g05: geometry=4/2 complete_ack_and_all_physical_tails_verified_before_release",
+ "native_g05: geometry=4/2 stale_heal_rejected_exact_completed_parts_full_body_and_upload_retirement_verified",
+ "native_g05: geometry=16/4 committed_multipart_and_two_distinct_pending_uploads_seeded",
+ "native_g05: geometry=16/4 real_committed_object_heal_paused_before_final_rename",
+ "native_g05: geometry=16/4 continue_ack_and_other_abort_preserve_pending_parts_and_original_commit",
+ "native_g05: geometry=16/4 complete_ack_and_all_physical_tails_verified_before_release",
+ "native_g05: geometry=16/4 stale_heal_rejected_exact_completed_parts_full_body_and_upload_retirement_verified"
+ ]
+ },
+ "default-heal-exclusive-owner": {
+ "test": "set_disk::ops::heal::heal_result_report_tests::native_default_heal_owner_fences_queued_put_and_version_delete",
+ "sourcePath": "crates/ecstore/src/set_disk/ops/heal.rs",
+ "sourceAssertions": [
+ "Default locked Deep Heal owns the real exclusive namespace during external part staging before any writer queues; a positive reader control distinguishes exclusivity.",
+ "Real PUT and exact historical DELETE acquisition futures each poll Pending under that owner; after release Heal reconstructs every original shard before foreground publication.",
+ "ACK successor or retained-current generation, exact version population, independent coding-index shard bytes and complete bodies remain correct across4/2 and16/4; this does not claim Admin normalization or live API timing."
+ ],
+ "witnessKind": "ordered-barrier-milestones",
+ "milestonePrefix": "native_a09: ",
+ "milestones": [
+ "native_a09: geometry=4/2 mutation=put default_deep_heal_staged_exclusive_owner_read_blocked",
+ "native_a09: geometry=4/2 mutation=put actual_foreground_namespace_future_pending_original_bytes_unchanged",
+ "native_a09: geometry=4/2 mutation=put heal_reconstructed_before_foreground_commit",
+ "native_a09: geometry=4/2 mutation=put foreground_ack_final_versions_parts_and_full_body_verified",
+ "native_a09: geometry=4/2 mutation=version-delete default_deep_heal_staged_exclusive_owner_read_blocked",
+ "native_a09: geometry=4/2 mutation=version-delete actual_foreground_namespace_future_pending_original_bytes_unchanged",
+ "native_a09: geometry=4/2 mutation=version-delete heal_reconstructed_before_foreground_commit",
+ "native_a09: geometry=4/2 mutation=version-delete foreground_ack_final_versions_parts_and_full_body_verified",
+ "native_a09: geometry=16/4 mutation=put default_deep_heal_staged_exclusive_owner_read_blocked",
+ "native_a09: geometry=16/4 mutation=put actual_foreground_namespace_future_pending_original_bytes_unchanged",
+ "native_a09: geometry=16/4 mutation=put heal_reconstructed_before_foreground_commit",
+ "native_a09: geometry=16/4 mutation=put foreground_ack_final_versions_parts_and_full_body_verified",
+ "native_a09: geometry=16/4 mutation=version-delete default_deep_heal_staged_exclusive_owner_read_blocked",
+ "native_a09: geometry=16/4 mutation=version-delete actual_foreground_namespace_future_pending_original_bytes_unchanged",
+ "native_a09: geometry=16/4 mutation=version-delete heal_reconstructed_before_foreground_commit",
+ "native_a09: geometry=16/4 mutation=version-delete foreground_ack_final_versions_parts_and_full_body_verified"
+ ]
+ },
+ "copy-and-metadata-final-rename": {
+ "test": "set_disk::ops::heal::heal_result_report_tests::native_copy_and_metadata_mutations_preserve_acknowledged_state_during_heal",
+ "sourcePath": "crates/ecstore/src/set_disk/ops/heal.rs",
+ "sourceAssertions": [
+ "Real ECStore cross-key streamed COPY, self metadata replacement and tag update ACK before releasing actual shared Deep reconstruction final-rename pause.",
+ "Unchanged COPY source completes repair; changed destination or metadata/tag generation rejects stale Heal with exact DiskError::Timeout, preserving all acknowledged physical bytes and reclaiming replaced generation.",
+ "Independent fresh Heal reconstructs metadata/tag targets before finalGET; exact source/destination/control payloads, user metadata/content type/tags and acknowledged shard bytes remain preserved in4/2 and16/4."
+ ],
+ "witnessKind": "ordered-barrier-milestones",
+ "milestonePrefix": "native_g06: ",
+ "milestones": [
+ "native_g06: geometry=4/2 mutation=copy-source real_reconstruction_paused_before_final_rename",
+ "native_g06: geometry=4/2 mutation=copy-source foreground_ack_and_all_physical_tails_verified_before_release",
+ "native_g06: geometry=4/2 mutation=copy-source exact_copy_metadata_tags_generations_and_full_bodies_verified",
+ "native_g06: geometry=4/2 mutation=copy-destination real_reconstruction_paused_before_final_rename",
+ "native_g06: geometry=4/2 mutation=copy-destination foreground_ack_and_all_physical_tails_verified_before_release",
+ "native_g06: geometry=4/2 mutation=copy-destination exact_copy_metadata_tags_generations_and_full_bodies_verified",
+ "native_g06: geometry=4/2 mutation=replace-metadata real_reconstruction_paused_before_final_rename",
+ "native_g06: geometry=4/2 mutation=replace-metadata foreground_ack_and_all_physical_tails_verified_before_release",
+ "native_g06: geometry=4/2 mutation=replace-metadata exact_copy_metadata_tags_generations_and_full_bodies_verified",
+ "native_g06: geometry=4/2 mutation=update-tags real_reconstruction_paused_before_final_rename",
+ "native_g06: geometry=4/2 mutation=update-tags foreground_ack_and_all_physical_tails_verified_before_release",
+ "native_g06: geometry=4/2 mutation=update-tags exact_copy_metadata_tags_generations_and_full_bodies_verified",
+ "native_g06: geometry=16/4 mutation=copy-source real_reconstruction_paused_before_final_rename",
+ "native_g06: geometry=16/4 mutation=copy-source foreground_ack_and_all_physical_tails_verified_before_release",
+ "native_g06: geometry=16/4 mutation=copy-source exact_copy_metadata_tags_generations_and_full_bodies_verified",
+ "native_g06: geometry=16/4 mutation=copy-destination real_reconstruction_paused_before_final_rename",
+ "native_g06: geometry=16/4 mutation=copy-destination foreground_ack_and_all_physical_tails_verified_before_release",
+ "native_g06: geometry=16/4 mutation=copy-destination exact_copy_metadata_tags_generations_and_full_bodies_verified",
+ "native_g06: geometry=16/4 mutation=replace-metadata real_reconstruction_paused_before_final_rename",
+ "native_g06: geometry=16/4 mutation=replace-metadata foreground_ack_and_all_physical_tails_verified_before_release",
+ "native_g06: geometry=16/4 mutation=replace-metadata exact_copy_metadata_tags_generations_and_full_bodies_verified",
+ "native_g06: geometry=16/4 mutation=update-tags real_reconstruction_paused_before_final_rename",
+ "native_g06: geometry=16/4 mutation=update-tags foreground_ack_and_all_physical_tails_verified_before_release",
+ "native_g06: geometry=16/4 mutation=update-tags exact_copy_metadata_tags_generations_and_full_bodies_verified"
+ ]
+ }
+ },
+ "cases": {
+ "heal.race-unversioned-overwrite": [
+ "committed-overwrite-cleanup",
+ "stale-read-repair-final-rename"
+ ],
+ "heal.race-versioned-put": [
+ "versioned-read-repair-lock-owner",
+ "explicit-version-append-final-rename"
+ ],
+ "heal.lock-timeout-and-caller-abort": [
+ "versioned-read-repair-lock-owner",
+ "cancelled-physical-unlink-owner",
+ "stale-read-repair-final-rename"
+ ],
+ "heal.race-version-delete": [
+ "explicit-version-delete-final-rename"
+ ],
+ "heal.race-markers-and-batch-delete": [
+ "marker-and-batch-delete-final-rename"
+ ],
+ "heal.race-multipart-complete-abort": [
+ "multipart-continue-complete-abort-final-rename"
+ ],
+ "heal.admin-nolock-safety": [
+ "default-heal-exclusive-owner"
+ ],
+ "heal.race-copy-and-metadata-update": [
+ "copy-and-metadata-final-rename"
+ ]
+ }
+ }
+ }
+}
diff --git a/scripts/validation_heal/race_native_evidence.py b/scripts/validation_heal/race_native_evidence.py
new file mode 100644
index 00000000..314d14a3
--- /dev/null
+++ b/scripts/validation_heal/race_native_evidence.py
@@ -0,0 +1,109 @@
+"""Admit exact-source native subsets without executing the downloaded binary."""
+
+import argparse
+import json
+from pathlib import Path
+import re
+
+try:
+ from . import race_native as suite
+except ImportError:
+ import race_native as suite
+
+
+def read(path, limit=1024**2):
+ suite.require(path.is_file() and not path.is_symlink() and path.stat().st_size < limit,
+ 'native evidence requires a bounded regular original file')
+ return json.loads(path.read_text())
+
+
+def verify(directory, candidate, helper_sha, case_id):
+ directory = Path(directory)
+ suite.require(directory.is_dir() and not directory.is_symlink(), 'native evidence directory differs')
+ suite.require(isinstance(helper_sha, str) and re.fullmatch(r'[0-9a-f]{40}', helper_sha),
+ 'native consumer requires an exact runtime helper')
+ suite.require(isinstance(candidate, dict) and isinstance(candidate.get('source_sha'), str)
+ and re.fullmatch(r'[0-9a-f]{40}', candidate['source_sha'])
+ and isinstance(candidate.get('package_sha256'), str)
+ and re.fullmatch(r'[0-9a-f]{64}', candidate['package_sha256']),
+ 'native consumer has no immutable candidate identity')
+ api = read(directory / 'tool-run.json')
+ suite.require(type(api.get('id')) is int and api['id'] > 0
+ and type(api.get('run_attempt')) is int and api['run_attempt'] > 0
+ and api.get('status') == 'completed' and api.get('conclusion') == 'success'
+ and api.get('event') == 'workflow_dispatch'
+ and api.get('repository', {}).get('full_name') == 'rustfs/rustfs'
+ and api.get('path') == '.github/workflows/rustfs-functional-chain.yml',
+ 'native tools lack a successful exact hosted workflow attempt')
+ tools = directory / 'native-tools'
+ evidence = directory / 'native-evidence'
+ suite.require(evidence.is_dir() and not evidence.is_symlink(), 'native result directory differs')
+ manifest, binary = suite.verify(tools)
+ suite.require(manifest['candidateCommit'] == candidate['source_sha']
+ and manifest['driverCommit'] == api.get('head_sha')
+ and manifest['testingCommit'] == helper_sha
+ and manifest['runId'] == str(api['id'])
+ and manifest['runAttempt'] == str(api['run_attempt']),
+ 'native artifact changed candidate, driver, helper or original attempt')
+ contract = suite.contract_for(candidate['source_sha'])
+ suite.require(case_id in contract['cases'], 'native subset is not reviewed for the selected case')
+ summary = read(evidence / 'race-native-results.json')
+ suite.require(type(summary.get('schemaVersion')) is int and summary['schemaVersion'] == 1
+ and summary.get('candidateCommit') == manifest['candidateCommit']
+ and summary.get('candidateSourceTree') == manifest['candidateSourceTree']
+ and summary.get('binarySha256') == manifest['binarySha256']
+ and summary.get('nativeManifestSha256') == suite.sha(tools / 'race-native.json')
+ and summary.get('requiredNativeSubsetsComplete') is True
+ and summary.get('canonicalCasePassClaimed') is False
+ and summary.get('fleetExecutionClaimed') is False,
+ 'native results changed source, binary, manifest or proof scope')
+ rows = summary.get('results')
+ regressions = contract['regressions']
+ suite.require(isinstance(rows, list) and len(rows) == len(regressions)
+ and all(isinstance(row, dict) for row in rows)
+ and {row.get('label') for row in rows} == set(regressions),
+ 'native results omit or duplicate required exact tests')
+ observed = {}
+ for row in rows:
+ label = row['label']
+ spec = regressions[label]
+ command = row.get('command')
+ suite.require(row.get('test') == spec['test'] and row.get('nativeRegressionPassed') is True
+ and type(row.get('exitCode')) is int and row['exitCode'] == 0
+ and row.get('runtimeEnvironment') == suite.RUNTIME_ENV
+ and isinstance(command, list) and len(command) == 5
+ and isinstance(command[0], str) and Path(command[0]).name == binary.name
+ and command[1:] == [spec['test'], '--exact', '--show-output', '--test-threads=1'],
+ 'native result has no exact one-test passing invocation')
+ log = evidence / (label + '.log')
+ suite.require(log.is_file() and not log.is_symlink() and log.stat().st_size < 16 * 1024**2
+ and suite.sha(log) == row.get('originalLogSha256'), 'original native log bytes differ')
+ output = log.read_text()
+ suite.exact_result(output, spec['test'], row['exitCode'])
+ events = suite.milestones(output, spec)
+ suite.require(row.get('milestones') == events, 'native result invented or omitted barrier events')
+ observed[label] = {'test': spec['test'], 'witnessKind': spec['witnessKind'], 'milestones': events}
+ suite.require(suite.verify(tools)[0] == manifest, 'native artifact changed during read-only admission')
+ return {'caseId': case_id, 'candidateCommit': manifest['candidateCommit'],
+ 'candidatePackageSha256': candidate['package_sha256'], 'candidateSourceTree': manifest['candidateSourceTree'],
+ 'candidateCargoLockSha256': manifest['candidateCargoLockSha256'], 'binarySha256': manifest['binarySha256'],
+ 'runId': api['id'], 'runAttempt': api['run_attempt'], 'driverCommit': api['head_sha'],
+ 'testingCommit': helper_sha, 'selectedContractSha256': manifest['selectedContractSha256'],
+ 'requiredRegressionLabels': contract['cases'][case_id],
+ 'observedRegressions': {label: observed[label] for label in contract['cases'][case_id]},
+ 'requiredNativeSubsetPassed': True, 'wholeCasePassClaimed': False,
+ 'fleetExecutionClaimed': False, 'canonicalCasePassClaimed': False}
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--directory', type=Path, required=True)
+ parser.add_argument('--candidate', type=Path, required=True)
+ parser.add_argument('--testing-commit', required=True)
+ parser.add_argument('--case-id', choices=sorted(suite.CASE_IDS), required=True)
+ args = parser.parse_args()
+ print(json.dumps(verify(args.directory, read(args.candidate), args.testing_commit, args.case_id), sort_keys=True))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/scripts/validation_heal/race_retention.py b/scripts/validation_heal/race_retention.py
new file mode 100644
index 00000000..010dc279
--- /dev/null
+++ b/scripts/validation_heal/race_retention.py
@@ -0,0 +1,249 @@
+"""Isolate stopped ACK-bearing races without replaying writes or restoring old shards."""
+
+import hashlib
+import json
+import re
+
+try:
+ from .heal_probe import require
+ from .healthy import physical_content
+except ImportError:
+ from heal_probe import require
+ from healthy import physical_content
+
+
+CASE = 'heal.race-unversioned-overwrite'
+CASES = {CASE: ('unresolved-g01-race', {'overwrite-vs-deep', 'overwrite-vs-read-repair'}),
+ 'heal.race-versioned-put': ('unresolved-g02-race', {'new-version-vs-historical-heal', 'read-all-versions-during-heal'}),
+ 'heal.race-markers-and-batch-delete': ('unresolved-g04-race', {'create-marker','delete-marker','batch-version-delete','unversioned-delete'}),
+ 'heal.admin-nolock-safety': ('unresolved-a09-race', {'overwrite-nolock', 'delete-nolock'}),
+ 'heal.race-multipart-complete-abort': ('unresolved-g05-race', {'upload-continue', 'complete', 'abort'}),
+ 'heal.race-copy-and-metadata-update': ('unresolved-g06-race', {'copy-source', 'copy-destination', 'replace-metadata', 'update-tags'})}
+
+
+def digest(value):
+ return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(',', ':')).encode()).hexdigest()
+
+
+def validate(plan):
+ multipart = plan.get('kind') == 'unresolved-g05-race'
+ fields = {'kind', 'runId', 'nodes', 'bucket', 'owners', 'businessSceneSha256',
+ 'candidateCommit', 'harnessCommit', 'binarySha256', 'ackFilesSha256'}
+ require(set(plan) == fields | ({'mpuKeys', 'mpuSceneSha256'} if multipart else set())
+ and plan['kind'] in ({kind for kind, _ in CASES.values()} | {'unresolved-i05-timeout', 'unresolved-i05-successor'})
+ and re.fullmatch(r'gh-[1-9][0-9]*-[1-9][0-9]*-h[12]', plan['runId'])
+ and len(plan['nodes']) == len(set(plan['nodes'])) == (1 if plan['runId'].endswith('-h1') else 4)
+ and all(re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9_.-]*', node) for node in plan['nodes'])
+ and re.fullmatch(r'rv-heal-[0-9a-f]{16}', plan['bucket'])
+ and all(re.fullmatch(r'[0-9a-f]{40}', plan[field]) for field in ('candidateCommit', 'harnessCommit'))
+ and all(re.fullmatch(r'[0-9a-f]{64}', plan[field]) for field in ('binarySha256', 'ackFilesSha256'))
+ and set(plan['owners']) == set(plan['businessSceneSha256']) == set(plan['nodes']),
+ 'G01/G02 isolation plan identity, scope or evidence digest differs')
+ for node in plan['nodes']:
+ owner = plan['owners'][node]
+ require(owner.get('runId') == plan['runId'] and owner.get('bucket') == plan['bucket']
+ and (owner.get('runtimeProfile') == 'lock-timeout' and plan['kind'] == 'unresolved-i05-timeout' or
+ owner.get('runtimeProfile') == 'lock-successor' and plan['kind'] == 'unresolved-i05-successor' or
+ owner.get('runtimeProfile') == 'manual'
+ and plan['kind'] not in {'unresolved-i05-timeout', 'unresolved-i05-successor'} or
+ owner.get('runtimeProfile') == 'frozen-race-legacy'
+ and plan['kind'] in {'unresolved-g04-race', 'unresolved-g05-race'})
+ and owner.get('binarySha256') == plan['binarySha256']
+ and owner.get('unit') == 'rustfs-validation-heal-' + plan['runId'] + '.service'
+ and len(owner.get('mounts', [])) == 4
+ and re.fullmatch(r'[0-9a-f]{64}', plan['businessSceneSha256'][node]),
+ 'G01/G02 isolation lost its exact reserved owner or stopped physical scene')
+ require(len({owner['hostname'] for owner in plan['owners'].values()}) == len(plan['nodes']),
+ 'G01/G02 isolation host aliases overlap')
+ if multipart:
+ require(plan['mpuKeys'] == ['objects-0256.bin'] and set(plan['mpuSceneSha256']) == set(plan['nodes'])
+ and all(re.fullmatch(r'[0-9a-f]{64}', value) for value in plan['mpuSceneSha256'].values()),
+ 'G05 isolation lost its exact same-key stopped multipart scene')
+
+
+def archived_plan(artifact, failed_run, candidate):
+ require(type(failed_run) is int and failed_run > 0, 'G01/G02 isolation needs one exact failed run')
+ paths = list(artifact.glob('*/run/*/cases/*.json'))
+ require(len(paths) == 1 and paths[0].stem in CASES, 'G01/G02 isolation cannot omit another case')
+ case = paths[0].stem
+ kind, branches = CASES[case]
+ result = json.loads(paths[0].read_text())
+ provenance = result.get('provenance') or {}
+ require(result.get('case_id') == case and result.get('verdict') == result.get('cleanup_status') == 'FAIL'
+ and provenance.get('source_commit') == candidate['source_sha']
+ and provenance.get('candidate_sha256') == candidate['package_sha256'],
+ 'G01/G02 isolation original failed result or candidate differs')
+ evidence = paths[0].parents[1] / 'evidence' / case
+ summary = json.loads((evidence / 'healthy-summary.json').read_text())
+ rows = summary.get('results', [])
+ retained = [row for row in rows if row.get('cleanup') != 'PASS']
+ require(summary.get('caseId') == case and summary.get('cleanup') == 'FAIL' and len(retained) == 1
+ and retained[0].get('verdict') in ({'FAIL','NOT_OBSERVED'} if kind=='unresolved-g04-race' else {'FAIL'})
+ and retained[0].get('topology') in {'H1', 'H2'}
+ and retained[0].get('branch') in branches
+ and all(row.get('caseId', case if kind == 'unresolved-a09-race' else None) == case for row in rows),
+ 'G01/G02 isolation requires one unmixed retained public branch')
+ row = retained[0]
+ folder = evidence / row['topology'] / row['branch']
+ def one(label):
+ matches = list(folder.glob('*-' + label + '.json'))
+ require(len(matches) == 1, 'G01/G02 isolation lacks one original ' + label)
+ return json.loads(matches[0].read_text())
+ binding, scene, dirty = one('binding'), one('retained-failure-scene'), one('last-ACK-rescue-required')
+ require(re.fullmatch(rf'gh-{failed_run}-[1-9][0-9]*-{row["topology"].lower()}', binding.get('runId', ''))
+ and binding.get('candidateCommit') == candidate['source_sha']
+ and binding.get('candidatePackageSha256') == candidate['package_sha256']
+ and binding.get('candidateBinarySha256') == provenance.get('binary_sha256')
+ and binding.get('harnessCommit') == provenance.get('testing_commit')
+ and dirty.get('state') == 'DIRTY' and dirty.get('namespacesRetained') is True
+ and dirty.get('lastACKPhysicalAuthorityVerified') is False
+ and dirty.get('originalGenerationBlindRestorationProhibited') is True,
+ 'G01/G02 isolation run/provenance or last-ACK refusal differs')
+ reservations = [json.loads(path.read_text()) for path in folder.glob('*-reserved.json')]
+ owners = {item['node']: item['owner'] for item in reservations}
+ require(len(owners) == len(reservations) == len(scene) and set(owners) == set(scene)
+ and all(item.get('reserved') is True for item in reservations), 'G01/G02 isolation lacks all reserved hosts')
+ ack_files = {}
+ multipart = kind == 'unresolved-g05-race'
+ markers = kind == 'unresolved-g04-race'
+ read_journals = (['mpu-read-ACKs', *['final-read-ACKs-node' + str(i) for i in range(1, len(owners) + 1)]]
+ if multipart else ['negative-read-ACKs-node'+str(i) for i in range(1,len(owners)+1)] if markers else
+ ['read-repair-ACKs'] if case == CASE and row['branch'] == 'overwrite-vs-read-repair' else [])
+ if multipart:
+ require({path.name for path in folder.glob('final-read-ACKs-*')} <= set(read_journals),
+ 'G05 isolation has an unbound final peer read journal')
+ if markers:
+ require({path.name for path in folder.glob('negative-read-ACKs-*')} <= set(read_journals),
+ 'G04 isolation has an unbound negative peer read journal')
+ for name in ('seed-ACKs', 'foreground-ACKs', 'historical-read-ACKs', *read_journals):
+ journal = folder / name
+ entries = sorted(journal.glob('*.json'))
+ if name == 'historical-read-ACKs' or name in read_journals or (multipart or markers) and name == 'foreground-ACKs':
+ require(len(entries) <= 1024, 'G02 historical journal exceeds its bound')
+ if not entries: continue
+ require(0 < len(entries) <= 1024, 'G01/G02 isolation lacks its bounded original request/ACK journal')
+ if multipart and name == 'seed-ACKs':
+ require(any(re.fullmatch(r'[0-9]{6}-request\.json', path.name) for path in entries),
+ 'G05 isolation requires an original SDK request, not only a model manifest')
+ for path in entries:
+ if markers and name in read_journals and re.fullmatch(r'[0-9]{6}-negative-proof\.json',path.name):
+ require(path.stat().st_size<=1024**2,'G04 negative proof exceeds its evidence bound')
+ item=json.loads(path.read_text());expected=item.get('expected') or {};params=expected.get('params') or {}
+ require(params.get('Bucket')==binding['bucketPrefix']
+ and params.get('Key') in {f'objects-{i:04d}.bin' for i in range(257)}
+ and expected.get('httpStatus') in (404,405) and item.get('state') in ('PASS','FAIL')
+ and item.get('canonicalCasePassClaimed') is False,
+ 'G04 negative proof belongs to another object or contract')
+ ack_files[name+'/'+path.name]=hashlib.sha256(path.read_bytes()).hexdigest();continue
+ if multipart and re.fullmatch(r'mpu-manifest-[0-9]{4}\.json', path.name):
+ require(path.stat().st_size <= 1024**2, 'G05 isolation multipart manifest exceeds its bound')
+ item = json.loads(path.read_text())
+ require(item.get('bucket') == binding['bucketPrefix']
+ and item.get('key') == 'objects-0256.bin' and item.get('fixture') == 'DM',
+ 'G05 isolation has a foreign multipart manifest')
+ ack_files[name + '/' + path.name] = hashlib.sha256(path.read_bytes()).hexdigest()
+ continue
+ require(re.fullmatch(r'[0-9]{6}-(request|ack)\.json', path.name) and path.stat().st_size <= 1024**2,
+ 'G01/G02 isolation journal contains an unexpected member')
+ item = json.loads(path.read_text())
+ require(item.get('runId') == binding['runId'], 'G01/G02 isolation journal belongs to another run')
+ # A missing/unknown ACK remains unknown. Isolation never infers its
+ # effect or sends another request, including for explicit refusals.
+ operation = item.get('operation') if path.name.endswith('-request.json') else (item.get('observation') or {}).get('operation')
+ if multipart or markers:
+ request_item = item
+ if path.name.endswith('-ack.json'):
+ request_name = item.get('requestFile', '')
+ require(re.fullmatch(r'[0-9]{6}-request\.json', request_name)
+ and request_name == path.name.replace('-ack.json', '-request.json'),
+ 'G05 isolation receipt lacks its exact original request')
+ request_path = journal / request_name
+ require(request_path.is_file() and request_path.stat().st_size <= 1024**2
+ and hashlib.sha256(request_path.read_bytes()).hexdigest() == item.get('requestSha256'),
+ 'G05 isolation receipt changed its original request bytes')
+ request_item = json.loads(request_path.read_text())
+ require(operation is None or operation == request_item.get('operation'),
+ 'G05 isolation receipt reports a different SDK operation')
+ operation = request_item.get('operation')
+ params = request_item.get('params') or {}
+ require(request_item.get('runId') == binding['runId'] and params.get('Bucket') == binding['bucketPrefix'],
+ 'G05 isolation SDK request belongs to another reserved bucket')
+ if operation in {'create_multipart_upload', 'upload_part', 'complete_multipart_upload',
+ 'abort_multipart_upload', 'list_parts', 'get_object', 'head_object', 'get_object_tagging'}:
+ if multipart:require(params.get('Key') == 'objects-0256.bin', 'G05 isolation SDK request changed its frozen multipart key')
+ if markers:
+ keys=([v.get('Key') for v in params.get('Delete',{}).get('Objects',[])] if operation=='delete_objects'
+ else [params.get('Key')] if 'Key' in params else [])
+ require(all(isinstance(key,str) and key in {f'objects-{i:04d}.bin' for i in range(257)} for key in keys)
+ and (operation!='delete_objects' or 0 MAX_BYTES or hashlib.sha256(archive.read_bytes()).hexdigest() != ARCHIVE_SHA256:
+ raise ValueError("released rc archive digest differs")
+ with tarfile.open(archive) as incoming:
+ members = incoming.getmembers()
+ if (len(members) != 3 or {m.name for m in members} != {"rc", "LICENSE-MIT", "LICENSE-APACHE"}
+ or any(not m.isfile() or not 0 < m.size <= MAX_BYTES for m in members)
+ or sum(m.size for m in members) > MAX_BYTES):
+ raise ValueError("released rc archive has unexpected members")
+ binary = incoming.extractfile(next(m for m in members if m.name == "rc")).read()
+ if hashlib.sha256(binary).hexdigest() != BINARY_SHA256:
+ raise ValueError("released rc binary digest differs")
+ if binary[:6] != b"\x7fELF\x02\x01" or binary[18:20] != b"\x3e\x00":
+ raise ValueError("released rc is not Linux x86-64 ELF")
+ target = directory / "rc"
+ with target.open("xb") as output:
+ output.write(binary)
+ target.chmod(0o700)
+ return target.resolve()
+
+
+def prepare(directory):
+ directory.mkdir(mode=0o700, parents=True, exist_ok=False)
+ archive = directory / "rc.tar.gz"
+ subprocess.run(["curl", "--fail", "--silent", "--show-error", "--location", "--proto", "=https",
+ "--proto-redir", "=https", "--connect-timeout", "15", "--max-time", "180",
+ "--max-filesize", str(MAX_BYTES), "--output", str(archive), URL], check=True, timeout=190)
+ target = unpack(archive, directory)
+ result = subprocess.run([str(target), "--version"], capture_output=True, text=True, check=True, timeout=10)
+ manifest = {"release": RELEASE, "sourceCommit": SOURCE, "target": "x86_64-unknown-linux-musl", "archiveUrl": URL,
+ "archiveSha256": ARCHIVE_SHA256, "binarySha256": BINARY_SHA256,
+ "binaryPath": str(target), "versionOutput": result.stdout.strip(),
+ "role": "validation client; not the RustFS service under test"}
+ with (directory / "manifest.json").open("x") as output:
+ json.dump(manifest, output, indent=2)
+ output.write("\n")
+ return target
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--directory", type=Path, required=True)
+ print(prepare(parser.parse_args().directory))
diff --git a/scripts/validation_heal/read_repair.py b/scripts/validation_heal/read_repair.py
new file mode 100644
index 00000000..e12ac928
--- /dev/null
+++ b/scripts/validation_heal/read_repair.py
@@ -0,0 +1,104 @@
+"""E03: one tracked read, a fresh ReadRepair admission and its exact task."""
+
+import hashlib
+import json
+import time
+from urllib.parse import quote
+
+try:
+ from .heal_probe import Probe, payload, require
+ from .scanner_repair import JournalWindow
+except ImportError:
+ from heal_probe import Probe, payload, require
+ from scanner_repair import JournalWindow
+
+
+BRANCHES = ("get-missing", "range-bitrot", "historical-version-read")
+ADMISSION_MESSAGE = "Read-repair heal request admitted"
+ADMISSION_TARGET = "rustfs_ecstore::set_disk::core::io_primitives"
+
+
+def source_tasks(logs, bucket, key):
+ """Consume candidate JSON events; free-text mentions cannot certify a source."""
+ found = set()
+ for node, observed in logs.items():
+ for line in observed.get("journal", "").splitlines():
+ start = line.find("{")
+ if start < 0: continue
+ try: event = json.loads(line[start:])
+ except json.JSONDecodeError: continue
+ if (event.get("target") != ADMISSION_TARGET or event.get("level") != "DEBUG"
+ or event.get("message") != ADMISSION_MESSAGE
+ or event.get("bucket") != bucket or event.get("object") != key):
+ continue
+ require(event.get("admission") == "accepted" and type(event.get("pool_index")) is int
+ and event["pool_index"] == 0 and type(event.get("set_index")) is int and event["set_index"] == 0
+ and type(event.get("part_number")) is int and event["part_number"] == 1
+ and isinstance(event.get("request_id"), str) and event["request_id"],
+ "read-repair event lacks exact accepted task/set/part identity")
+ found.add((node, event["request_id"]))
+ return found
+
+
+def validate_read(observed, entry, *, expected_range=None, body=None):
+ response = observed.get("response") or {}
+ require(observed.get("acknowledged") is True and observed.get("resultUnknown", False) is False,
+ "tracked read failed or has an unknown transport outcome")
+ headers = response.get("ResponseMetadata", {}).get("HTTPHeaders", {})
+ request_id = response.get("ResponseMetadata", {}).get("RequestId") or headers.get("x-request-id") or headers.get("x-amz-request-id")
+ require(isinstance(request_id, str) and request_id, "tracked read omitted a server request ID")
+ version = response.get("VersionId", "null")
+ require(version == entry["versionId"], "tracked read returned another version")
+ if expected_range is None:
+ require(response.get("ResponseMetadata", {}).get("HTTPStatusCode") == 200
+ and observed.get("payload") == entry["payload"], "tracked GET differs from its acknowledged bytes")
+ else:
+ start, end = expected_range
+ selected = body[start:end + 1]
+ require(response.get("ResponseMetadata", {}).get("HTTPStatusCode") == 206
+ and response.get("ContentRange") == f"bytes {start}-{end}/{entry['payload']['size']}"
+ and observed.get("payload") == {"size": len(selected), "sha256": hashlib.sha256(selected).hexdigest()},
+ "tracked Range differs from its exact acknowledged interval")
+ return request_id
+
+
+def tracked_read_task(probe, fleet, bucket, branch, entry):
+ require(fleet.runtime_profile == "read-repair", "read repair requires its explicit isolated runtime profile")
+ window = JournalWindow(fleet, "read-source", event_targets={ADMISSION_TARGET})
+ params = {"Bucket": bucket, "Key": entry["key"]}
+ if entry["versionId"] != "null": params["VersionId"] = entry["versionId"]
+ interval = (0, min(1024**2 + 16, entry["payload"]["size"]) - 1) if branch == "range-bitrot" else None
+ body = payload(f"{bucket}/{entry['key']}/0", entry["payload"]["size"]) if interval else None
+ if interval: params["Range"] = f"bytes={interval[0]}-{interval[1]}"
+ read = fleet.record("single-read-trigger", probe.s3("get_object", params))
+ request_id = validate_read(read, entry, expected_range=interval, body=body)
+ deadline = time.monotonic() + 180
+ fresh = set()
+ while not fresh:
+ events = window.capture()
+ fresh = source_tasks({node: {"journal": "\n".join(json.dumps(event) for event in rows)}
+ for node, rows in events.items()}, bucket, entry["key"])
+ require(time.monotonic() < deadline, "ATTRIBUTION_NOT_OBSERVED: no fresh accepted ReadRepair task for the read target")
+ if not fresh: time.sleep(0.2)
+ require(len(fresh) == 1, "read source has ambiguous task ownership")
+ node, token = next(iter(fresh))
+ # Status queries observe the automatically admitted request. No Admin
+ # start, stop, retry or deep verifier contributes to this repair proof.
+ endpoint = "node" + str(fleet.nodes.index(node) + 1)
+ status_probe = Probe(probe.binding, endpoint)
+ path = "/rustfs/admin/v3/heal/" + quote(bucket, safe="") + "/" + quote(entry["key"], safe="/")
+ while True:
+ observed = fleet.record("read-source-task-status", status_probe.admin(path, [("clientToken", token)], b""))
+ require(observed.get("httpStatus") == 200 and isinstance(observed.get("response"), dict),
+ "ReadRepair task cannot be queried by its exact source ID")
+ status = observed["response"]
+ if status.get("summary") != "running": break
+ require(time.monotonic() < deadline, "ReadRepair task did not finish before the physical repair budget")
+ time.sleep(0.2)
+ require(status.get("summary") == "finished", "ReadRepair task did not report finished execution")
+ fleet.record("read-source-binding", {"source": "read-repair", "s3RequestId": request_id, "taskId": token,
+ "sourceNode": node, "key": entry["key"], "versionId": entry["versionId"],
+ "singleTargetRead": True, "adminMutationUsed": False, "scannerAutoHealAndBestEffortMrfDisabled": True,
+ "durableMrf": "candidate consumer stays initialized; no degraded writes create responsibility in this fixture",
+ "requirement": "still requires exact version receipt, physical bytes, streaming checksum and full S3 proof"})
+ return status
diff --git a/scripts/validation_heal/recipes.json b/scripts/validation_heal/recipes.json
new file mode 100644
index 00000000..a6935225
--- /dev/null
+++ b/scripts/validation_heal/recipes.json
@@ -0,0 +1,3605 @@
+{
+ "schemaVersion": 1,
+ "executionMode": "operator-assisted",
+ "scoring": "manual verdicts; not canonical automated dispatch",
+ "cases": [
+ {
+ "code": "A01",
+ "id": "heal.admin-healthy-noop",
+ "variants": [
+ "normal",
+ "deep"
+ ],
+ "steps": [
+ "用 DB 夹具生成完整 manifest;先对每个对象保存 S3 基线和四盘分片摘要,关闭 scanner/MRF/auto-heal。",
+ "依次执行 START object(scanMode=1、2),WAIT 原 token 直至终态;再 STATUS 同 token 两次。",
+ "对每个对象的 outcome 区分 Normal 的 metadata_healthy 与 Deep 的 verified_healthy;两者均要求 healed=0、unchanged>0,Normal 不得冒充 payload 完整性证明。比较目标/控制对象物理摘要和 S3 清单,重复请求不得新建版本。"
+ ],
+ "commands": [
+ "SEED_DB",
+ "BASELINE",
+ "START",
+ "WAIT",
+ "STATUS",
+ "VERIFY",
+ "PHYSICAL"
+ ],
+ "requiredEvidence": [
+ "preflight",
+ "baseline",
+ "fault",
+ "trigger",
+ "control",
+ "physical",
+ "s3",
+ "recovery",
+ "logs"
+ ],
+ "recovery": "先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。",
+ "verdict": "逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。"
+ },
+ {
+ "code": "A02",
+ "id": "heal.admin-scope-isolation",
+ "variants": [
+ "root",
+ "bucket",
+ "prefix",
+ "object",
+ "pool-set"
+ ],
+ "steps": [
+ "用 DS 在两个桶的 a/、ab/、b/ 写入不同内容;另在未选 set 放控制对象;逐对象预先记录实际 pool/set。",
+ "分别隔离选中对象、相邻前缀及另一桶/未选 set 的一个 part。每个 selector 使用新夹具和故障 ID。",
+ "对象请求路径到完整 key;prefix 路径到 a/ 且 recursive=true;bucket 路径只含桶;root 为 /;pool/set 写入 body。START 后 WAIT。",
+ "先 PHYSICAL:选择范围内全部重建、范围外控制故障仍缺失;然后 GET 验证,避免验证请求提前修好控制故障。root 必须包含整个专用服务清单。"
+ ],
+ "commands": [
+ "SEED_DS",
+ "BASELINE",
+ "FAULT",
+ "START",
+ "WAIT",
+ "PHYSICAL",
+ "VERIFY"
+ ],
+ "requiredEvidence": [
+ "preflight",
+ "baseline",
+ "fault",
+ "trigger",
+ "control",
+ "physical",
+ "s3",
+ "recovery",
+ "logs"
+ ],
+ "recovery": "先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。",
+ "verdict": "逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。"
+ },
+ {
+ "code": "A03",
+ "id": "heal.admin-token-cross-node-status",
+ "variants": [
+ "coordinator-to-peer",
+ "coordinator-to-stable-endpoint"
+ ],
+ "steps": [
+ "在 H2 同一跨节点 set 写入 DB;保存协调者、目标、稳定入口分别对应的端点,不把同一节点的两个别名当跨节点。",
+ "隔离目标节点 part;从 n0 START,记录原始 clientToken;通过 n1 和 stable 对同一路径、同 token 执行 STATUS。",
+ "按同一任务身份对齐三端状态,最多等候冻结的状态收敛预算;终态及核心计数必须一致,404/unknown 不能视为结束。",
+ "WAIT 后先物理证明目标重建,再从所有 S3 入口 VERIFY;保存每个请求实际连接的节点日志。"
+ ],
+ "commands": [
+ "SEED_DB",
+ "FAULT",
+ "START",
+ "STATUS",
+ "WAIT",
+ "PHYSICAL",
+ "VERIFY"
+ ],
+ "requiredEvidence": [
+ "preflight",
+ "baseline",
+ "fault",
+ "trigger",
+ "control",
+ "physical",
+ "s3",
+ "recovery",
+ "logs"
+ ],
+ "recovery": "先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。",
+ "verdict": "逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。"
+ },
+ {
+ "code": "A04",
+ "id": "heal.admin-dry-run",
+ "variants": [
+ "missing-remove0-recreate0",
+ "missing-remove1-recreate0",
+ "bitflip-remove0-recreate1",
+ "bitflip-remove1-recreate1",
+ "versioned-missing",
+ "versioned-bitflip"
+ ],
+ "steps": [
+ "分别用 DB 与 DV 建夹具,冻结全部 versionId/marker;关闭后台来源,不在注入后 GET。",
+ "在原始 part 上做 missing/bitflip,每组独立新目录;保存故障后的 PHYSICAL 快照作为 dry-run 前基线。",
+ "START body 同时传 dryRun=true、scanMode=2 和该实例的 remove/recreate;WAIT 仅接受 dry_run_observed,不能要求 dry-run 持有 incarnation。",
+ "前后所有分片/元数据/marker 字节与版本清单相同;没有 repaired ACK。完成非变更证明后恢复故障,再 VERIFY。"
+ ],
+ "commands": [
+ "SEED_DB",
+ "SEED_DV",
+ "BASELINE",
+ "FAULT",
+ "PHYSICAL",
+ "START",
+ "WAIT",
+ "RECOVER",
+ "VERIFY"
+ ],
+ "requiredEvidence": [
+ "preflight",
+ "baseline",
+ "fault",
+ "trigger",
+ "control",
+ "physical",
+ "s3",
+ "recovery",
+ "logs"
+ ],
+ "recovery": "先保存失败现场及物理/状态证据,停止本例写入与在途任务,再按 RECOVER 撤销本例故障。并发版本已变化时禁止盲目恢复旧 xl.meta;恢复不能充作产品修复。未恢复环境标 DIRTY,保留本例备份和原失败结果。",
+ "verdict": "逐项满足设计公共契约、该分支步骤及附加断言后人工评审。缺资源为 BLOCKED,窗口未命中为 NOT_OBSERVED,产品错误为 FAIL,工具/恢复错误为 INFRA_ERROR;未运行不记 PASS。"
+ },
+ {
+ "code": "A05",
+ "id": "heal.admin-overlap-and-force-start",
+ "variants": [
+ "same-scope",
+ "parent-child",
+ "disjoint",
+ "force-start"
+ ],
+ "steps": [
+ "用 DX 超过 128 项的外部分片夹具注入多对象缺片;预先从固定候选记录 overlap policy(复用 token 或冲突错误),禁止接受任意结果。",
+ "START 递归范围,WINDOW 证明 0 0, "exact failed workflow run required")
+ unfinished_copy = list(artifact.glob("*/run/heal.race-copy-and-metadata-update/cases/*.json"))
+ if any(json.loads(path.read_text()).get("verdict") == "RUNNING" for path in unfinished_copy):
+ return interrupted_race_plan(artifact, failed_run, candidate)
+ if list(artifact.glob("*/run/heal.lock-timeout-and-caller-abort/cases/*.json")):
+ return timeout_retention_plan(artifact, failed_run, candidate)
+ if (list(artifact.glob("*/run/heal.race-unversioned-overwrite/cases/*.json"))
+ or list(artifact.glob("*/run/heal.race-versioned-put/cases/*.json"))
+ or list(artifact.glob("*/run/heal.race-markers-and-batch-delete/cases/*.json"))
+ or list(artifact.glob("*/run/heal.admin-nolock-safety/cases/*.json"))
+ or list(artifact.glob("*/run/heal.race-copy-and-metadata-update/cases/*.json"))
+ or list(artifact.glob("*/run/heal.race-multipart-complete-abort/cases/*.json"))):
+ return race_retention_plan(artifact, failed_run, candidate)
+ if list(artifact.glob("*/run/heal.outcome-counters-and-error-truth/cases/*.json")):
+ if list(artifact.glob("*/run/heal.outcome-counters-and-error-truth/evidence/heal.outcome-counters-and-error-truth/DB/H1/unknown-negative/*-one-way-legacy-write-phase.json")):
+ return acknowledged_legacy_plan(artifact, failed_run, candidate)
+ if list(artifact.glob("*/run/heal.outcome-counters-and-error-truth/evidence/heal.outcome-counters-and-error-truth/DB/H1/unknown-negative/*-original-opaque-byte-zero-bitflip.json")):
+ return interrupted_opaque_plan(artifact, failed_run, candidate)
+ return acknowledged_grace_plan(artifact, failed_run, candidate)
+ if list(artifact.glob("*/run/heal.checkpoint-and-mrf-corruption/cases/*.json")):
+ results = list(artifact.glob("*/run/heal.checkpoint-and-mrf-corruption/cases/*.json"))
+ if any(json.loads(path.read_text()).get("verdict") == "RUNNING" for path in results):
+ return checkpoint_plan(artifact, failed_run, candidate)
+ folder = next(artifact.glob("*/run/heal.checkpoint-and-mrf-corruption/evidence/heal.checkpoint-and-mrf-corruption"))
+ if list((folder / "DB/H2/mrf-checksum").glob("*-actual-original-MRF-CRC-injection.json")):
+ return retained_mrf_recovery_plan(artifact, failed_run, candidate)
+ return rejected_mrf_recovery_plan(artifact, failed_run, candidate)
+ if list(artifact.glob("*/run/heal.bucket-incarnation-recreation/cases/*.json")):
+ return recreation_recovery_plan(artifact, failed_run, candidate)
+ if list(artifact.glob("*/run/heal.enospc-inodes-and-quota/cases/heal.enospc-inodes-and-quota.json")):
+ require(not list(artifact.glob("*/run/heal.object-size-boundaries/cases/*.json")), "mixed failed case recovery artifact")
+ return quota_recovery_plan(artifact, failed_run, candidate)
+ network = list(artifact.glob("*/run/heal.network-partition-and-lock-quorum/cases/heal.network-partition-and-lock-quorum.json"))
+ if network:
+ return network_recovery_plan(artifact, failed_run, candidate)
+ performance = list(artifact.glob("*/run/heal.performance-abba/cases/heal.performance-abba.json"))
+ if performance:
+ return performance_recovery_plan(artifact, failed_run, candidate)
+ results = list(artifact.glob("*/run/heal.object-size-boundaries/cases/heal.object-size-boundaries.json"))
+ require(len(results) == 1, "failed artifact must have one exact D01 canonical result")
+ result = json.loads(results[0].read_text())
+ require(result["verdict"] == "FAIL" and result["cleanup_status"] == "FAIL"
+ and result["provenance"]["source_commit"] == candidate["source_sha"]
+ and result["provenance"]["candidate_sha256"] == candidate["package_sha256"], "failed result candidate or cleanup identity differs")
+ evidence = results[0].parents[1] / "evidence/heal.object-size-boundaries"
+ summary = json.loads((evidence / "healthy-summary.json").read_text())
+ plans = []
+ for topology in summary["results"]:
+ if topology["cleanup"] == "PASS":
+ continue
+ require(topology["topology"] in {"H1", "H2"} and topology["fixture"] == "DB"
+ and "invalid fault ID" in topology.get("failure", {}).get("message", ""), "cleanup failure needs a different recovery procedure")
+ failed = [(name, branch) for name, branch in topology["branches"].items()
+ if "invalid fault ID" in branch.get("failure", {}).get("message", "")]
+ require(len(failed) == 1, "cannot identify one rejected fault branch")
+ branch, _ = failed[0]
+ folder = evidence / "DB" / topology["topology"]
+ files = list(folder.glob("*-physical-before-" + branch + ".json"))
+ faults = list(folder.glob("*-fault-plan-" + branch + ".json"))
+ bindings = list(folder.glob("*-binding.json"))
+ require(len(files) == len(faults) == len(bindings) == 1, "recovery lacks exact archived pre-fault/binding/plan evidence")
+ before, fault, binding = (json.loads(path.read_text()) for path in (files[0], faults[0], bindings[0]))
+ require(re.fullmatch(rf"gh-{failed_run}-[1-9][0-9]*-{topology['topology'].lower()}", binding["runId"]) is not None,
+ "archived run namespace differs from failed Actions run")
+ require(len(fault["faults"]) == 1, "rejected-ID recovery cannot omit earlier faults in the same branch")
+ target = fault["faults"][0]
+ rejected_id = "D01-" + branch + "-0"
+ require(re.fullmatch(r"D01-[a-z0-9-]{49,128}", rejected_id) is not None, "failed ID was not rejected for excessive length")
+ require(target["node"] in before and target["sha256"] and binding["candidateBinarySha256"] == result["provenance"]["binary_sha256"],
+ "fault/candidate physical identity differs")
+ nodes = list(before)
+ require(all(re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", node) for node in nodes), "invalid archived node name")
+ plans.append({"runId": binding["runId"], "nodes": nodes, "node": target["node"], "disk": target["disk"],
+ "rejectedFaultId": rejected_id, "binarySha256": binding["candidateBinarySha256"], "before": before,
+ "expectedSceneSha256": hashlib.sha256(json.dumps(physical_content(before)[target["node"]],
+ sort_keys=True, separators=(",", ":")).encode()).hexdigest()})
+ require(plans, "failed artifact has no supported retained environment")
+ return plans
+
+
+def rejected_mrf_recovery_plan(artifact, failed_run, candidate):
+ """Finalize only a refused first MRF injection with unchanged original bytes."""
+ case = "heal.checkpoint-and-mrf-corruption"
+ results = list(artifact.glob("*/run/*/cases/*.json"))
+ require(len(results) == 1 and results[0].name == case + ".json", "F07 recovery cannot omit another failed case")
+ result = json.loads(results[0].read_text())
+ provenance = result.get("provenance") or {}
+ require(result.get("case_id") == case and result.get("verdict") == "FAIL" and result.get("cleanup_status") == "FAIL"
+ and provenance.get("source_commit") == candidate["source_sha"]
+ and provenance.get("candidate_sha256") == candidate["package_sha256"], "F07 recovery candidate or result differs")
+ evidence = results[0].parents[1] / "evidence" / case
+ summary = json.loads((evidence / "healthy-summary.json").read_text())
+ rows = summary.get("results") or []
+ require(summary.get("caseId") == case and summary.get("cleanup") == "FAIL"
+ and summary.get("complete") is False and summary.get("canonicalCasePassClaimed") is False
+ and rows and all(row.get("cleanup") == "PASS" for row in rows[:-1])
+ and rows[-1].get("topology") == "H2" and rows[-1].get("fixture") == "DB"
+ and rows[-1].get("branch") == "mrf-checksum" and rows[-1].get("verdict") == "FAIL"
+ and rows[-1].get("cleanup") == "FAIL" and "unresolvedMutation" not in rows[-1]
+ and "MRF fault lacks an actual committed active owned responsibility on this exact slot" in
+ rows[-1].get("failure", {}).get("message", ""), "F07 recovery supports only its first rejected MRF injection")
+ folder = evidence / "DB/H2/mrf-checksum"
+ def one(label):
+ found = list(folder.glob("*-" + label + ".json"))
+ require(len(found) == 1, "F07 recovery lacks one exact " + label)
+ return json.loads(found[0].read_text())
+ binding, before, failure = one("binding"), one("original-partial-write-physical"), one("original-physical-failure-scene")
+ original, retained = one("original-stopped-MRF-records"), one("original-MRF-failure-scene")
+ require(re.fullmatch(rf"gh-{failed_run}-[1-9][0-9]*-h2", binding.get("runId", ""))
+ and binding.get("candidateCommit") == candidate["source_sha"]
+ and binding.get("candidatePackageSha256") == candidate["package_sha256"]
+ and binding.get("candidateBinarySha256") == provenance.get("binary_sha256")
+ and binding.get("harnessCommit") == provenance.get("testing_commit")
+ and len(before) == 4 and set(before) == set(failure) == set(original) == set(retained)
+ and physical_content(before) == physical_content(failure) and original == retained
+ and all(value.get("quiescent") is True and value.get("pid") == 0
+ and value.get("binarySha256") == provenance.get("binary_sha256") for value in failure.values()),
+ "F07 rejected injection changed its candidate, physical scene or original MRF records")
+ nodes, bucket = list(failure), binding.get("bucketPrefix", "")
+ require(all(re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", node) for node in nodes)
+ and re.fullmatch(r"rv-heal-[0-9a-f]{16}", bucket), "F07 recovery has invalid owned nodes or bucket")
+ gate = one("gate-recovery")
+ require(gate.get("node") in nodes and gate.get("state") == "CLEAR"
+ and gate.get("originalPermissionsRestored") is True
+ and not list(folder.glob("*-actual-original-MRF-CRC-injection.json"))
+ and not list(folder.glob("*-injected-stopped-MRF-records.json")), "F07 injection or gate recovery is ambiguous")
+ requests, acks = sorted((folder / "fixture").glob("*-request.json")), sorted((folder / "fixture").glob("*-ack.json"))
+ require(len(requests) == len(acks) == 7, "F07 recovery lacks its complete create and six PUT acknowledgements")
+ for seq, (request_path, ack_path) in enumerate(zip(requests, acks), 1):
+ request, ack = json.loads(request_path.read_text()), json.loads(ack_path.read_text())
+ observed = ack.get("observation") or {}
+ require(type(request.get("sequence")) is int and type(ack.get("sequence")) is int
+ and request["sequence"] == ack["sequence"] == seq
+ and request.get("runId") == ack.get("runId") == binding["runId"]
+ and ack.get("requestFile") == request_path.name
+ and ack.get("requestSha256") == hashlib.sha256(request_path.read_bytes()).hexdigest()
+ and observed.get("operation") == request.get("operation") == ("create_bucket" if seq == 1 else "put_object")
+ and observed.get("params") == request.get("params") and request.get("params", {}).get("Bucket") == bucket
+ and observed.get("clientCallInvoked") is True and observed.get("resultUnknown") is False
+ and observed.get("acknowledged") is True and ack.get("operationCheck") == "PASS",
+ "F07 recovery ledger changed identity or contains an unknown mutation")
+ return [{"kind": "rejected-mrf-injection", "runId": binding["runId"], "nodes": nodes, "node": nodes[0],
+ "bucket": bucket, "before": failure, "originalMrf": original,
+ "binarySha256": provenance["binary_sha256"], "rejectedFaultId": "F07-mrf-checksum-0-0-0"}]
+
+
+def retained_mrf_recovery_plan(artifact, failed_run, candidate):
+ """Export and discard an advanced stopped fixture without resurrecting MRF bytes."""
+ try:
+ from . import persistence_mrf, journal_fault
+ except ImportError:
+ import persistence_mrf, journal_fault
+ case = "heal.checkpoint-and-mrf-corruption"
+ paths = list(artifact.glob("*/run/*/cases/*.json"))
+ require(len(paths) == 1 and paths[0].name == case + ".json", "retained MRF recovery requires one unmixed result")
+ result = json.loads(paths[0].read_text())
+ provenance = result.get("provenance") or {}
+ require(result.get("case_id") == case and result.get("verdict") == "FAIL" and result.get("cleanup_status") == "FAIL"
+ and provenance.get("source_commit") == candidate["source_sha"]
+ and provenance.get("candidate_sha256") == candidate["package_sha256"], "retained MRF candidate or result differs")
+ evidence = paths[0].parents[1] / "evidence" / case
+ summary = json.loads((evidence / "healthy-summary.json").read_text())
+ rows = summary.get("results") or []
+ require(summary.get("caseId") == case and summary.get("cleanup") == "FAIL" and summary.get("complete") is False
+ and summary.get("canonicalCasePassClaimed") is False and rows
+ and all(row.get("cleanup") == "PASS" for row in rows[:-1])
+ and rows[-1].get("topology") == "H2" and rows[-1].get("fixture") == "DB"
+ and rows[-1].get("branch") == "mrf-checksum" and rows[-1].get("verdict") == "FAIL"
+ and rows[-1].get("cleanup") == "FAIL" and "unresolvedMutation" not in rows[-1]
+ and rows[-1].get("failure", {}).get("message") ==
+ "NOT_OBSERVED: original MRF corruption has no candidate diagnostic on vm000",
+ "retained MRF recovery supports only its archived post-restart diagnostic refusal")
+ folder = evidence / "DB/H2/mrf-checksum"
+ def one(label):
+ found = [path for path in folder.glob("*-" + label + ".json")
+ if re.fullmatch(r"[0-9]{5}-" + re.escape(label) + r"\.json", path.name)]
+ require(len(found) == 1, "retained MRF recovery lacks one exact " + label)
+ return json.loads(found[0].read_text())
+ binding, failure = one("binding"), one("original-physical-failure-scene")
+ original, injected, retained = (one(label) for label in
+ ("original-stopped-MRF-records", "injected-stopped-MRF-records", "original-MRF-failure-scene"))
+ nodes = [f"vm00{index}" for index in range(4)]
+ bucket = binding.get("bucketPrefix", "")
+ require(re.fullmatch(rf"gh-{failed_run}-[1-9][0-9]*-h2", binding.get("runId", ""))
+ and re.fullmatch(r"rv-heal-[0-9a-f]{16}", bucket)
+ and binding.get("candidateCommit") == candidate["source_sha"]
+ and binding.get("candidatePackageSha256") == candidate["package_sha256"]
+ and binding.get("candidateBinarySha256") == provenance.get("binary_sha256")
+ and binding.get("harnessCommit") == provenance.get("testing_commit")
+ and all(set(scene) == set(nodes) for scene in (failure, original, injected, retained))
+ and all(value.get("quiescent") is True and value.get("pid") == 0
+ and value.get("binarySha256") == provenance.get("binary_sha256") for value in failure.values()),
+ "retained MRF run, candidate, stopped scene or node identity differs")
+ receipts = [json.loads(path.read_text()) for path in folder.glob("*-actual-original-MRF-CRC-injection.json")]
+ require(len(receipts) == 1 and receipts[0].get("node") == "vm000"
+ and receipts[0].get("faultId") == "F07-mrf-checksum-0-0-0" and receipts[0].get("disk") == 0
+ and receipts[0].get("runId") == binding["runId"]
+ and receipts[0].get("binarySha256") == provenance.get("binary_sha256"),
+ "retained MRF recovery cannot omit another injection")
+ persistence_mrf.retained_corruption(original, injected, receipts, bucket)
+ for observed in retained.values():
+ persistence_mrf.indexed(observed)
+ require(observed.get("binarySha256") == provenance.get("binary_sha256"), "retained MRF candidate bytes differ")
+ gate = one("gate-recovery")
+ require(gate.get("node") == "vm003" and gate.get("state") == "CLEAR"
+ and gate.get("originalPermissionsRestored") is True, "retained MRF gate was not restored")
+ recovery = one("recovery")
+ require(recovery.get("cleanup") == "FAIL" and len(recovery.get("errors", [])) == 1
+ and recovery["errors"][0].get("state") == "DIRTY"
+ and recovery["errors"][0].get("namespacesRetained") is True
+ and "product changed or advanced the durable responsibility; refusing old-record resurrection" in
+ recovery["errors"][0].get("error", ""), "retained MRF rescue refusal differs")
+ requests, acks = sorted((folder / "fixture").glob("*-request.json")), sorted((folder / "fixture").glob("*-ack.json"))
+ require(len(requests) == len(acks) == 7, "retained MRF recovery lacks complete fixture acknowledgements")
+ for seq, (request_path, ack_path) in enumerate(zip(requests, acks), 1):
+ request, ack = json.loads(request_path.read_text()), json.loads(ack_path.read_text())
+ observed = ack.get("observation") or {}
+ require(type(request.get("sequence")) is int and type(ack.get("sequence")) is int
+ and request["sequence"] == ack["sequence"] == seq
+ and request.get("runId") == ack.get("runId") == binding["runId"]
+ and ack.get("requestFile") == request_path.name
+ and ack.get("requestSha256") == hashlib.sha256(request_path.read_bytes()).hexdigest()
+ and observed.get("operation") == request.get("operation") == ("create_bucket" if seq == 1 else "put_object")
+ and observed.get("params") == request.get("params") and request.get("params", {}).get("Bucket") == bucket
+ and observed.get("clientCallInvoked") is True and observed.get("resultUnknown") is False
+ and observed.get("acknowledged") is True and ack.get("operationCheck") == "PASS",
+ "retained MRF ledger changed identity or contains an unknown mutation")
+ fault = {key: value for key, value in receipts[0].items() if key not in {"node", "applied", "originalBackupRetained"}}
+ raw = persistence_mrf.indexed(original["vm000"])[0, fault["relativePath"]]
+ require(journal_fault.entry(raw) == fault["before"], "retained MRF original backup identity differs")
+ return [{"kind": "advanced-mrf-fixture", "runId": binding["runId"], "nodes": nodes, "node": nodes[0],
+ "bucket": bucket, "before": failure, "originalMrf": retained,
+ "binarySha256": provenance["binary_sha256"], "faults": {"vm000": [fault]},
+ "originalBackup": {"vm000": {fault["faultId"]: base64.b64encode(raw).decode()}}}]
+
+
+def recreation_recovery_plan(artifact, failed_run, candidate):
+ """Archive a stopped C11 fixture after an exact known DeleteBucket refusal."""
+ case = "heal.bucket-incarnation-recreation"
+ paths = list(artifact.glob("*/run/*/cases/*.json"))
+ require(len(paths) == 1 and paths[0].name == case + ".json",
+ "C11 recovery requires one unmixed failed result")
+ result = json.loads(paths[0].read_text())
+ provenance = result.get("provenance") or {}
+ require(result.get("case_id") == case and result.get("verdict") == "FAIL"
+ and result.get("cleanup_status") == "FAIL"
+ and provenance.get("source_commit") == candidate["source_sha"]
+ and provenance.get("candidate_sha256") == candidate["package_sha256"],
+ "C11 failed-result candidate or cleanup differs")
+ evidence = paths[0].parents[1] / f"evidence/{case}"
+ summary = json.loads((evidence / "healthy-summary.json").read_text())
+ rows = summary.get("results", [])
+ require(summary.get("caseId") == case and summary.get("cleanup") == "FAIL"
+ and summary.get("complete") is False and summary.get("canonicalCasePassClaimed") is False
+ and len(rows) == 1 and rows[0].get("topology") == "H1"
+ and rows[0].get("fixture") == "DV" and rows[0].get("branch") == "old-metadata-rejoin"
+ and rows[0].get("verdict") == "FAIL" and rows[0].get("cleanup") == "FAIL"
+ and rows[0].get("failure", {}).get("type") == "RecordedCallError"
+ and rows[0].get("unresolvedMutation") == {"operation": "normal-S3-delete-and-recreate"},
+ "C11 recovery supports only its first known DeleteBucket refusal")
+ folder = evidence / "H1/old-metadata-rejoin"
+ require(len(list(evidence.rglob("*-binding.json"))) == 1,
+ "C11 recovery cannot omit another bound namespace")
+ def one(label):
+ found = list(folder.glob("*-" + label + ".json"))
+ require(len(found) == 1, "C11 recovery lacks one exact " + label)
+ return json.loads(found[0].read_text())
+ binding, scene, old = one("binding"), one("original-recreation-failure-scene"), one("old-ACK-manifest")
+ require(re.fullmatch(rf"gh-{failed_run}-[1-9][0-9]*-h1", binding.get("runId", ""))
+ and binding.get("candidateCommit") == candidate["source_sha"]
+ and binding.get("candidatePackageSha256") == candidate["package_sha256"]
+ and binding.get("candidateBinarySha256") == provenance.get("binary_sha256")
+ and binding.get("harnessCommit") == provenance.get("testing_commit")
+ and len(scene) == 1 and all(value.get("quiescent") is True and value.get("pid") == 0
+ and value.get("binarySha256") == provenance.get("binary_sha256") for value in scene.values()),
+ "C11 archive lost its stopped run/candidate identity")
+ node, bucket = next(iter(scene)), binding.get("bucketPrefix", "")
+ require(re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", node)
+ and re.fullmatch(r"rv-heal-[0-9a-f]{16}", bucket), "C11 archive has invalid owned host/bucket")
+ recovered = one("gate-recovery")
+ require(recovered.get("node") == node and recovered.get("state") == "CLEAR"
+ and recovered.get("originalPermissionsRestored") is True,
+ "C11 physical gate was not already recovered")
+ requests = sorted((folder / "fixture").glob("*-request.json"))
+ acknowledgements = sorted((folder / "fixture").glob("*-ack.json"))
+ require(len(requests) == len(acknowledgements) and len(requests) > 1,
+ "C11 archive has an unacknowledged or missing S3 call")
+ observed = []
+ for sequence, (request_path, ack_path) in enumerate(zip(requests, acknowledgements), 1):
+ request, ack = json.loads(request_path.read_text()), json.loads(ack_path.read_text())
+ observation = ack.get("observation") or {}
+ require(type(request.get("sequence")) is int and type(ack.get("sequence")) is int
+ and request["sequence"] == ack["sequence"] == sequence
+ and request.get("runId") == ack.get("runId") == binding["runId"]
+ and ack.get("requestFile") == request_path.name
+ and ack.get("requestSha256") == hashlib.sha256(request_path.read_bytes()).hexdigest()
+ and observation.get("operation") == request.get("operation")
+ and observation.get("params") == request.get("params")
+ and observation.get("clientCallInvoked") is True
+ and observation.get("resultUnknown") is False,
+ "C11 S3 ledger changed identity/digest or contains an unknown mutation")
+ require(request.get("params", {}).get("Bucket") in {bucket, bucket + "-control"},
+ "C11 S3 call escaped its archived owned buckets")
+ if sequence < len(requests):
+ require(ack.get("operationCheck") == "PASS" and observation.get("acknowledged") is True,
+ "C11 archive has an earlier rejected S3 operation")
+ observed.append(observation)
+ refusal = observed[-1]
+ response = refusal.get("response") or {}
+ require(rows[0]["failure"].get("message") ==
+ f"unexpected delete_bucket result; inspect ACK {len(requests):06d}, do not silently retry"
+ and refusal.get("operation") == "delete_bucket" and refusal.get("params") == {"Bucket": bucket}
+ and refusal.get("acknowledged") is False
+ and json.loads(acknowledgements[-1].read_text()).get("operationCheck") == "FAIL"
+ and response.get("Error") == {"Code": "InternalError", "Message": "File access denied"}
+ and response.get("ResponseMetadata", {}).get("HTTPStatusCode") == 500,
+ "C11 refusal is uncertain or differs from the audited product error")
+ wanted = {(row["key"], row["versionId"]) for row in old["expected"]}
+ deletes = [(row["params"].get("Key"), row["params"].get("VersionId")) for row in observed[:-1]
+ if row["operation"] == "delete_object" and row["params"]["Bucket"] == bucket]
+ require(wanted and len(wanted) == len(old["expected"]) and len(deletes) >= len(wanted)
+ and len(set(deletes[-len(wanted):])) == len(wanted) and set(deletes[-len(wanted):]) == wanted,
+ "C11 archive lacks every acknowledged original version/marker deletion")
+ require(not list(folder.glob("*-archive-and-detach-original-replay.json"))
+ and not list(folder.glob("*-successor-ACK-bound-S3.json"))
+ and all(row.get("path", "").split("/", 1)[0] in {bucket, bucket + "-control"}
+ for value in scene.values() for row in value["files"]),
+ "C11 retained scene includes another replay transaction or successor")
+ return [{"kind": "rejected-incarnation-delete", "runId": binding["runId"], "nodes": [node],
+ "node": node, "buckets": [bucket, bucket + "-control"], "before": scene,
+ "binarySha256": provenance["binary_sha256"], "originalProductError": response["Error"],
+ "acknowledgedVersionDeletions": len(wanted), "unknownMutationClaimed": False}]
+
+
+def performance_recovery_plan(artifact, failed_run, candidate):
+ """Finalize a performance run that expired before its next fault was applied.
+
+ The performance executor registers a fault before invoking the node-side
+ mutation. If the lease expires at that boundary, cleanup still sees the
+ registration and tries to recover an intent that was never created. This
+ path is deliberately narrower than a general fault rescue: it accepts only
+ the archived phase boundary with no fault or fault-proof record and an
+ unchanged physical scene, then asks the node to perform its ordinary
+ run-owned cleanup.
+ """
+ case = "heal.performance-abba"
+ results = list(artifact.glob(f"*/run/{case}/cases/{case}.json"))
+ require(len(results) == 1, "performance recovery requires one exact J06 result")
+ result = json.loads(results[0].read_text())
+ provenance = result.get("provenance") or {}
+ require(result.get("case_id") == case and result.get("verdict") == "FAIL"
+ and result.get("cleanup_status") == "FAIL"
+ and provenance.get("source_commit") == candidate["source_sha"]
+ and provenance.get("candidate_sha256") == candidate["package_sha256"],
+ "performance failed-result candidate or cleanup differs")
+ evidence = results[0].parents[1] / f"evidence/{case}"
+ summary = json.loads((evidence / "healthy-summary.json").read_text())
+ require(summary.get("cleanup") == "FAIL" and len(summary.get("results", [])) == 1,
+ "performance recovery cannot omit or combine a retained instance")
+ row = summary["results"][0]
+ failure = row.get("failure", {})
+ require(row.get("topology") == "H2" and row.get("verdict") == "FAIL"
+ and row.get("cleanup") == "FAIL"
+ and failure.get("type") == "ValueError"
+ and "apply-fault failed" in failure.get("message", "")
+ and "lease expired" in failure.get("message", ""),
+ "performance recovery only supports a fault lease boundary failure")
+ folder = evidence / "H2"
+ binding_files = list(folder.glob("*-binding.json"))
+ failure_files = list(folder.glob("*-physical-failure.json"))
+ boundary_files = list(folder.glob("*-physical-phase-baseline-g3-p3-B.json"))
+ protocol_files = list(folder.glob("*-phase-protocol-g3-p3-B.json"))
+ require(len(binding_files) == len(failure_files) == len(boundary_files) == len(protocol_files) == 1,
+ "performance recovery lacks one exact retained boundary")
+ binding = json.loads(binding_files[0].read_text())
+ before = json.loads(boundary_files[0].read_text())
+ failure_scene = json.loads(failure_files[0].read_text())
+ protocol = json.loads(protocol_files[0].read_text())
+ require(re.fullmatch(rf"gh-{failed_run}-[1-9][0-9]*-h2", binding.get("runId", ""))
+ and binding.get("candidateBinarySha256") == provenance.get("binary_sha256")
+ and binding.get("harnessCommit") == provenance.get("testing_commit")
+ and protocol.get("group") == 3 and protocol.get("phase") == 3 and protocol.get("mode") == "B"
+ and physical_content(before) == physical_content(failure_scene),
+ "performance retained scene or run identity changed")
+ # A failed remote apply has no durable fault evidence. A fault record or
+ # proof at this boundary would require the normal registered-fault rescue.
+ require(not list(folder.glob("*-fault-g3-p3-B.json"))
+ and not list(folder.glob("*-fault-proof-g3-p3-B.json")),
+ "performance boundary contains an applied fault; refuse shortcut cleanup")
+ nodes = sorted(failure_scene)
+ require(nodes and all(re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", node) for node in nodes),
+ "performance archived node identity is invalid")
+ bucket = binding.get("bucketPrefix")
+ require(isinstance(bucket, str) and re.fullmatch(r"[a-z0-9][a-z0-9.-]+[a-z0-9]", bucket),
+ "performance binding has no exact primary bucket")
+ buckets = {bucket}
+ for value in failure_scene.values():
+ for item in value.get("files", []):
+ name = item.get("path", "").split("/", 1)[0]
+ if name == bucket or name.startswith(bucket + "-"):
+ buckets.add(name)
+ require(buckets == {bucket, bucket + "-load"},
+ "performance retained scene omitted or added a run-owned bucket")
+ return [{"kind": "performance-no-fault-applied", "runId": binding["runId"], "nodes": nodes,
+ "node": nodes[0],
+ "buckets": sorted(buckets),
+ "before": failure_scene, "expectedSceneSha256": hashlib.sha256(json.dumps(
+ physical_content(failure_scene)[nodes[0]], sort_keys=True, separators=(",", ":")
+ ).encode()).hexdigest(), "binarySha256": provenance["binary_sha256"]}]
+
+
+def recovery_scene_matches(current, plan):
+ """A retry may contain only the exact original shards already rescued."""
+ observed = physical_content(current)
+ expected = physical_content(plan.get("failure", plan["before"]))
+ if plan.get("kind") == "interrupted-j02-opaque":
+ if set(observed) != set(expected): return False
+ original = physical_content(plan["before"])
+ for node, scene in observed.items():
+ if scene["formats"] != expected[node]["formats"]: return False
+ current = {(entry["disk"], entry["path"]): entry for entry in scene["files"]}
+ failed = {(entry["disk"], entry["path"]): entry for entry in expected[node]["files"]}
+ before = {(entry["disk"], entry["path"]): entry for entry in original[node]["files"]}
+ if len(current) != len(scene["files"]) or set(current) != set(failed) or set(before) != set(failed): return False
+ if any(value != failed[key] and value != before[key] for key, value in current.items()): return False
+ return True
+ if plan.get("kind") not in {"owned-quota", "interrupted-checkpoint-before-admission"}:
+ return observed == expected
+ original = physical_content(plan["before"])
+ if set(observed) != set(expected):
+ return False
+ for node, scene in observed.items():
+ if scene["formats"] != expected[node]["formats"]:
+ return False
+ files = {(entry["disk"], entry["path"]): entry for entry in scene["files"]}
+ failed = {(entry["disk"], entry["path"]): entry for entry in expected[node]["files"]}
+ before = {(entry["disk"], entry["path"]): entry for entry in original[node]["files"]}
+ if len(files) != len(scene["files"]) or not set(failed) <= set(files) <= set(before):
+ return False
+ if any(files[key] != entry for key, entry in failed.items()):
+ return False
+ if any(files[key] != before[key] for key in set(files) - set(failed)):
+ return False
+ return True
+
+
+def isolate_incomplete_scene(fleet, plan, artifact):
+ """Preserve a drifted fixture; this never restores or finalizes its evidence."""
+ case = "heal.checkpoint-and-mrf-corruption"
+ folders = list(artifact.glob(f"*/run/{case}/evidence/{case}/DB/H2/mrf-checksum"))
+ require(len(folders) == 1, "incomplete fixture lacks its original archive")
+ receipts = [json.loads(path.read_text()) for path in folders[0].glob("*-reserved.json")]
+ require(len(receipts) == len(plan["nodes"]) and {item.get("node") for item in receipts} == set(plan["nodes"]),
+ "incomplete fixture lacks one original reservation per node")
+ owners = {}
+ for item in receipts:
+ owner = item.get("owner") or {}
+ require(item.get("reserved") is True and owner.get("runId") == plan["runId"]
+ and owner.get("bucket") == plan["bucket"] and owner.get("binarySha256") == plan["binarySha256"]
+ and owner.get("runtimeProfile") == "mrf-crash", "incomplete fixture original reservation differs")
+ owners[item["node"]] = owner
+ audits = {}
+ for node in plan["nodes"]:
+ audits[node] = fleet.record("incomplete-scene-preflight", {"node": node,
+ **fleet.remote(node, "retained-scene-audit", expectedOwner=owners[node])})
+ require(audits[node].get("inspectionOnly") is True and audits[node].get("mutated") is False
+ and audits[node].get("credentialContentsRead") is False, "incomplete fixture audit was not read-only")
+ require(any(any(root is None for root in audit["baseline"]["roots"]) for audit in audits.values()),
+ "incomplete-scene isolation requires actual missing roots")
+ # All nodes are frozen and audited before the first rename. Each node keeps
+ # a durable inventory intent and repeats it before every same-device move.
+ for node in plan["nodes"]:
+ proof = fleet.record("incomplete-scene-isolation", {"node": node,
+ **fleet.remote(node, "retained-scene-isolate", expectedOwner=owners[node],
+ expectedBaseline=audits[node]["baseline"])})
+ require(proof.get("activeNamespaceIsolated") is True and proof.get("originalSceneRestored") is False
+ and proof.get("originalEvidenceComplete") is False and proof.get("backupRestorationUsed") is False
+ and proof.get("canonicalPassClaimed") is False
+ and proof.get("baseline") == audits[node]["baseline"], "incomplete fixture isolation failed")
+ for node in plan["nodes"]:
+ proof = fleet.record("incomplete-scene-post-audit", {"node": node,
+ **fleet.remote(node, "retained-scene-audit", expectedOwner=owners[node])})
+ require(proof.get("activeNamespaceIsolated") is True and proof.get("inspectionOnly") is True
+ and proof.get("mutated") is False and proof.get("baseline") == audits[node]["baseline"],
+ "incomplete fixture changed after isolation")
+ return {"runId": plan["runId"], "cleanup": "ISOLATED", "activeNamespaceIsolated": True,
+ "originalEvidenceComplete": False, "originalSceneRestored": False, "originalVerdictRewritten": False,
+ "missingDisks": {node: [index for index, row in enumerate(audits[node]["baseline"]["roots"]) if row is None]
+ for node in plan["nodes"]}}
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--failed-run-id", type=int, required=True)
+ parser.add_argument("--artifact", type=Path, required=True)
+ parser.add_argument("--candidate", type=Path, required=True)
+ parser.add_argument("--evidence", type=Path, required=True)
+ args = parser.parse_args()
+ plans = recovery_plan(args.artifact, args.failed_run_id, json.loads(args.candidate.read_text()))
+ os.environ["RUSTFS_SSH_USER"] = os.environ.get("RUSTFS_SSH_USER") or "azureuser"
+ assigned = (os.environ.get("RUSTFS_NODES") or "vm000 vm001 vm002 vm003").split()
+ require(all(set(plan["nodes"]) <= set(assigned) for plan in plans), "archived nodes do not belong to the current auto-testing fleet")
+ args.evidence.mkdir(parents=True, exist_ok=False)
+ complete = []
+ for plan in plans:
+ fleet = Fleet(plan["nodes"], args.evidence, plan["runId"])
+ if plan.get("buckets"):
+ fleet.snapshot_buckets = plan["buckets"]
+ fleet.record("archived-recovery-plan", {key: value for key, value in plan.items() if key != "before"})
+ if plan.get("kind") == "interrupted-g06-after-authority":
+ complete.append({"failedRunId": args.failed_run_id, **isolate_interrupted_race(fleet, plan)})
+ continue
+ if plan.get("kind") in {"unresolved-g01-race", "unresolved-g02-race", "unresolved-a09-race", "unresolved-g04-race", "unresolved-g05-race", "unresolved-g06-race", "unresolved-i05-timeout", "unresolved-i05-successor"}:
+ complete.append({"failedRunId": args.failed_run_id, **isolate_race_scene(fleet, plan)})
+ continue
+ if plan.get("kind") == "unresolved-network-load":
+ complete.append({"failedRunId": args.failed_run_id, **isolate_network_scene(fleet, plan)})
+ continue
+ if plan.get("kind") == "network-expiry-recovery":
+ for node in plan["nodes"]:
+ fleet.record("network-recovery-stop", {"node": node, **fleet.remote(node, "stop")})
+ for node in plan["nodes"]:
+ proof = fleet.record("network-expiry-recovery", {"node": node,
+ **fleet.remote(node, "network-release", expired=True)})
+ require(proof.get("state") == "CLEAR" and proof.get("rulesRemaining") == 0,
+ "expired network gate was not removed exactly")
+ for node in plan["nodes"]:
+ proof = fleet.record("network-finalization", {"node": node, **fleet.remote(node, "cleanup")})
+ require(proof.get("cleanup") == "PASS" and proof.get("unitStopped") is True
+ and proof.get("rootsRemoved") is True, "network namespace finalization failed")
+ complete.append({"failedRunId": args.failed_run_id, "runId": plan["runId"],
+ "cleanup": "PASS", "originalVerdictRewritten": False})
+ continue
+ states = [fleet.record("namespace-state", {"node": node,
+ **fleet.remote(node, "namespace-state")}) for node in plan["nodes"]]
+ has_retained = any(item.get("retained", {}).get("state")
+ or any(item.get("retained", {}).get("roots", [])) for item in states)
+ all_absent = all(not item["state"]["exists"] and not any(root["exists"] for root in item["roots"]) for item in states)
+ if plan.get("kind") in {"acknowledged-j02-before-replay", "acknowledged-j02-before-legacy-put"}:
+ complete.append({"failedRunId": args.failed_run_id, **isolate_acknowledged_grace(fleet, plan, states)})
+ continue
+ if plan.get("kind") in {"interrupted-checkpoint-before-admission", "interrupted-j02-opaque"}:
+ complete.append({"failedRunId": args.failed_run_id,
+ **restore_and_isolate(fleet, plan, states, recovery_scene_matches)})
+ continue
+ if plan.get("kind") == "advanced-mrf-fixture" and (has_retained or (not all_absent and any(
+ not root["exists"] for item in states for root in item["roots"]))):
+ complete.append({"failedRunId": args.failed_run_id, **isolate_incomplete_scene(fleet, plan, args.artifact)})
+ continue
+ if all(not item["state"]["exists"] and not any(root["exists"] for root in item["roots"])
+ for item in states):
+ for item in states:
+ require(item["service"].get("MainPID") == "0"
+ and item["service"].get("ActiveState") == "inactive"
+ and item["service"].get("SubState") == "dead",
+ "absent namespace still has an active or unknown service; no finalization claimed")
+ fleet.record("already-clean", {"node": item["node"], "cleanup": "PASS",
+ "unitStopped": True, "rootsRemoved": True})
+ complete.append({"failedRunId": args.failed_run_id, "runId": plan["runId"],
+ "cleanup": "PASS", "originalVerdictRewritten": False,
+ "alreadyClean": True})
+ continue
+ current = fleet.snapshot("audited-physical-scene", quiescent=True)
+ require(recovery_scene_matches(current, plan),
+ "retained fleet drifted from its exact archived scene; no recovery performed")
+ fleet.record("retained-service-logs", fleet.remote(plan["node"], "logs"))
+ if plan.get("kind") == "advanced-mrf-fixture":
+ archived = fleet.record("retained-advanced-MRF-records", {node: fleet.remote(node,
+ "mrf-records", quiescent=True) for node in plan["nodes"]})
+ require(archived == plan["originalMrf"], "advanced MRF scene drifted; retain namespace")
+ for action in ("journal-retained-audit", "journal-retained-audit-finalize"):
+ for node in plan["nodes"]:
+ scene = physical_content(plan["before"])[node]
+ proof = fleet.record("audited-finalization" if action.endswith("finalize") else "exported-original-journal-backups", {"node": node,
+ **fleet.remote(node, action, bucket=plan["bucket"], binarySha256=plan["binarySha256"],
+ faults=plan["faults"].get(node, []), originalBackup=plan["originalBackup"].get(node, {}),
+ expectedMrfSha256=hashlib.sha256(json.dumps(plan["originalMrf"][node], sort_keys=True, separators=(",", ":")).encode()).hexdigest(),
+ expectedSceneSha256=hashlib.sha256(json.dumps(scene, sort_keys=True, separators=(",", ":")).encode()).hexdigest())})
+ require(proof.get("originalEvidenceExported") is True and proof.get("backupRestorationUsed") is False
+ and proof.get("productRepairClaimed") is False
+ and (proof.get("readyToFinalize") is True if action.endswith("audit") else
+ proof.get("cleanup") == "PASS" and proof.get("unitStopped") is True and proof.get("rootsRemoved") is True),
+ "advanced MRF evidence export or finalization failed")
+ elif plan.get("kind") == "rejected-mrf-injection":
+ archived = fleet.record("retained-original-MRF-records", {node: fleet.remote(node,
+ "mrf-records", quiescent=True) for node in plan["nodes"]})
+ require(archived == plan["originalMrf"], "F07 original MRF bytes drifted; retain namespace")
+ for node in plan["nodes"]:
+ gate = fleet.record("audited-gate-state", {"node": node, **fleet.remote(node, "gate-state")})
+ require(gate.get("state") == "CLEAR", "F07 member gate is not clear")
+ require(recovery_scene_matches(fleet.snapshot("physical-immediately-before-finalization", quiescent=True), plan),
+ "F07 stopped scene changed during evidence export; retain namespace")
+ for action in ("journal-rejection-audit", "journal-rejection-audit-finalize"):
+ for node in plan["nodes"]:
+ scene = physical_content(plan["before"])[node]
+ proof = fleet.record("audited-finalization" if action.endswith("finalize") else "audited-no-injection", {"node": node,
+ **fleet.remote(node, action, bucket=plan["bucket"],
+ binarySha256=plan["binarySha256"], rejectedFaultId=plan["rejectedFaultId"],
+ expectedMrfSha256=hashlib.sha256(json.dumps(plan["originalMrf"][node], sort_keys=True, separators=(",", ":")).encode()).hexdigest(),
+ expectedSceneSha256=hashlib.sha256(json.dumps(scene, sort_keys=True, separators=(",", ":")).encode()).hexdigest())})
+ require(proof.get("faultNeverApplied") is True and (proof.get("readyToFinalize") is True if action.endswith("audit") else
+ proof.get("cleanup") == "PASS" and proof.get("unitStopped") is True and proof.get("rootsRemoved") is True),
+ "F07 rejected journal namespace audit or finalization failed")
+ elif plan.get("kind") == "rejected-incarnation-delete":
+ require(all(value.get("binarySha256") == plan["binarySha256"] for value in current.values()),
+ "retained candidate binary differs from the archived failed scene")
+ gate = fleet.record("audited-gate-state", fleet.remote(plan["node"], "gate-state"))
+ require(gate.get("state") == "CLEAR", "C11 gate drifted after archived recovery")
+ system = fleet.record("retained-system-bucket-records", fleet.remote(plan["node"],
+ "bucket-record-snapshot", buckets=plan["buckets"]))
+ require(system.get("credentialsRead") is False, "C11 recovery crossed its credential boundary")
+ require(recovery_scene_matches(fleet.snapshot("physical-immediately-before-finalization", quiescent=True), plan),
+ "C11 stopped scene changed during its evidence export; retain namespace")
+ proof = fleet.record("audited-finalization", fleet.remote(plan["node"], "cleanup"))
+ require(proof.get("unitStopped") is True and proof.get("rootsRemoved") is True,
+ "C11 owned namespace finalization was not complete")
+ elif plan.get("kind") == "performance-no-fault-applied":
+ for node in plan["nodes"]:
+ proof = fleet.record("audited-finalization", {"node": node,
+ **fleet.remote(node, "cleanup")})
+ require(proof.get("cleanup") == "PASS" and proof.get("unitStopped") is True
+ and proof.get("rootsRemoved") is True,
+ "performance retained environment finalization failed")
+ elif plan.get("kind") == "owned-quota":
+ for fault in plan["faults"]:
+ fleet.record("quota-original-shard-rescue", fleet.remote(plan["node"], "recover-fault", **fault))
+ require(physical_content(fleet.snapshot("complete-pre-fault-rescue", quiescent=True)) == physical_content(plan["before"]),
+ "quota rescue did not restore every original file; preserve namespace")
+ proof = fleet.record("audited-finalization", fleet.remote(plan["node"], "quota-audit-finalize", **{
+ key: plan[key] for key in ("bucket", "binarySha256", "expectedSceneSha256")}))
+ else:
+ proof = fleet.record("audited-finalization", fleet.remote(plan["node"], "audit-finalize", **{
+ key: plan[key] for key in ("disk", "rejectedFaultId", "binarySha256", "expectedSceneSha256")}))
+ if plan.get("kind") not in {"performance-no-fault-applied", "rejected-mrf-injection", "advanced-mrf-fixture"}:
+ for node in plan["nodes"]:
+ if node != plan["node"]:
+ fleet.record("unchanged-peer-finalization", fleet.remote(node, "cleanup"))
+ require(proof.get("cleanup") == "PASS", "audited environment finalization failed")
+ complete.append({"failedRunId": args.failed_run_id, "runId": plan["runId"], "cleanup": "PASS", "originalVerdictRewritten": False})
+ save(args.evidence / "recovery-summary.json", {
+ "cleanup": "ISOLATED" if any(item["cleanup"] == "ISOLATED" for item in complete) else "PASS",
+ "environments": complete, "canonicalPassClaimed": False})
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/validation_heal/replace_drive.py b/scripts/validation_heal/replace_drive.py
new file mode 100644
index 00000000..ea57e41c
--- /dev/null
+++ b/scripts/validation_heal/replace_drive.py
@@ -0,0 +1,183 @@
+"""Whole private logical-root replacement; the shared mount stays attached."""
+
+import base64
+import hashlib
+import json
+import os
+from pathlib import Path
+import stat
+import subprocess
+import uuid
+
+if __package__:
+ from .drive_gate import DENY_DAC_CAPS, READ_PROBE_SOURCE, process_identity
+else:
+ from drive_gate import DENY_DAC_CAPS, READ_PROBE_SOURCE, process_identity
+
+
+def require(value, message):
+ if not value: raise ValueError(message)
+
+
+def digest(path):
+ info = path.lstat()
+ require(stat.S_ISREG(info.st_mode) and info.st_nlink == 1 and info.st_size <= 64 * 1024**2,
+ "replacement archive contains a link or unsupported file")
+ value = hashlib.sha256()
+ with path.open("rb") as stream:
+ while chunk := stream.read(1024**2): value.update(chunk)
+ return value.hexdigest()
+
+
+def tree_identity(root):
+ info = root.lstat()
+ require(stat.S_ISDIR(info.st_mode) and info.st_uid == 0 and stat.S_IMODE(info.st_mode) == 0o700,
+ "replacement original root is not private/root-owned")
+ rows = []
+ for folder, children, names in os.walk(root, followlinks=False):
+ for name in (*children, *names):
+ path = Path(folder) / name
+ item = path.lstat()
+ require(item.st_dev == info.st_dev and not path.is_symlink(), "replacement archive crossed a mount or link")
+ if stat.S_ISDIR(item.st_mode): continue
+ require(len(rows) < 16384, "replacement archive exceeds the registered file budget")
+ rows.append({"path": path.relative_to(root).as_posix(), "size": item.st_size, "sha256": digest(path)})
+ value = json.dumps(sorted(rows, key=lambda item: item["path"]), sort_keys=True, separators=(",", ":")).encode()
+ return {"sha256": hashlib.sha256(value).hexdigest(), "files": len(rows), "device": info.st_dev, "inode": info.st_ino}
+
+
+def checked_replacement(state, roots):
+ path = state / "disk-replacement.json"
+ if not path.exists(): return None
+ info = path.lstat()
+ require(stat.S_ISREG(info.st_mode) and info.st_uid == 0 and info.st_nlink == 1
+ and stat.S_IMODE(info.st_mode) == 0o600 and info.st_size < 4096, "invalid replacement ownership marker")
+ value = json.loads(path.read_text())
+ disk = value.get("disk")
+ require(value.get("schemaVersion") == 1 and type(disk) is int and 0 <= disk < len(roots)
+ and value.get("dataPath") == str(roots[disk] / "data")
+ and value.get("backupPath") == str(roots[disk] / "quarantine" / "old-disk")
+ and value.get("phase") == "armed" and isinstance(value.get("original"), dict),
+ "replacement marker moved outside its exact private member")
+ quarantine = roots[disk] / "quarantine"
+ info = quarantine.lstat()
+ require(stat.S_ISDIR(info.st_mode) and info.st_uid == 0 and stat.S_IMODE(info.st_mode) == 0o700
+ and info.st_dev == roots[disk].lstat().st_dev, "replacement quarantine crossed a link, mount or owner")
+ return value
+
+
+def begin(state, roots, disk, expected_format_sha256):
+ require(type(disk) is int and 0 <= disk < len(roots) and checked_replacement(state, roots) is None,
+ "invalid or repeated logical disk replacement")
+ data, backup = roots[disk] / "data", roots[disk] / "quarantine" / "old-disk"
+ quarantine = backup.parent.lstat()
+ require(stat.S_ISDIR(quarantine.st_mode) and quarantine.st_uid == 0 and stat.S_IMODE(quarantine.st_mode) == 0o700
+ and quarantine.st_dev == roots[disk].lstat().st_dev, "replacement quarantine is not an exact private same-mount directory")
+ require(not backup.exists() and not backup.is_symlink(), "replacement original backup already exists")
+ original = tree_identity(data)
+ format_path = data / ".rustfs.sys" / "format.json"
+ require(digest(format_path) == expected_format_sha256, "original disk format differs before whole-root replacement")
+ value = {"schemaVersion": 1, "phase": "armed", "disk": disk, "dataPath": str(data),
+ "backupPath": str(backup), "original": original, "originalFormatSha256": expected_format_sha256}
+ fd = os.open(state / "disk-replacement.json", os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
+ with os.fdopen(fd, "w") as stream:
+ json.dump(value, stream); stream.flush(); os.fsync(stream.fileno())
+ os.rename(data, backup)
+ data.mkdir(mode=0o700)
+ for folder in (roots[disk], backup.parent):
+ fd = os.open(folder, os.O_RDONLY | os.O_DIRECTORY)
+ try: os.fsync(fd)
+ finally: os.close(fd)
+ require(tree_identity(backup) == original and not any(data.iterdir()), "replacement did not retain exact original bytes and an empty new root")
+ return {**value, "oldRootMovedIntact": True, "newDataRootEmpty": True,
+ "sharedMountDetached": False, "backupRestorationUsed": False}
+
+
+def attest(state, roots):
+ value = checked_replacement(state, roots)
+ if value is None: return {"replacementAbsent": True}
+ backup, data = Path(value["backupPath"]), Path(value["dataPath"])
+ # An interrupted begin remains recoverable only when the original tree is
+ # still at one of the two exact intended roots; otherwise retain the run.
+ original_path = backup if backup.exists() else data
+ require(tree_identity(original_path) == value["original"], "original replacement archive drifted; retain namespace")
+ return {**value, "originalArchiveUnchanged": True, "originalStillAtDataRoot": original_path == data}
+
+
+def install_foreign(state, roots, raw_base64, expected_sha256):
+ value = attest(state, roots)
+ require(not value.get("replacementAbsent") and not value["originalStillAtDataRoot"], "foreign format requires an isolated original member")
+ data = Path(value["dataPath"])
+ require(not any(data.iterdir()), "foreign format requires the witnessed empty replacement root")
+ raw = base64.b64decode(raw_base64, validate=True)
+ require(len(raw) < 65536 and hashlib.sha256(raw).hexdigest() == expected_sha256, "foreign donor bytes differ from their exported digest")
+ donor = json.loads(raw)
+ original = json.loads((Path(value["backupPath"]) / ".rustfs.sys/format.json").read_bytes())
+ require(donor.get("version") == "1" and donor.get("format") == "xl" and donor.get("xl", {}).get("version") == "3",
+ "foreign donor is not a supported released candidate format")
+ donor_id, original_id = uuid.UUID(donor["id"]), uuid.UUID(original["id"])
+ members = [member for group in donor["xl"]["sets"] for member in group]
+ original_members = {member for group in original["xl"]["sets"] for member in group}
+ require(donor_id.int != 0 and donor_id != original_id and donor["xl"]["this"] in members
+ and len(members) == len(set(members)) and all(uuid.UUID(member).int != 0 for member in members)
+ and not set(members) & original_members, "foreign donor reused the original cluster or a member identity")
+ folder = data / ".rustfs.sys"
+ folder.mkdir(mode=0o700)
+ descriptor = os.open(folder / "format.json", os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
+ with os.fdopen(descriptor, "wb") as stream:
+ stream.write(raw); stream.flush(); os.fsync(stream.fileno())
+ descriptor = os.open(folder, os.O_RDONLY | os.O_DIRECTORY)
+ try: os.fsync(descriptor)
+ finally: os.close(descriptor)
+ return {"disk": value["disk"], "foreignFormatSha256": expected_sha256, "foreignClusterId": str(donor_id),
+ "foreignDiskUUID": donor["xl"]["this"], "actualDonorBytesCopiedUnchanged": True,
+ "originalArchiveUnchanged": True, "syntheticFormatOrUUIDUsed": False}
+
+
+def restore_original(state, roots):
+ value = attest(state, roots)
+ require(not value.get("replacementAbsent") and not value["originalStillAtDataRoot"], "original member is not at its witnessed archive")
+ backup, data = Path(value["backupPath"]), Path(value["dataPath"])
+ rejected = backup.parent / "rejected-root"
+ require(not rejected.exists() and not rejected.is_symlink(), "rejected replacement archive already exists")
+ rejected_identity = tree_identity(data)
+ # Retain the rejected scene; this recovery is explicitly not a product
+ # rebuild. The existing intent still makes a partial rename recoverable.
+ os.rename(data, rejected)
+ os.rename(backup, data)
+ require(tree_identity(data) == value["original"] and tree_identity(rejected) == rejected_identity,
+ "original-member recovery changed its frozen bytes or rejected scene")
+ descriptor = os.open(data.parent, os.O_RDONLY | os.O_DIRECTORY)
+ try: os.fsync(descriptor)
+ finally: os.close(descriptor)
+ (state / "disk-replacement.json").unlink()
+ return {"disk": value["disk"], "originalRestoredExactly": True, "rejectedScene": rejected_identity,
+ "backupRestorationUsed": True, "productRepairClaimed": False, "sharedMountDetached": False}
+
+
+def start_properties(state, roots):
+ value = checked_replacement(state, roots)
+ if value is None: return []
+ require(Path(value["backupPath"]).is_dir(), "replacement original has not moved to quarantine")
+ return ["--property=PrivateMounts=true", "--property=InaccessiblePaths=" + str(Path(value["backupPath"]).parent),
+ "--property=CapabilityBoundingSet=~CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH"]
+
+
+def probe(state, roots, pid):
+ value = attest(state, roots)
+ require(not value.get("replacementAbsent") and not value["originalStillAtDataRoot"]
+ and type(pid) is int and pid > 1, "live replacement namespace required")
+ identity = process_identity(pid)
+ require(identity["uid"] == 0 and identity["gid"] == 0 and identity["capEff"] & DENY_DAC_CAPS == 0
+ and Path(f"/proc/{pid}/ns/mnt").readlink() != Path("/proc/self/ns/mnt").readlink(),
+ "replacement backup is not isolated in the actual service identity/namespace")
+ result = subprocess.run(["nsenter", "--mount=/proc/" + str(pid) + "/ns/mnt", "setpriv",
+ "--bounding-set=-dac_override,-dac_read_search", "python3", "-c", READ_PROBE_SOURCE,
+ value["backupPath"] + "/.rustfs.sys/format.json"], capture_output=True, text=True, timeout=15)
+ require(result.returncode == 0, "replacement service read probe did not finish")
+ observed = json.loads(result.stdout)
+ require(observed.get("errno") == 13 and observed.get("uid") == identity["uid"] and observed.get("gid") == identity["gid"],
+ "service can read old replacement backup or lacks exact EACCES")
+ return {"disk": value["disk"], "backupPath": value["backupPath"], "serviceReadDenied": True,
+ "sameServiceIdentity": True, "privateMountNamespace": True, "readProbe": observed,
+ "originalArchiveUnchanged": True, "backupRestorationUsed": False}
diff --git a/scripts/validation_heal/replacement.py b/scripts/validation_heal/replacement.py
new file mode 100644
index 00000000..ff001762
--- /dev/null
+++ b/scripts/validation_heal/replacement.py
@@ -0,0 +1,404 @@
+"""B03: full private-drive replacement with original same-slot shard oracles."""
+
+import copy
+import time
+import uuid
+
+try:
+ from .heal_probe import Probe, canonical, compare_inventories, require
+ from .heal_workload import verify_seed
+ from .healthy import validate_topology
+ from .shards import files, map_target, run_task
+ from .fleet_node import PROCESS_FIELDS
+ from .scanner_repair import JournalWindow, SOURCE_TARGETS, event_time
+except ImportError:
+ from heal_probe import Probe, canonical, compare_inventories, require
+ from heal_workload import verify_seed
+ from healthy import validate_topology
+ from shards import files, map_target, run_task
+ from fleet_node import PROCESS_FIELDS
+ from scanner_repair import JournalWindow, SOURCE_TARGETS, event_time
+
+
+BRANCHES = ("new-disk-data", "new-disk-versioned")
+AUTOMATIC_BRANCHES = ("replacement-unversioned", "replacement-versioned-marker")
+GRACEFUL_BRANCHES = ("target-partial-graceful", "versioned-target-partial")
+CRASH_BRANCHES = ("target-partial-sigkill", "target-partial-sigkill-versioned")
+COORDINATOR_BRANCHES = ("replacement-coordinator-kill",)
+FORMAT_SECONDS = 90
+REBUILD_SECONDS = 300
+STATUS_PATH = "/rustfs/admin/v4/heal/replacement-recovery"
+
+
+def replacement_records(probe, fleet, label):
+ observed = {}
+ for endpoint in probe.binding["endpoints"]:
+ value = fleet.record(label, {"endpoint": endpoint,
+ **Probe(probe.binding, endpoint).admin(STATUS_PATH, [], b"", method="GET")})
+ body = value.get("response", {})
+ cluster = body.get("cluster", {})
+ require(value.get("httpStatus") == 200 and type(body.get("contractVersion")) is int
+ and body["contractVersion"] == 2 and body.get("scope") == "cluster_survivor_disks"
+ and cluster.get("definitive") is True and cluster.get("reason") is None
+ and isinstance(cluster.get("records"), list), "replacement source status is partial, unknown or incompatible")
+ observed[endpoint] = cluster["records"]
+ return observed
+
+
+def generation(records, endpoint, earliest, *, original=None, completed=False):
+ require(isinstance(records, list) and len(records) == 1 and isinstance(records[0], dict),
+ "replacement did not expose exactly one owned target generation")
+ record = records[0]
+ token = record.get("taskId")
+ require(isinstance(token, str) and str(uuid.UUID(token)) == token and record.get("generation") == token
+ and record.get("setDiskId") == "pool_0_set_0" and record.get("targetSlots") == [endpoint],
+ "replacement generation is not bound to the exact target endpoint/set")
+ binding = {name: record[name] for name in ("taskId", "generation", "setDiskId", "targetSlots")}
+ require(original is None or binding == original, "automatic repair crossed replacement generations or target members")
+ require(record.get("state") in {"waiting_for_replacement", "running", "cleanup_pending", "completed"}
+ and record.get("reason") is None, "replacement generation is incomplete, unrecoverable or unknown")
+ if record["state"] == "completed":
+ require(type(record.get("verifiedAt")) is int and earliest <= record["verifiedAt"] <= int(time.time()) + 5,
+ "replacement completion is stale or has no actual verification timestamp")
+ else: require(record.get("verifiedAt") is None, "unfinished replacement claimed a completion timestamp")
+ require(not completed or record["state"] == "completed", "replacement generation has not completed its responsibility")
+ return binding
+
+
+def target_progress(snapshot, proofs):
+ indexed, complete, external_complete, external_pending = files(snapshot), [], 0, 0
+ for target in proofs:
+ identity = target["node"], target["disk"], f"{target['bucket']}/{target['key']}/xl.meta"
+ metadata = indexed.get(identity, {}).get("metadata")
+ matched = None if not metadata else next((version for version in metadata["versions"]
+ if (version.get("versionId") or "null") == target["versionId"]), None)
+ valid = False
+ if matched is not None:
+ logical = {name: matched.get(name) for name in target["logical"] if name != "erasure"}
+ logical["erasure"] = {name: matched.get("erasure", {}).get(name) for name in target["logical"]["erasure"]}
+ require(logical == target["logical"] and matched.get("role") == target["role"]
+ and matched.get("erasure", {}).get("index") == target["index"],
+ "partial replacement changed the acknowledged version authority or original shard role")
+ if target.get("zeroProof"):
+ require(matched.get("size") == 0 and not matched.get("inline"), "replacement invented data for a zero-length ACK")
+ valid = True
+ elif target.get("inlineProof"):
+ inline = matched.get("inline") or {}
+ require(inline.get("length") == target["inlineValue"]["length"]
+ and inline.get("sha256") == target["inlineValue"]["sha256"], "partial replacement committed incorrect inline bytes")
+ valid = True
+ else:
+ entry = indexed.get((target["node"], target["disk"], target["relativePath"]))
+ if entry:
+ require(entry["sha256"] == target["sha256"] and entry["size"] == target["size"],
+ "partial replacement committed incorrect original same-role shard bytes")
+ valid = True
+ if valid: complete.append((target["key"], target["versionId"]))
+ if not target.get("zeroProof") and not target.get("inlineProof"):
+ if valid: external_complete += 1
+ else: external_pending += 1
+ return {"completeDataVersions": complete, "dataVersions": len(proofs),
+ "externalComplete": external_complete, "externalPending": external_pending,
+ "partial": 0 < len(complete) < len(proofs) and external_complete > 0 and external_pending > 0}
+
+
+def target_restart_identity(before, after, target):
+ require(set(before) == set(after) and target in before and len(before) == 4, "target restart omitted an actual H2 peer")
+ for node, old in before.items():
+ new = after[node]
+ ignored = {"pid", "startTicks", "mountNamespace"} if node == target else set()
+ require(all(new.get(name) == old.get(name) for name in PROCESS_FIELDS if name not in ignored),
+ "target restart changed candidate, boot, unit, settings or a non-target peer")
+ if node == target:
+ require(type(new.get("pid")) is int and new["pid"] > 1 and type(new.get("startTicks")) is int
+ and isinstance(new.get("mountNamespace"), str) and new["mountNamespace"]
+ and (new["pid"], new["startTicks"]) != (old["pid"], old["startTicks"]), "replacement target was not actually restarted")
+ return {"onlyOriginalTargetProcessRestarted": True, "sameBootCandidateUnitSettingsAndRoots": True,
+ "newPrivateMountNamespaceMustProveArchiveRefusal": True}
+
+
+def interrupt_target(fleet, node, proofs, mode, original_format):
+ before = fleet.record("replacement-processes-before-interruption", {peer: fleet.remote(peer, "process") for peer in fleet.nodes})
+ identity = before[node]
+ fleet.frozen_processes[node] = identity
+ fleet.record("replacement-target-freeze", fleet.remote(node, "process-signal", signal="SIGSTOP", expected=identity))
+ frozen = fleet.snapshot("replacement-target-frozen-physical")
+ progress = target_progress(frozen, proofs)
+ require(progress["partial"], "NOT_OBSERVED: replacement was not physically partial at its frozen interruption boundary")
+ require(all(frozen[peer]["formats"] == original_format[peer]["formats"] for peer in fleet.nodes),
+ "partial replacement changed its new target UUID or another disk identity")
+ exit_proof = fleet.record("replacement-target-actual-exit", fleet.remote(node,
+ "process-graceful" if mode == "graceful" else "process-crash", expected=identity))
+ if mode == "graceful":
+ require(exit_proof.get("actualExitCode") == 0 and exit_proof.get("signal") == "SIGTERM", "target has no real graceful exit")
+ else:
+ require(exit_proof.get("actualSignal") == 9 and exit_proof.get("graceful") is False
+ and exit_proof.get("platformEventIssued") is False, "target crash lacks an exact SIGKILL witness")
+ del fleet.frozen_processes[node]
+ fleet.record("replacement-target-release-unit", fleet.remote(node, "stop"))
+ stopped = fleet.record("replacement-target-after-exit-physical", {peer: fleet.remote(peer, "snapshot", quiescent=peer == node) for peer in fleet.nodes})
+ after_progress = target_progress(stopped, proofs)
+ require(after_progress["partial"] and set(progress["completeDataVersions"]) <= set(after_progress["completeDataVersions"]),
+ "target exit lost a committed shard or missed the partial interruption window")
+ require(all(stopped[peer]["formats"] == original_format[peer]["formats"] for peer in fleet.nodes),
+ "target exit changed the original replacement generation's physical identity")
+ fleet.record("replacement-original-target-start", fleet.remote(node, "start"))
+ fleet.wait_ready()
+ after = fleet.record("replacement-processes-after-interruption", {peer: fleet.remote(peer, "process") for peer in fleet.nodes})
+ continuity = target_restart_identity(before, after, node)
+ archive = fleet.record("replacement-backup-refusal-after-interruption", fleet.remote(node, "replacement-probe"))
+ require(archive.get("serviceReadDenied") is True and archive.get("sameOriginalServiceProcess") is True,
+ "restarted target can access its original backup")
+ return {"interruption": mode, "before": progress, "afterActualExit": after_progress,
+ "actualExit": exit_proof, "processContinuity": continuity, "sameNewDiskUUIDAndSetSlot": True}
+
+
+def coordinator(events, token, target):
+ owners = []
+ for node, rows in events.items():
+ admitted = [event for event in rows if event.get("target") == "rustfs::heal::manager"
+ and event.get("request_id") == token and event.get("source") == "auto_heal"
+ and event.get("result") == "accepted" and event.get("message") == "Heal queue request accepted"
+ and event_time(event) is not None]
+ started = [event for event in rows if event.get("target") == "rustfs::heal::task"
+ and event.get("task_id") == token and event.get("set_disk_id") == "pool_0_set_0"
+ and event.get("stage") == "start" and event.get("message") == "Heal erasure set started"
+ and event_time(event) is not None]
+ if any(event_time(start) >= event_time(admission) for start in started for admission in admitted):
+ owners.append(node)
+ require(len(owners) == 1 and owners[0] != target,
+ "NOT_OBSERVED: replacement has no uniquely witnessed coordinator distinct from the interrupted target")
+ return owners[0]
+
+
+def automatic_completion(probe, fleet, endpoint, earliest, *, interruption=None, proofs=(), original_format=None,
+ journal=None, coordinator_interruptor=None):
+ deadline, original, timeline = time.monotonic() + REBUILD_SECONDS, None, []
+ interrupted = None
+ while True:
+ observed = replacement_records(probe, fleet, "automatic-replacement-generation-window")
+ ready = True
+ for peer, records in observed.items():
+ if not records:
+ ready = False
+ continue
+ bound = generation(records, endpoint, earliest, original=original)
+ original = original or bound
+ ready = ready and records[0]["state"] == "completed"
+ timeline.append(observed)
+ if interruption is not None and interrupted is None:
+ require(not ready, "NOT_OBSERVED: replacement completed before its required target interruption")
+ if any(records and records[0]["state"] == "running" for records in observed.values()):
+ progress = target_progress(fleet.snapshot("replacement-running-physical-window"), proofs)
+ if progress["partial"]:
+ owner = coordinator(journal.capture(), original["taskId"], proofs[0]["node"])
+ fleet.record("replacement-distinct-coordinator-proof", {"node": owner,
+ "target": proofs[0]["node"], "originalGeneration": original, "source": "fresh accepted auto_heal and exact erasure task start"})
+ if interruption == "coordinator-sigkill":
+ require(coordinator_interruptor is not None, "replacement coordinator observer missing")
+ interrupted = coordinator_interruptor(fleet, owner, proofs, original_format, original)
+ else:
+ interrupted = interrupt_target(fleet, proofs[0]["node"], proofs, interruption, original_format)
+ interrupted["coordinatorKeptRunning"] = owner
+ continue
+ if ready and original is not None:
+ require(len({canonical(records[0]) for records in observed.values()}) == 1,
+ "completed replacement responsibility differs across peers")
+ break
+ require(time.monotonic() < deadline, "ATTRIBUTION_NOT_OBSERVED: original automatic replacement generation did not complete on every peer")
+ time.sleep(2)
+ for index in range(2):
+ repeated = replacement_records(probe, fleet, "automatic-replacement-completion-repeat")
+ for records in repeated.values(): generation(records, endpoint, earliest, original=original, completed=True)
+ require(canonical(repeated) == canonical(observed), "completed replacement generation changed after its terminal observation")
+ require(interruption is None or interrupted is not None, "NOT_OBSERVED: no actual partial target interruption occurred")
+ return {"source": "automatic-replacement", "generation": original, "observations": len(timeline),
+ "completionObservedOnEveryPeer": True, "budgetSeconds": REBUILD_SECONDS,
+ "adminHealMutations": 0, "targetS3ReadsBeforePhysicalProof": 0,
+ "coordinatorInterruption" if interruption == "coordinator-sigkill" else "targetInterruption": interrupted}
+
+
+def wait_format(fleet, node, disk):
+ deadline = time.monotonic() + FORMAT_SECONDS
+ while True:
+ observed = fleet.record("replacement-format-initialization-window", fleet.remote(node, "replacement-state"))
+ require(observed.get("readOnly") is True and observed.get("originalArchiveUnchanged") is True,
+ "replacement initialization lost its read-only original-archive witness")
+ formats = observed.get("formats")
+ require(isinstance(formats, list) and len(formats) == 4
+ and {item.get("disk") for item in formats} == set(range(4)), "replacement format probe omitted a member")
+ target = next(item for item in formats if item["disk"] == disk)
+ require(type(target.get("present")) is bool, "replacement format presence is unknown")
+ if target["present"]: return observed
+ require(time.monotonic() < deadline,
+ "candidate did not initialize the replacement format within its registered 90-second budget")
+ time.sleep(2)
+
+
+def format_identity(before, after, member):
+ topology = "H1" if len(before) == 1 else "H2"
+ validate_topology(before, topology); validate_topology(after, topology)
+ old = next(entry for entry in before[member[0]]["formats"] if entry["disk"] == member[1])
+ new = next(entry for entry in after[member[0]]["formats"] if entry["disk"] == member[1])
+ old_uuid, new_uuid = old["value"]["xl"]["this"], new["value"]["xl"]["this"]
+ sets = old["value"]["xl"]["sets"]
+ slot = sets[0].index(old_uuid)
+ require(new_uuid != old_uuid and new_uuid not in sets[0], "replacement did not expose a distinct new disk UUID")
+ require(set(before) == set(after), "replacement changed the original node fleet")
+ for node, observation in before.items():
+ current = {entry["disk"]: entry for entry in after[node]["formats"]}
+ require(set(current) == {entry["disk"] for entry in observation["formats"]}, "replacement omitted a physical member")
+ for entry in observation["formats"]:
+ expected = copy.deepcopy(entry["value"])
+ expected["xl"]["sets"][0][slot] = new_uuid
+ if (node, entry["disk"]) == member: expected["xl"]["this"] = new_uuid
+ require(current[entry["disk"]]["value"] == expected,
+ "replacement changed pool/set geometry, another disk identity or an unrelated format field")
+ return {"oldDiskUUID": old_uuid, "newDiskUUID": new_uuid, "sameOriginalSetSlot": slot,
+ "originalPoolAndAllOtherMemberIdentitiesPreserved": True}
+
+
+def metadata_semantics(metadata):
+ value = copy.deepcopy(metadata)
+ for version in value.get("versions", []):
+ inline = version.get("inline")
+ if inline: version["inline"] = {name: inline.get(name) for name in ("length", "sha256")}
+ version.get("erasure", {}).pop("checksums", None)
+ return value
+
+
+def physical_rebuild(before, after, member):
+ identity = format_identity(before, after, member)
+ left, right = files(before), files(after)
+ require(left and set(left) <= set(right), "replacement lost an original object/version/part file")
+ original_target = {key: entry for key, entry in left.items() if key[:2] == member}
+ require(original_target, "replacement has no original target file oracle")
+ for key, entry in left.items():
+ actual = right[key]
+ if key[:2] != member:
+ require(actual == entry, "replacement changed a surviving member's original shard/metadata")
+ elif entry.get("metadata"):
+ require(actual.get("metadata") and canonical(metadata_semantics(actual["metadata"])) ==
+ canonical(metadata_semantics(entry["metadata"])), "new disk changed an original version/marker/inline/role authority")
+ else:
+ require(actual == entry, "replacement did not reconstruct exact original same-role part bytes")
+ for key in set(right) - set(left):
+ # A target-side rollback backup is bounded to the exact original xl.meta
+ # bytes for that same member/key. No orphan or system record is allowed.
+ original = left.get((key[0], key[1], key[2].removesuffix(".bkp"))) if key[2].endswith("/xl.meta.bkp") else None
+ require(key[:2] == member and original and original.get("metadata")
+ and all(right[key].get(name) == original.get(name) for name in ("sha256", "size")),
+ "replacement created an unexpected file or foreign rollback backup")
+ return {**identity, "allOriginalTargetFilesRebuilt": len(original_target),
+ "originalSameRolePartAndInlineDigestsRestored": True, "survivingFilesUnchanged": True,
+ "backupRestorationUsed": False}
+
+
+def run_branch(recorder, fleet, bucket, branch, expected, baseline, active, admissions, *, coordinator_interruptor=None):
+ try:
+ from .versions import map_inline_target, validate_version_receipts, verify_version_s3
+ except ImportError:
+ from versions import map_inline_target, validate_version_receipts, verify_version_s3
+ automatic = branch in (*AUTOMATIC_BRANCHES, *GRACEFUL_BRANCHES, *CRASH_BRANCHES, *COORDINATOR_BRANCHES)
+ require(branch in (*BRANCHES, *AUTOMATIC_BRANCHES, *GRACEFUL_BRANCHES, *CRASH_BRANCHES, *COORDINATOR_BRANCHES), "unknown replacement branch")
+ require(branch not in COORDINATOR_BRANCHES or len(fleet.nodes) == 4 and coordinator_interruptor is not None,
+ "replacement coordinator crash requires actual H2 and its independent observer")
+ require(not automatic or fleet.runtime_profile == "replacement", "automatic replacement lacks its isolated source configuration")
+ before = fleet.snapshot("replacement-original-" + branch, quiescent=True)
+ if automatic:
+ fleet.start()
+ healthy = replacement_records(recorder.probe, fleet, "automatic-replacement-empty-baseline")
+ require(all(not records for records in healthy.values()), "replacement baseline retained an older generation")
+ fleet.stop()
+ require(files(before) == files(fleet.snapshot("replacement-original-after-source-preflight", quiescent=True)),
+ "replacement source preflight changed an acknowledged original file")
+ node, disk = fleet.nodes[-1], 3
+ member = node, disk
+ physical_format = next(entry for entry in before[node]["formats"] if entry["disk"] == disk)
+ proofs = []
+ indexed = files(before)
+ for entry in expected:
+ if entry["deleteMarker"]: continue
+ metadata = indexed[(node, disk, f"{bucket}/{entry['key']}/xl.meta")]["metadata"]
+ version = next(version for version in metadata["versions"] if (version.get("versionId") or "null") == entry["versionId"])
+ target = map_inline_target(before, bucket, entry, allow_empty=True) if version.get("inline") or entry["payload"]["size"] == 0 \
+ else map_target(before, bucket, entry["key"], "data", 1, entry["versionId"], latest_required=False)
+ shard = next(value for value in target["cohort"] if (value["node"], value["disk"]) == member)
+ proofs.append({**target, **shard})
+ earliest = int(time.time())
+ journal = JournalWindow(fleet, "replacement-interruption-source", event_targets=SOURCE_TARGETS) \
+ if branch in (*GRACEFUL_BRANCHES, *CRASH_BRANCHES, *COORDINATOR_BRANCHES) else None
+ replaced = fleet.record("replacement-original-root-isolated", fleet.remote(node, "replace-drive", disk=disk,
+ expectedFormatSha256=physical_format["sha256"]))
+ require(replaced.get("oldRootMovedIntact") is True and replaced.get("newDataRootEmpty") is True
+ and replaced.get("sharedMountDetached") is False, "replacement lacks exact original and empty-root evidence")
+ fleet.start()
+ denied = fleet.record("replacement-backup-service-refusal", fleet.remote(node, "replacement-probe"))
+ require(denied.get("serviceReadDenied") is True and denied.get("sameOriginalServiceProcess") is True,
+ "replacement candidate can access its old physical backup")
+ wait_format(fleet, node, disk)
+ initialized = fleet.snapshot("replacement-candidate-initialization")
+ fleet.record("replacement-new-format-proof", format_identity(before, initialized, member))
+ if not automatic:
+ require(not any(item["disk"] == disk for item in initialized[node]["files"]),
+ "ATTRIBUTION_NOT_OBSERVED: new drive received object files before its manual repair token")
+ require({key: value for key, value in files(initialized).items() if key[:2] != member} ==
+ {key: value for key, value in indexed.items() if key[:2] != member}, "replacement initialization changed surviving objects")
+ if automatic:
+ endpoint = replaced["dataPath"] if len(fleet.nodes) == 1 else f"http://rustfs-node{fleet.nodes.index(node) + 1}:9000{replaced['dataPath']}"
+ receipts = automatic_completion(recorder.probe, fleet, endpoint, earliest,
+ interruption="graceful" if branch in GRACEFUL_BRANCHES else "sigkill" if branch in CRASH_BRANCHES else
+ "coordinator-sigkill" if branch in COORDINATOR_BRANCHES else None,
+ proofs=proofs, original_format=initialized, journal=journal, coordinator_interruptor=coordinator_interruptor)
+ else:
+ status = run_task(recorder.probe, fleet, {"bucket": bucket, "key": ""}, 2, active,
+ option_overrides={"recursive": True}, admissions=admissions)
+ receipts = validate_version_receipts(status, bucket, expected, 2,
+ required_repairs=[(entry["key"], entry["versionId"]) for entry in expected])
+ fleet.stop()
+ after = fleet.snapshot("replacement-after-manual", quiescent=True)
+ physical = fleet.record("replacement-full-physical-proof", physical_rebuild(before, after, member))
+ fleet.record("replacement-original-archive-after-rebuild", fleet.remote(node, "replacement-state"))
+ streaming, external = [], []
+ for target in proofs:
+ if target.get("zeroProof"):
+ streaming.append({"key": target["key"], "zeroLengthMetadataVerified": True, "noPartInvented": True})
+ continue
+ operation = "verify-inline" if target.get("inlineProof") else "verify-part"
+ if operation == "verify-part":
+ external.append(target)
+ continue
+ params = {name: target[name] for name in ("disk", "relativePath", "versionId")}
+ if operation == "verify-part": params["partNumber"] = target["partNumber"]
+ proof = fleet.record("replacement-target-streaming-check", fleet.remote(node, operation, **params))
+ require(proof.get("physicalBitrotVerified") is True, "reconstructed target has no verified streaming checksum")
+ if operation == "verify-inline":
+ require(proof.get("inlineLength") == target["inlineValue"]["length"]
+ and proof.get("inlineSha256") == target["inlineValue"]["sha256"], "new target inline bytes differ from the original same-role oracle")
+ else:
+ require(proof.get("sha256") == target["sha256"] and proof.get("physicalSize") == target["size"],
+ "new target external shard differs from the original same-role oracle")
+ streaming.append({"key": target["key"], "versionId": target["versionId"], "proof": proof})
+ for offset in range(0, len(external), 128):
+ batch = external[offset:offset + 128]
+ observed = fleet.record("replacement-target-streaming-batch", fleet.remote(node, "verify-parts", parts=[
+ {name: target[name] for name in ("disk", "relativePath", "versionId", "partNumber")} for target in batch]))
+ require(observed.get("complete") is True and isinstance(observed.get("parts"), list)
+ and len(observed["parts"]) == len(batch), "replacement streaming batch omitted an exact target part")
+ for target, proof in zip(batch, observed["parts"], strict=True):
+ require(proof.get("physicalBitrotVerified") is True
+ and all(proof.get(name) == target[name] for name in ("disk", "relativePath", "versionId", "partNumber", "sha256"))
+ and proof.get("physicalSize") == target["size"], "replacement target streaming batch changed an acknowledged same-role part")
+ streaming.append({"key": target["key"], "versionId": target["versionId"], "proof": proof})
+ fleet.start()
+ fleet.record("replacement-backup-service-refusal-after", fleet.remote(node, "replacement-probe"))
+ fixture = "DB" if branch in {"new-disk-data", "replacement-unversioned", "target-partial-graceful", "target-partial-sigkill", *COORDINATOR_BRANCHES} else "DV"
+ s3 = verify_version_s3(recorder.probe, fleet, bucket, expected, baseline, "replacement-s3-after", fixture=fixture)
+ peers = []
+ for endpoint in recorder.probe.binding["endpoints"]:
+ inventory = fleet.record("replacement-all-peer-s3", {"endpoint": endpoint,
+ **Probe(recorder.probe.binding, endpoint).inventory(bucket, unversioned_current_tags=fixture == "DB")})
+ peers.append({"endpoint": endpoint, **verify_seed(expected, inventory),
+ "manifest": compare_inventories(baseline, inventory)})
+ return {"verdict": "PASS", "responsibility": receipts, "physical": physical,
+ "allTargetVersionStreamingProofs": streaming, "s3": s3, "allPeerS3": peers}
diff --git a/scripts/validation_heal/replacement_journal.py b/scripts/validation_heal/replacement_journal.py
new file mode 100644
index 00000000..4e4611b1
--- /dev/null
+++ b/scripts/validation_heal/replacement_journal.py
@@ -0,0 +1,65 @@
+"""Read the exact candidate's run-owned replacement generation records."""
+
+import hashlib
+import json
+import os
+import stat
+import uuid
+
+
+REFERENCE = {"candidateCommit": "072b57b37559fbe626aeda8abf61617fdcbdd926", "sourceFiles": {
+ "crates/heal/src/heal/resume.rs": "ed55baa03fafa79bf662393eafcf53b4f7752ca8b2f77824cc6deff085a0cf1d",
+ "crates/heal/src/heal/resume/replacement.rs": "d44d4b688a12fd09c4c9c48067e0f3dd024daf429ad4d4eeb63d4783bb6a143a"},
+ "resumeSchema": 7, "commitSeal": "sealed", "role": "read-only generation witness; no replacement record synthesis"}
+FILES = {"intent": ("ahm_replacement_intent.json", 1024**2),
+ "seal": ("ahm_replacement_intent_seal", 32),
+ "proof": ("ahm_replacement_completion_proof.json", 1024**2)}
+
+
+def require(value, message):
+ if not value: raise ValueError(message)
+
+
+def read_records(roots, token, checked):
+ require(isinstance(token, str) and str(uuid.UUID(token)) == token and uuid.UUID(token).int != 0,
+ "replacement checkpoint requires a canonical nonzero generation")
+ require(len(roots) == 4, "replacement checkpoint omitted an original physical root")
+
+ def collect():
+ rows = []
+ for disk, root in enumerate(roots):
+ data = root / "data"
+ for kind, (suffix, limit) in FILES.items():
+ relative = f".rustfs.sys/buckets/ahm-replacement/{token}_{suffix}"
+ path = checked(data / relative, data)
+ row = {"disk": disk, "kind": kind, "relativePath": relative, "present": False}
+ try: fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW)
+ except FileNotFoundError:
+ rows.append(row)
+ continue
+ with os.fdopen(fd, "rb") as stream:
+ before = os.fstat(stream.fileno())
+ require(stat.S_ISREG(before.st_mode) and before.st_nlink == 1 and before.st_size <= limit,
+ "replacement record exceeds its exact regular-file bound")
+ raw = stream.read(limit + 1)
+ after = os.fstat(stream.fileno())
+ try: current = path.lstat()
+ except FileNotFoundError: return None
+ fields = ("st_dev", "st_ino", "st_size", "st_mtime_ns", "st_ctime_ns", "st_nlink")
+ if len(raw) != before.st_size or any(getattr(before, field) != getattr(other, field)
+ for other in (after, current) for field in fields): return None
+ row.update(present=True, size=len(raw), sha256=hashlib.sha256(raw).hexdigest())
+ if kind == "seal":
+ require(raw == b"sealed", "replacement generation has no reviewed commit seal")
+ row["sealed"] = True
+ else:
+ row["value"] = json.loads(raw)
+ require(isinstance(row["value"], dict), "replacement record is not a complete JSON object")
+ rows.append(row)
+ return rows
+
+ for _ in range(3):
+ first, second = collect(), collect()
+ if first is not None and first == second:
+ return {"taskId": token, "readOnly": True, "stable": True, "records": second}
+ raise ValueError("replacement checkpoint did not stabilize inside its three-read budget")
diff --git a/scripts/validation_heal/replacement_preflight.py b/scripts/validation_heal/replacement_preflight.py
new file mode 100644
index 00000000..d68722fe
--- /dev/null
+++ b/scripts/validation_heal/replacement_preflight.py
@@ -0,0 +1,142 @@
+#!/usr/bin/env python3
+"""Run the replacement cases' read-only endpoint admission check before changes."""
+
+from __future__ import annotations
+
+import argparse
+from concurrent.futures import ThreadPoolExecutor, as_completed
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import sys
+
+try:
+ from .healthy import Fleet
+ from .heal_probe import save
+except ImportError:
+ from healthy import Fleet
+ from heal_probe import save
+
+
+def checked_identity(value: object) -> bool:
+ return (isinstance(value, dict) and value.get("readOnly") is True
+ and value.get("mutated") is False and value.get("canonicalPassClaimed") is False
+ and isinstance(value.get("checks"), dict)
+ and set(value["checks"]) == {
+ "exactEndpointMounts", "mountIdsPresent", "filesystemIdentitiesDistinct",
+ "endpointPhysicalDevicesDistinct", "endpointDevicesSeparateFromRoot",
+ "mountSwitchAdapterReviewed",
+ }
+ and type(value.get("admitted")) is bool)
+
+
+REPLACEMENT_CASES = {
+ "heal.disk-replacement-formatted",
+ "heal.disk-replacement-unformatted",
+}
+
+
+def run_preflight(nodes: list[str], evidence: Path, workflow_run_id: str, attempt: str,
+ testing_commit: str, candidate: dict,
+ case_id: str = "heal.disk-replacement-formatted") -> dict:
+ if case_id not in REPLACEMENT_CASES:
+ raise ValueError(f"unsupported replacement admission case: {case_id}")
+ if not re.fullmatch(r"[1-9][0-9]*", workflow_run_id) or not re.fullmatch(r"[1-9][0-9]*", attempt):
+ raise ValueError("replacement preflight requires the immutable workflow run and attempt")
+ if not re.fullmatch(r"[0-9a-f]{40}", testing_commit):
+ raise ValueError("replacement preflight requires the exact auto-testing commit")
+ if (not re.fullmatch(r"[0-9a-f]{40}", str(candidate.get("source_sha", "")))
+ or not re.fullmatch(r"[0-9a-f]{64}", str(candidate.get("package_sha256", "")))
+ or not isinstance(candidate.get("package_url"), str) or not candidate["package_url"]):
+ raise ValueError("replacement preflight candidate package provenance is incomplete")
+ if len(nodes) != 4 or len(set(nodes)) != 4 or any(
+ not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", node) for node in nodes):
+ raise ValueError("replacement preflight requires the four current auto-testing Heal nodes")
+
+ results = []
+ infrastructure_errors = []
+ blocked = False
+ for topology, selected in (("H1", nodes[:1]), ("H2", nodes)):
+ run_id = f"gh-{workflow_run_id}-{attempt}-{topology.lower()}"
+ directory = evidence / "replacement-admission" / topology
+ directory.mkdir(parents=True, exist_ok=True)
+ fleet = Fleet(selected, directory, run_id)
+ observed = {}
+ with ThreadPoolExecutor(max_workers=len(selected)) as executor:
+ pending = {executor.submit(fleet.remote, node, "replacement-admission-preflight"): node
+ for node in selected}
+ for future in as_completed(pending):
+ node = pending[future]
+ try:
+ value = future.result()
+ if not checked_identity(value):
+ infrastructure_errors.append({"topology": topology, "node": node,
+ "reason": "remote preflight response is incomplete or malformed"})
+ observed[node] = {"invalidResponse": True}
+ else:
+ observed[node] = value
+ blocked = blocked or value["admitted"] is not True
+ except Exception as exc:
+ infrastructure_errors.append({"topology": topology, "node": node,
+ "reason": f"{type(exc).__name__}: {exc}"})
+ observed[node] = {"preflightError": f"{type(exc).__name__}: {exc}"}
+ ordered = {node: observed[node] for node in selected}
+ fleet.record("replacement-admission-preflight", {"topology": topology, "runId": run_id,
+ "readOnly": True, "mutated": False, "nodes": ordered})
+ results.append({"topology": topology, "runId": run_id, "nodes": ordered,
+ "deploymentSkipped": True, "fixtureSeedSkipped": True})
+
+ infrastructure_errors.sort(key=lambda item: (item["topology"], item["node"], item["reason"]))
+ status = "INFRA_ERROR" if infrastructure_errors else "BLOCKED" if blocked else "ADMITTED"
+ summary = {
+ "schemaVersion": 1,
+ "caseId": case_id,
+ "status": status,
+ "executionStage": "pre-deployment",
+ "workflowRunId": int(workflow_run_id),
+ "workflowRunAttempt": int(attempt),
+ "testingCommit": testing_commit,
+ "candidate": {"sourceSha": candidate["source_sha"], "packageUrl": candidate["package_url"],
+ "packageSha256": candidate["package_sha256"]},
+ "preflightHelperSha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
+ "fleetNodeSha256": hashlib.sha256(Path(__file__).with_name("fleet_node.py").read_bytes()).hexdigest(),
+ "mutationStarted": False,
+ "replacementAdmissionBlocked": status == "BLOCKED",
+ "deploymentSkipped": status != "ADMITTED",
+ "fixtureSeedSkipped": status != "ADMITTED",
+ "canonicalPassClaimed": False,
+ "cleanup": "PASS",
+ "cleanupDisposition": "NOT_REQUIRED_NO_MUTATION",
+ "results": results,
+ "infrastructureErrors": infrastructure_errors,
+ }
+ save(evidence / "healthy-summary.json", summary)
+ return summary
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--case-id", choices=sorted(REPLACEMENT_CASES),
+ default="heal.disk-replacement-formatted")
+ parser.add_argument("--candidate", type=Path, required=True)
+ parser.add_argument("--testing-commit", required=True)
+ parser.add_argument("--evidence", type=Path, required=True)
+ args = parser.parse_args()
+ try:
+ nodes = os.environ.get("RUSTFS_NODES", "vm000 vm001 vm002 vm003").split()
+ candidate = json.loads(args.candidate.read_text(encoding="utf-8"))
+ summary = run_preflight(nodes, args.evidence, os.environ.get("GITHUB_RUN_ID", ""),
+ os.environ.get("GITHUB_RUN_ATTEMPT", "1"), args.testing_commit, candidate,
+ args.case_id)
+ print(json.dumps({"status": summary["status"], "mutationStarted": False,
+ "replacementAdmissionBlocked": summary["replacementAdmissionBlocked"]}, sort_keys=True))
+ return 0
+ except (OSError, ValueError, KeyError, json.JSONDecodeError) as exc:
+ print(f"replacement pre-deployment preflight failed: {exc}", file=sys.stderr)
+ return 1
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/scripts/validation_heal/retained_scene.py b/scripts/validation_heal/retained_scene.py
new file mode 100644
index 00000000..d034054c
--- /dev/null
+++ b/scripts/validation_heal/retained_scene.py
@@ -0,0 +1,319 @@
+"""Retain incomplete stopped fixtures by same-filesystem, non-overwriting rename.
+
+Only owner/binary and scoped fixture/MRF/multipart bytes are read. Other entries, including
+credentials, are retained by inode and metadata without reading their contents.
+"""
+
+import ctypes
+import hashlib
+import json
+import os
+import re
+from pathlib import Path
+import stat
+import uuid
+
+
+def require(value, message):
+ if not value:
+ raise ValueError(message)
+
+
+def encoded(value):
+ return json.dumps(value, sort_keys=True, separators=(",", ":")).encode()
+
+
+def sha(value):
+ return hashlib.sha256(encoded(value)).hexdigest()
+
+
+def stable(info):
+ return (info.st_mode, info.st_uid, info.st_gid, info.st_dev, info.st_ino, info.st_nlink,
+ info.st_size, info.st_mtime_ns, info.st_ctime_ns)
+
+
+def private_json(path, *, root_owned=True):
+ fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
+ try:
+ before = os.fstat(fd)
+ require(stat.S_ISREG(before.st_mode) and before.st_nlink == 1 and before.st_size <= 16384
+ and not before.st_mode & 0o022 and (not root_owned or before.st_uid == 0),
+ "retained scene marker has unsafe ownership, type or size")
+ raw = os.read(fd, 16385)
+ after = os.fstat(fd)
+ require(len(raw) == before.st_size and stable(before) == stable(after), "retained scene marker changed during audit")
+ value = json.loads(raw)
+ require(isinstance(value, dict), "retained scene marker is not an object")
+ return value
+ finally:
+ os.close(fd)
+
+
+def destinations(state, roots, *, per_run_parent=False):
+ if per_run_parent:
+ return state.parent / ("retained-" + state.name) / state.name, tuple(
+ root.parent.parent / ("heal-validation-retained-" + root.name) / root.name for root in roots)
+ return state.parent / "retained" / state.name, tuple(
+ root.parent.parent / "heal-validation-retained" / root.name for root in roots)
+
+
+def sync_directory(path):
+ fd = os.open(path, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
+ try:
+ os.fsync(fd)
+ finally:
+ os.close(fd)
+
+
+def rename_new(source, target):
+ # Linux RENAME_NOREPLACE prevents replacing even an empty competing directory.
+ rename = ctypes.CDLL(None, use_errno=True).renameat2
+ rename.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_uint]
+ rename.restype = ctypes.c_int
+ result = rename(-100, os.fsencode(source), -100, os.fsencode(target), 1)
+ require(result == 0, "retained scene rename refused (errno=" + str(ctypes.get_errno()) + ")")
+ sync_directory(source.parent)
+ if source.parent != target.parent:
+ sync_directory(target.parent)
+
+
+def parent_ready(parent, boundary, node, *, create=False):
+ node["checked"](parent, boundary)
+ if create and not parent.exists():
+ parent.mkdir(mode=0o700)
+ sync_directory(parent.parent)
+ if parent.exists():
+ info = parent.lstat()
+ require(stat.S_ISDIR(info.st_mode) and info.st_uid == 0 and stat.S_IMODE(info.st_mode) == 0o700
+ and info.st_dev == boundary.stat().st_dev, "retained archive parent is not private on the original filesystem")
+
+
+def census(folder, owner, *, state, node, mpu_keys=()):
+ """Hash the complete inode inventory; read only explicitly scoped content."""
+ rows, content_count, mpu_bytes_hashed = [], 0, 0
+ boundary_device = folder.lstat().st_dev
+ network = owner.get("runtimeProfile") == "lock-quorum"
+ fixture_buckets = {owner["bucket"]}
+ if network:
+ fixture_buckets.add(owner["bucket"] + "-db")
+ multipart_keys = {hashlib.sha256(f"{owner['bucket']}/dl/{endpoint}/{name}.bin".encode()).hexdigest()
+ for endpoint in ("node1", "node3", "node4")
+ for name in ("abort", "complete", "create", "upload")} if network else set()
+ if mpu_keys and owner.get('runtimeProfile') in {'manual', 'frozen-race-legacy'}:
+ node['mpu_scene_backend'].validate_scope(owner, mpu_keys)
+ multipart_keys = {hashlib.sha256(f"{owner['bucket']}/{key}".encode()).hexdigest() for key in mpu_keys}
+ def visit(path):
+ nonlocal content_count, mpu_bytes_hashed
+ node["checked"](path, folder)
+ info = path.lstat()
+ require(info.st_dev == boundary_device and (stat.S_ISDIR(info.st_mode)
+ or stat.S_ISREG(info.st_mode) and info.st_nlink == 1),
+ "retained scene contains a mount, link or special file")
+ relative = path.relative_to(folder).as_posix()
+ require(len(path.relative_to(folder).parts) <= 64, "retained scene tree is too deep")
+ # Renaming the top directory changes only its ctime. Descendant ctime
+ # remains part of the witness, including for unread credential files.
+ row = [hashlib.sha256(relative.encode()).hexdigest(), info.st_mode, info.st_uid, info.st_gid,
+ info.st_dev, info.st_ino, info.st_size, info.st_mtime_ns,
+ info.st_ctime_ns if path != folder else None]
+ require(len(rows) < 30000, "retained scene inventory exceeds its explicit bound")
+ rows.append(row)
+ if stat.S_ISDIR(info.st_mode):
+ for child in sorted(path.iterdir()):
+ visit(child)
+ after = path.lstat()
+ require(stable(info) == stable(after), "retained scene directory changed during audit")
+ else:
+ parts = Path(relative).parts
+ fixture = not state and len(parts) > 2 and parts[0] == "data" and parts[1] in fixture_buckets
+ network_mpu = (not state and (network or mpu_keys) and len(parts) > 4
+ and parts[:3] == ("data", ".rustfs.sys", "multipart") and parts[3] in multipart_keys)
+ network_backup = (not state and network and len(parts) == 2 and parts[0] == "quarantine"
+ and re.fullmatch(r"I03-missing-target\.(original|intent\.json|applied\.json)", parts[1]))
+ mrf = not state and relative in {
+ "data/.rustfs.sys/.heal-mrf-snapshot.0.bin", "data/.rustfs.sys/.heal-mrf-snapshot.1.bin",
+ "data/.rustfs.sys/.heal-mrf-manifest.json"}
+ backup = (not state and owner.get("runtimeProfile") == "root-restart"
+ and len(parts) == 2 and parts[0] == "quarantine"
+ and re.fullmatch(r"F07-shard-[0-3]\.(original|intent\.json|applied\.json)", parts[1]))
+ opaque_backup = (not state and owner.get("runtimeProfile") == "outcome-legacy"
+ and len(parts) == 2 and parts[0] == "quarantine"
+ and re.fullmatch(r"J02-[0-9a-f]{32}\.(original|intent\.json|applied\.json|failed-[0-9a-f]{64})", parts[1]))
+ race_backup = (not state and owner.get("runtimeProfile") in {"manual", "frozen-race-legacy"}
+ and len(parts) == 2 and parts[0] == "quarantine"
+ and re.fullmatch((r"G0[45]" if owner.get("runtimeProfile") == "frozen-race-legacy" else r"(?:G0[12456]|A09)")
+ + r"-[a-z0-9-]{1,48}\.(original|intent\.json|applied\.json)", parts[1]))
+ if network_mpu and not network:
+ require(info.st_size <= 64 * 1024**2
+ and mpu_bytes_hashed + info.st_size <= node['mpu_scene_backend'].MAX_BYTES,
+ 'G05 retained multipart content exceeds its byte budget')
+ mpu_bytes_hashed += info.st_size
+ if (state and relative == "rustfs") or fixture or mrf or backup or opaque_backup or network_mpu or network_backup or race_backup:
+ fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
+ try:
+ require(stable(os.fstat(fd)) == stable(info) and info.st_size <= 2 * 1024**3,
+ "retained content changed or exceeds its bound")
+ digest = hashlib.sha256()
+ while chunk := os.read(fd, 1024**2):
+ digest.update(chunk)
+ require(stable(os.fstat(fd)) == stable(info), "retained content changed while hashing")
+ row.append(digest.hexdigest())
+ content_count += 1
+ finally:
+ os.close(fd)
+ require(stable(path.lstat()) == stable(info), "retained scene file changed during audit")
+ visit(folder)
+ return {"treeSha256": sha(rows), "entries": len(rows), "contentFilesHashed": content_count,
+ "device": boundary_device, "inode": folder.lstat().st_ino}
+
+
+def mounted_paths():
+ values = []
+ for line in Path("/proc/self/mountinfo").read_text().splitlines():
+ fields = line.split()
+ require(len(fields) >= 6, "retained scene mount inventory is malformed")
+ value = re.sub(r"\\([0-7]{3})", lambda match: chr(int(match.group(1), 8)), fields[4])
+ values.append(Path(value))
+ return values
+
+
+def inspect(request, node):
+ state, roots = node["paths"](request["runId"])
+ expected = request["expectedOwner"]
+ per_run = expected.get("runtimeProfile") in {"outcome-legacy", "lock-quorum", "manual", "frozen-race-legacy", "lock-timeout", "lock-successor"}
+ retained_state, retained_roots = destinations(state, roots, per_run_parent=per_run)
+ if per_run:
+ old_state, old_roots = destinations(state, roots)
+ require(not any(node["checked"](path, state.parent if index == 4 else node["MOUNTS"][index]).exists()
+ for index, path in enumerate((*old_roots, old_state))),
+ "shared archive already exists; refusing another retention layout")
+ require(isinstance(expected, dict) and expected.get("runId") == request["runId"]
+ and expected.get("unit") == node["unit"](request["runId"])
+ and expected.get("hostname") == node["socket"].gethostname()
+ and expected.get("runtimeProfile") in {"mrf-crash", "root-restart", "outcome-grace", "outcome-legacy", "lock-quorum", "manual", "frozen-race-legacy", "lock-timeout", "lock-successor"}, "retained scene archived owner differs")
+ current = node["mounts"]()
+ require(len(current) == len(expected.get("mounts", [])) == 4
+ and all(all(actual[key] == old[key] for key in ("target", "source", "fstype", "uuid", "maj:min"))
+ for actual, old in zip(current, expected["mounts"], strict=True)),
+ "retained scene original mount identities changed")
+ node["stopped"](expected)
+ for parent, boundary in [(retained_state.parent, state.parent),
+ *((target.parent, mount) for target, mount in zip(retained_roots, node["MOUNTS"], strict=True))]:
+ parent_ready(parent, boundary, node)
+ locations, witnesses = [], []
+ mounted = mounted_paths()
+ for index, (source, target) in enumerate(zip((*roots, state), (*retained_roots, retained_state), strict=True)):
+ boundary = node["MOUNTS"][index] if index < 4 else state.parent
+ node["checked"](target, boundary)
+ require(not (source.exists() and target.exists()), "both active and retained scene exist; refusing ambiguity")
+ folder = source if source.exists() else target if target.exists() else None
+ locations.append("active" if folder == source else "retained" if folder == target else "absent")
+ if folder is None:
+ require(index < 4, "retained scene lost its original state and candidate")
+ witnesses.append(None)
+ continue
+ require(not any(mount == folder or mount.is_relative_to(folder) for mount in mounted),
+ "retained scene contains a mounted subtree")
+ info = folder.lstat()
+ require(stat.S_ISDIR(info.st_mode) and info.st_dev == boundary.stat().st_dev,
+ "retained scene directory is not on its original filesystem")
+ owner = private_json(node["checked"](folder / "owner.json", folder), root_owned=index == 4)
+ require(encoded(owner) == encoded(expected), "retained scene owner content differs from original receipt")
+ if index == 4:
+ require(info.st_uid == 0 and stat.S_IMODE(info.st_mode) == 0o700,
+ "retained scene state is not private")
+ require(node["digest"](node["checked"](folder / "rustfs", folder)) == expected["binarySha256"],
+ "retained scene candidate binary differs")
+ require(not any((folder / name).exists() for name in (
+ "drive-gate.json", "write-quorum-gate.json", "network-gate.json", "disk-replacement.json")),
+ "retained scene still has a gate or replacement intent")
+ witnesses.append(census(folder, expected, state=index == 4, node=node, mpu_keys=request.get('mpuKeys', ())))
+ baseline = {"runId": request["runId"], "ownerSha256": sha(expected), "roots": witnesses[:4], "state": witnesses[4]}
+ if per_run:
+ baseline["archiveLayout"] = "per-run-parent"
+ intent = retained_state.parent / (request["runId"] + ".intent.json")
+ node["checked"](intent, state.parent)
+ network = expected["runtimeProfile"] == "lock-quorum"
+ if expected["runtimeProfile"] in {"root-restart", "outcome-grace", "outcome-legacy", "lock-quorum", "manual", "frozen-race-legacy", "lock-timeout", "lock-successor"}:
+ original_sha = request.get("expectedOriginalSceneSha256", "")
+ require(re.fullmatch(r"[0-9a-f]{64}", original_sha) is not None,
+ "restored fixture requires its original physical scene digest")
+ baseline["originalSceneSha256"] = original_sha
+ multipart = network or expected['runtimeProfile'] in {'manual', 'frozen-race-legacy'} and 'mpuKeys' in request
+ if multipart:
+ node['mpu_scene_backend'].validate_scope(expected, request['mpuKeys'])
+ require(re.fullmatch(r"[0-9a-f]{64}", request.get("expectedMpuSceneSha256", "")) is not None,
+ "network retention requires its stopped original MPU scene digest")
+ baseline["originalMpuSceneSha256"] = request["expectedMpuSceneSha256"]
+ baseline["originalMpuKeysSha256"] = sha(request["mpuKeys"])
+ require("absent" not in locations, "restored fixture cannot omit an original root")
+ if not intent.exists():
+ require(all(location == "active" for location in locations), "unbound retained fixture")
+ scene = node["snapshot"]({"runId": request["runId"], "action": "snapshot", "quiescent": True,
+ **({"buckets": [expected["bucket"], expected["bucket"] + "-db"]} if network else {})})
+ require(scene.get("quiescent") is True and type(scene.get("pid")) is int and scene["pid"] == 0
+ and scene.get("binarySha256") == expected["binarySha256"]
+ and sha({key: scene[key] for key in ("files", "formats")}) == original_sha,
+ "restored fixture differs from the original business scene")
+ if multipart:
+ mpu = node["mpu_scene_backend"].run({"runId": request["runId"], "action": "mpu-scene",
+ "bucket": expected["bucket"], "keys": request["mpuKeys"],
+ "expectedBinarySha256": expected["binarySha256"]}, node)
+ require(mpu["sceneSha256"] == request["expectedMpuSceneSha256"],
+ "network retained multipart scene differs from the original stopped census")
+ if intent.exists():
+ require(encoded(private_json(intent)) == encoded(baseline), "retained scene drifted from its durable isolation intent")
+ else:
+ require("retained" not in locations, "retained directory lacks its durable isolation intent")
+ return baseline, locations, state, roots, retained_state, retained_roots, intent
+
+
+def run(request, node):
+ fields = {"runId", "action", "expectedOwner"}
+ if request.get("expectedOwner", {}).get("runtimeProfile") in {"root-restart", "outcome-grace", "outcome-legacy", "lock-quorum", "manual", "frozen-race-legacy", "lock-timeout", "lock-successor"}:
+ fields.add("expectedOriginalSceneSha256")
+ if request.get("expectedOwner", {}).get("runtimeProfile") == "lock-quorum" or (
+ request.get('expectedOwner', {}).get('runtimeProfile') in {'manual', 'frozen-race-legacy'}
+ and ('mpuKeys' in request or 'expectedMpuSceneSha256' in request)):
+ fields.update({"expectedMpuSceneSha256", "mpuKeys"})
+ if request["action"] == "retained-scene-isolate":
+ fields.add("expectedBaseline")
+ require(set(request) == fields,
+ "retained scene accepts only its archived owner and frozen inventory")
+ require(request["action"] in {"retained-scene-audit", "retained-scene-isolate"}, "invalid retained scene action")
+ baseline, locations, state, roots, retained_state, retained_roots, intent = inspect(request, node)
+ mutated = False
+ if request["action"] == "retained-scene-isolate":
+ require(encoded(request["expectedBaseline"]) == encoded(baseline), "retained scene changed after fleet preflight")
+ for parent, boundary in [(retained_state.parent, state.parent),
+ *((target.parent, mount) for target, mount in zip(retained_roots, node["MOUNTS"], strict=True))]:
+ parent_ready(parent, boundary, node, create=True)
+ if not intent.exists():
+ temporary = intent.with_name(intent.name + "." + uuid.uuid4().hex + ".new")
+ fd = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
+ with os.fdopen(fd, "wb") as stream:
+ stream.write(encoded(baseline)); stream.flush(); os.fsync(stream.fileno())
+ rename_new(temporary, intent)
+ for index, (source, target) in enumerate(zip((*roots, state), (*retained_roots, retained_state), strict=True)):
+ if locations[index] != "active":
+ continue
+ fresh = inspect(request, node)
+ require(encoded(fresh[0]) == encoded(baseline), "retained scene changed immediately before rename")
+ rename_new(source, target)
+ mutated = True
+ final = inspect(request, node)
+ require(encoded(final[0]) == encoded(baseline) and "active" not in final[1],
+ "retained scene post-rename inventory differs")
+ locations = final[1]
+ result = {"baseline": baseline, "locations": locations, "originalEvidenceComplete": False,
+ "originalSceneRestored": False, "canonicalPassClaimed": False, "backupRestorationUsed": False,
+ "credentialContentsRead": False, "inspectionOnly": request["action"] == "retained-scene-audit",
+ "mutated": mutated, "activeNamespaceIsolated": "active" not in locations}
+ if request["expectedOwner"].get("runtimeProfile") in {"outcome-legacy", "lock-quorum", "manual", "frozen-race-legacy", "lock-timeout", "lock-successor"}:
+ result["privateArchiveParents"] = []
+ for target in (*retained_roots, retained_state):
+ info = target.parent.lstat() if target.parent.exists() else None
+ result["privateArchiveParents"].append({"exists": False} if info is None else {
+ "exists": True, "uid": info.st_uid, "gid": info.st_gid, "mode": stat.S_IMODE(info.st_mode),
+ "device": info.st_dev, "inode": info.st_ino})
+ return result
diff --git a/scripts/validation_heal/root_restart.py b/scripts/validation_heal/root_restart.py
new file mode 100644
index 00000000..6a8c7049
--- /dev/null
+++ b/scripts/validation_heal/root_restart.py
@@ -0,0 +1,293 @@
+#!/usr/bin/env python3
+"""F01: original root responsibility, owner and timeout across graceful restart."""
+
+from __future__ import annotations
+
+import argparse
+import copy
+import json
+import sys
+import time
+import uuid
+
+try:
+ from .heal_probe import Probe, canonical, require
+ from .healthy import BASE_OPTIONS, physical_content
+ from .cursor import OBJECTS, TASK_SECONDS, complete_outcome, wanted_pairs, run_topology as dx_topology, main as dx_main
+ from .shards import files, validate_repaired_physical
+ from .stop import status, wait_partial, idle_fleet, validate_unknown_token
+ from .cancel_restart import REFERENCE as JOURNAL_REFERENCE
+ from .fleet_node import PROCESS_FIELDS
+ from .versions import public_version_id
+except ImportError:
+ from heal_probe import Probe, canonical, require
+ from healthy import BASE_OPTIONS, physical_content
+ from cursor import OBJECTS, TASK_SECONDS, complete_outcome, wanted_pairs, run_topology as dx_topology, main as dx_main
+ from shards import files, validate_repaired_physical
+ from stop import status, wait_partial, idle_fleet, validate_unknown_token
+ from cancel_restart import REFERENCE as JOURNAL_REFERENCE
+ from fleet_node import PROCESS_FIELDS
+ from versions import public_version_id
+
+
+CASE_ID = "heal.root-graceful-coordinator-restart"
+BRANCHES = ("root-partial-graceful", "same-record-disk-resume")
+PATH = "/rustfs/admin/v3/heal/"
+TIMEOUT_SECONDS = 300
+OPTIONS = {**BASE_OPTIONS, "recursive": True, "pool": None, "set": None}
+REFERENCE = {**JOURNAL_REFERENCE, "intentSchema": 3, "rootHealType": {"type": "cluster"},
+ "taskTimeoutSeconds": TIMEOUT_SECONDS, "recovery": "original admission replay; no cursor-resume or failover claim",
+ "processBoundary": "same unit/env/volumes/candidate and boot; coordinator only; pidfd SIGTERM, actual normal exit zero"}
+
+
+def records(fleet, token, label, *, stopped_node=None):
+ return fleet.record(label, {node: fleet.remote(node, "task-records", taskId=token,
+ quiescent=node == stopped_node) for node in fleet.nodes})
+
+
+def owned_records(observed, token):
+ require(isinstance(token, str) and str(uuid.UUID(token)) == token, "root token is not a canonical task UUID")
+ present = {}
+ require(observed and len(observed) in {1, 4}, "root journal omitted bound physical nodes")
+ for node, value in observed.items():
+ require(value.get("readOnly") is True and value.get("taskId") == token
+ and isinstance(value.get("records"), list) and len(value["records"]) == 12, "root journal omitted a disk/name witness")
+ seen = set()
+ for entry in value["records"]:
+ pair = entry.get("disk"), entry.get("kind")
+ require(type(pair[0]) is int and 0 <= pair[0] < 4 and pair[1] in {"intent", "terminal", "report"}
+ and pair not in seen and type(entry.get("present")) is bool, "root journal has duplicate or invalid physical identities")
+ seen.add(pair)
+ if entry["present"]:
+ require(pair[1] not in present, "root task has multiple durable owners")
+ present[pair[1]] = {"node": node, **entry}
+ return present
+
+
+def root_intent(observed, token, coordinator):
+ present = owned_records(observed, token)
+ require(set(present) == {"intent"}, "NOT_OBSERVED: original runnable root intent is unavailable or already terminal")
+ entry = present["intent"]
+ value = entry.get("value", {})
+ require(entry["node"] == coordinator and type(value.get("schema")) is int and value["schema"] == 3
+ and value.get("task_id") == token and value.get("heal_type") == {"type": "cluster"}
+ and not value.get("bucket_incarnation_id") and value.get("priority") == "High"
+ and type(value.get("retry_attempts")) is int and value["retry_attempts"] == 0,
+ "root intent changed coordinator/scope/identity/priority or contains a retry")
+ options = value.get("options", {})
+ required = {"scan_mode": 2, "remove_corrupted": False, "recreate_missing": False,
+ "update_parity": False, "recursive": True, "dry_run": False, "no_lock": False,
+ "pool_index": None, "set_index": None}
+ require(all(type(options.get(name)) is type(expect) and options[name] == expect for name, expect in required.items()),
+ "root intent lost its exact admitted options/selectors")
+ require(isinstance(value.get("created_at"), dict), "root intent omitted original admission time")
+ return entry
+
+
+def duration_ns(value):
+ require(isinstance(value, dict) and set(value) == {"secs", "nanos"}
+ and type(value["secs"]) is int and 0 <= value["secs"] < 2**64
+ and type(value["nanos"]) is int and 0 <= value["nanos"] < 1_000_000_000, "invalid persisted timeout duration")
+ return value["secs"] * 1_000_000_000 + value["nanos"]
+
+
+def same_owner(before, after):
+ require(all(before[name] == after[name] for name in ("node", "disk", "relativePath")), "root record moved to a different physical owner")
+ left, right = copy.deepcopy(before["value"]), copy.deepcopy(after["value"])
+ old_timeout, new_timeout = left["options"].pop("timeout", None), right["options"].pop("timeout", None)
+ require(canonical(left) == canonical(right), "restart changed the original root responsibility or non-timeout options")
+ remaining = duration_ns(new_timeout)
+ maximum = duration_ns(old_timeout) if old_timeout is not None else TIMEOUT_SECONDS * 1_000_000_000
+ require(0 < remaining <= maximum <= TIMEOUT_SECONDS * 1_000_000_000, "graceful restart removed, refunded or exhausted the original task budget")
+ return {"samePhysicalOwner": True, "nonTimeoutIntentUnchanged": True, "beforeTimeoutNs": maximum,
+ "checkpointRemainingTimeoutNs": remaining, "budgetNotRefunded": True}
+
+
+def partial_physical(original, current, targets):
+ actual, restored, remaining = files(current), [], []
+ for target in targets:
+ entry = actual.get((target["node"], target["disk"], target["relativePath"]))
+ if entry is None:
+ remaining.append(target)
+ else:
+ require(entry["sha256"] == target["sha256"] and entry["size"] == target["size"], "partial root repair committed incorrect shard bytes")
+ restored.append(target)
+ require(restored and remaining, "NOT_OBSERVED: actual root repair was not partial at the interruption boundary")
+ expected = copy.deepcopy(original)
+ missing = {(entry["node"], entry["disk"], entry["relativePath"]) for entry in remaining}
+ for node, value in expected.items():
+ value["files"] = [entry for entry in value["files"] if (node, entry["disk"], entry["path"]) not in missing]
+ proof = validate_repaired_physical(expected, current, restored[0], repair_targets=restored)
+ return remaining, {**proof, "actualRestoredParts": len(restored), "actualPendingParts": len(remaining), "backupRescueUsed": False}
+
+
+def restart_identity(before, after, coordinator):
+ require(set(before) == set(after) and coordinator in before, "restart omitted an original peer")
+ for node, old in before.items():
+ new = after[node]
+ require(all(new.get(name) == old.get(name) for name in PROCESS_FIELDS if name not in {"pid", "startTicks"}),
+ "restart changed boot, unit, namespace, candidate, command or environment")
+ require(type(new.get("pid")) is int and new["pid"] > 1 and type(new.get("startTicks")) is int, "restart has no actual new process")
+ if node == coordinator:
+ require((new["pid"], new["startTicks"]) != (old["pid"], old["startTicks"]), "coordinator was not restarted")
+ else:
+ require((new["pid"], new["startTicks"]) == (old["pid"], old["startTicks"]), "non-coordinator peer restarted or changed process")
+ return {"sameUnitEnvironmentVolumesAndCandidate": True, "sameBootAndNamespace": True, "onlyCoordinatorProcessChanged": True}
+
+
+def recovery_status(probe, fleet, token):
+ deadline = time.monotonic() + 15
+ while True:
+ observed = fleet.record("original-token-recovery-query", probe.admin(PATH, [("clientToken", token)], b""))
+ if observed.get("httpStatus") == 200 and isinstance(observed.get("response"), dict):
+ return observed["response"]
+ # The exact absent-token error is an observation during admission
+ # replay, never completion evidence. All other failures remain errors.
+ validate_unknown_token(observed, token)
+ require(time.monotonic() < deadline, "original root token stayed unavailable beyond the 15-second recovery budget")
+ time.sleep(0.1)
+
+
+def root_terminal(observed, token, owner, current, bucket, expected):
+ wanted = wanted_pairs(expected)
+ present = owned_records(observed, token)
+ require(set(present) == {"terminal", "report"}, "completed root task lost its terminal/report or retained runnable responsibility")
+ require(all((entry["node"], entry["disk"]) == (owner["node"], owner["disk"]) for entry in present.values()),
+ "root terminal/report changed original record disk")
+ terminal, report = present["terminal"]["value"], present["report"]["value"]
+ require(type(terminal.get("schema")) is int and terminal["schema"] == 1 and terminal.get("task_id") == token
+ and terminal.get("heal_type") == {"type": "cluster"} and terminal.get("status") == "Completed",
+ "root durable terminal changed task/scope/schema or is not completed")
+ require(canonical(terminal.get("options")) == canonical(owner["value"]["options"]),
+ "completed root task changed its checkpointed options or refunded the remaining budget")
+ require(type(report.get("schema")) is int and report["schema"] == 1 and report.get("terminal") == terminal
+ and canonical(report.get("outcome")) == canonical(current.get("outcome")), "root durable report lost the completed responsibility")
+ require(type(report.get("next_seq")) is int and type(report.get("min_seq")) is int
+ and 1 <= report["min_seq"] <= report["next_seq"] and isinstance(report.get("seqed_items"), list)
+ and 0 < len(report["seqed_items"]) <= 1024, "root report omitted valid bounded result items")
+ previous, seen = 0, set()
+ for sequence, item in report["seqed_items"]:
+ pair = item.get("object"), public_version_id(item.get("versionId"))
+ require(type(sequence) is int and max(previous + 1, report["min_seq"]) <= sequence < report["next_seq"]
+ and item.get("bucket") == bucket and pair in wanted and pair not in seen
+ and item.get("objectSize") == wanted[pair]["payload"]["size"], "root report items escaped complete ACK identities")
+ previous = sequence; seen.add(pair)
+ return {"runnableIntentRetiredOnEveryDrive": True, "sameOriginalRecordDisk": True,
+ "completedReportMatchesControlPlane": True, "retainedDiagnosticsAreFullManifest": False}
+
+
+def crashed_owner(before, after):
+ require(canonical(before) == canonical(after),
+ "SIGKILL changed or moved the frozen original root intent without a graceful checkpoint")
+ return {"samePhysicalOwnerAndIntentBytes": True, "gracefulTimeoutCheckpointClaimed": False,
+ "recoveryBoundary": "replay the persisted admission; no cursor or elapsed-budget checkpoint claim"}
+
+
+def observe_task(probe, fleet, binding, bucket, expected, targets, active, *, original, interruption="graceful"):
+ require(interruption in {"graceful", "sigkill"}, "unsupported root interruption")
+ require(interruption != "sigkill" or len(fleet.nodes) == 4, "F04 root coordinator crash requires actual H2")
+ fleet.record("root-restart-contract", {**REFERENCE, "interruption": interruption,
+ "processBoundary": REFERENCE["processBoundary"] if interruption == "graceful" else
+ "coordinator-only pidfd SIGKILL; persisted admission replay, without shutdown checkpoint"})
+ started = fleet.record("root-admission", probe.admin(PATH, [], json.dumps(OPTIONS).encode()))
+ require(started.get("httpStatus") == 200 and isinstance(started.get("response"), dict), "root admission failed")
+ token = started["response"].get("clientToken")
+ require(isinstance(token, str) and str(uuid.UUID(token)) == token, "root admission omitted canonical task ID")
+ active.append((PATH, token))
+ first = wait_partial(probe, fleet, PATH, token, targets, repair_required=True)
+ admitted = records(fleet, token, "root-intent-admitted-owner")
+ owners = owned_records(admitted, token)
+ require(set(owners) == {"intent"}, "NOT_OBSERVED: original root owner finished before the interruption window")
+ coordinator = owners["intent"]["node"]
+ require(coordinator in fleet.nodes, "root coordinator is outside the actual bound fleet")
+ require(interruption != "sigkill" or all(target["node"] != coordinator for target in targets),
+ "NOT_OBSERVED: F04 root crash must keep every missing-shard target process alive")
+ root_intent(admitted, token, coordinator)
+ fleet.record("actual-root-coordinator", {"node": coordinator, "source": "unique physical admitted intent owner", "taskId": token})
+ processes = fleet.record("processes-before-interruption", {node: fleet.remote(node, "process") for node in fleet.nodes})
+ identity = processes[coordinator]
+ fleet.frozen_processes[coordinator] = identity
+ fleet.record("root-freeze", fleet.remote(coordinator, "process-signal", signal="SIGSTOP", expected=identity))
+ frozen = fleet.record("root-frozen-process", fleet.remote(coordinator, "process"))
+ require(frozen.get("state") == "T" and all(frozen.get(name) == identity.get(name) for name in PROCESS_FIELDS), "root freeze missed the original process")
+ scene = fleet.snapshot("root-frozen-physical")
+ pending, before_physical = partial_physical(original, scene, targets)
+ before_record = root_intent(records(fleet, token, "root-intent-before-stop"), token, coordinator)
+ exit_proof = fleet.record("root-actual-exit", fleet.remote(coordinator,
+ "process-graceful" if interruption == "graceful" else "process-crash", expected=identity))
+ if interruption == "sigkill":
+ require(exit_proof.get("actualSignal") == 9 and exit_proof.get("graceful") is False
+ and exit_proof.get("pidfdBound") is True and exit_proof.get("platformEventIssued") is False,
+ "root coordinator has no exact process-bound SIGKILL exit witness")
+ del fleet.frozen_processes[coordinator]
+ fleet.record("release-exited-unit", fleet.remote(coordinator, "stop"))
+ after_record = root_intent(records(fleet, token, "root-intent-after-exit", stopped_node=coordinator), token, coordinator)
+ budget = same_owner(before_record, after_record) if interruption == "graceful" else crashed_owner(before_record, after_record)
+ exit_scenes = []
+ for index in range(3):
+ exit_scenes.append(fleet.record("root-physical-after-exit", {node: fleet.remote(node, "snapshot", quiescent=node == coordinator) for node in fleet.nodes}))
+ if index < 2:
+ time.sleep(5)
+ require(all(physical_content(value) == physical_content(exit_scenes[0]) for value in exit_scenes[1:]),
+ "coordinator's retiring peer writes did not settle before restart")
+ after_scene = exit_scenes[-1]
+ remaining, after_physical = partial_physical(original, after_scene, targets)
+ require({entry["key"] for entry in remaining} <= {entry["key"] for entry in pending}, "interruption lost an already repaired shard")
+ fleet.record("root-original-unit-restart", fleet.remote(coordinator, "start"))
+ fleet.wait_ready()
+ restarted = fleet.record("processes-after-restart", {node: fleet.remote(node, "process") for node in fleet.nodes})
+ continuity = restart_identity(processes, restarted, coordinator)
+ deadline = time.monotonic() + TASK_SECONDS
+ while True:
+ current = recovery_status(probe, fleet, token)
+ require(current.get("summary") in {"running", "finished"}, "original root task failed or became cancelled during recovery")
+ if current["summary"] == "finished":
+ break
+ require(time.monotonic() < deadline, "original root task did not recover within its pre-registered budget")
+ time.sleep(0.05)
+ terminal = complete_outcome(current, bucket, expected, {(entry["key"], entry["versionId"]) for entry in remaining})
+ active.remove((PATH, token))
+ idle = idle_fleet(fleet, binding)
+ journal_before = records(fleet, token, "root-completed-records")
+ journal = root_terminal(journal_before, token, after_record, current, bucket, expected)
+ complete_scene = fleet.snapshot("root-completed-live-scene")
+ # Repeat coordinator restart after completion. It must replay the durable
+ # terminal unchanged, never create a new task or write another user shard.
+ fleet.record("completed-coordinator-stop", fleet.remote(coordinator, "stop"))
+ fleet.record("completed-coordinator-start", fleet.remote(coordinator, "start"))
+ fleet.wait_ready()
+ repeated = fleet.record("processes-after-repeat-restart", {node: fleet.remote(node, "process") for node in fleet.nodes})
+ restart_identity(restarted, repeated, coordinator)
+ repeat_idle = idle_fleet(fleet, binding)
+ repeat = recovery_status(probe, fleet, token)
+ require(repeat.get("summary") == "finished" and canonical(repeat.get("outcome")) == canonical(current["outcome"]), "repeat restart lost or changed original terminal responsibility")
+ journal_after = records(fleet, token, "root-completed-records-after-repeat")
+ root_terminal(journal_after, token, after_record, repeat, bucket, expected)
+ require(canonical(journal_before) == canonical(journal_after), "repeat restart rewrote the completed root journal")
+ require(physical_content(complete_scene) == physical_content(fleet.snapshot("root-repeat-live-scene")), "completed task revived and rewrote user files after repeat restart")
+ if interruption == "sigkill":
+ return {"root-coordinator-kill": {"verdict": "PASS", "originalTaskId": token,
+ "runningWindow": first["outcome"]["counters"], "beforeInterruptionPhysical": before_physical,
+ "afterActualExitPhysical": after_physical, "actualExit": exit_proof, "processContinuity": continuity,
+ "persistedAdmission": budget, "sameTaskRecovered": True, "newStartRequestIssued": False,
+ "completeResponsibility": terminal, "terminalJournal": journal, "repeatRestartIdle": repeat_idle,
+ "completedJournalBytesPreserved": True, "allPeerIdle": idle}}
+ return {"root-partial-graceful": {"verdict": "PASS", "originalTaskId": token, "runningWindow": first["outcome"]["counters"],
+ "beforeInterruptionPhysical": before_physical, "afterActualExitPhysical": after_physical,
+ "sameTaskRecovered": True, "newStartRequestIssued": False, "processContinuity": continuity,
+ "completeResponsibility": terminal, "allPeerIdle": idle},
+ "same-record-disk-resume": {"verdict": "PASS", "originalTaskId": token, "originalRecordOwner": before_record,
+ "budget": budget, "terminalJournal": journal, "repeatRestartIdle": repeat_idle,
+ "completedJournalBytesPreserved": True, "cursorResumeClaimed": False, "recordDiskFailoverClaimed": False}}
+
+
+def run_topology(args, topology, nodes, *, observer=observe_task, branches=BRANCHES):
+ return dx_topology(args, topology, nodes, observer=observer, branches=branches, runtime_profile="root-restart", policy_reference=REFERENCE)
+
+
+if __name__ == "__main__":
+ try:
+ dx_main(case_id=CASE_ID, observer=observe_task, branches=BRANCHES, runner=run_topology)
+ except Exception as exc:
+ print(f"Heal root-restart validation failed: {exc}", file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_heal/run_case.sh b/scripts/validation_heal/run_case.sh
new file mode 100755
index 00000000..77dc86e3
--- /dev/null
+++ b/scripts/validation_heal/run_case.sh
@@ -0,0 +1,319 @@
+#!/usr/bin/env bash
+set -Eeuo pipefail
+
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+RUSTFS_HEAL_SCRIPT_SOURCE_ONLY=1 source "${ROOT}/rustfs_heal_test.sh"
+ASSUME_YES=1
+candidate="" testing_commit="" artifacts="" case_id=""
+driver=healthy.py
+environment_gate=0
+replacement_admission_gate=0
+i05_args=()
+f07_args=()
+while [ "$#" -gt 0 ]; do
+ case "$1" in
+ --validation-case) case_id="$2"; shift 2 ;;
+ --candidate-manifest) candidate="$2"; shift 2 ;;
+ --testing-commit) testing_commit="$2"; shift 2 ;;
+ --validation-artifacts-dir) artifacts="$2"; shift 2 ;;
+ --dry-run) DRY_RUN=1; shift ;;
+ -y|--yes) shift ;;
+ *) die "unknown Heal validation option: $1" ;;
+ esac
+done
+case "${case_id}" in
+ heal.admin-healthy-noop|heal.admin-invalid-request|heal.admin-authorization) ;;
+ heal.bucket-incarnation-recreation) driver=incarnation.py ;;
+ heal.outcome-counters-and-error-truth) driver=outcome.py ;;
+ heal.checkpoint-and-mrf-corruption) driver=persistence.py ;;
+ heal.lock-timeout-and-caller-abort) driver=lock_timeout_runner.py ;;
+ heal.node-outage-rejoin) driver=node_outage.py ;;
+ heal.status-partial-disabled-unknown) driver=status.py ;;
+ heal.incremental-cursor-and-truncation) driver=cursor.py ;;
+ heal.admin-stop-and-stale-token) driver=stop.py ;;
+ heal.cancelled-task-restart) driver=cancel_restart.py ;;
+ heal.admin-overlap-and-force-start) driver=overlap.py ;;
+ heal.root-graceful-coordinator-restart) driver=root_restart.py ;;
+ heal.full-cluster-restart) driver=full_restart.py ;;
+ heal.mrf-offline-write-replay) driver=mrf_replay.py ;;
+ heal.performance-abba) driver=performance.py ;;
+ heal.admin-scope-isolation) driver=scopes.py ;;
+ heal.admin-option-contract) driver=options.py ;;
+ heal.queue-pressure-and-admission) driver=queue_pressure.py ;;
+ heal.bucket-metadata-integrity) driver=bucket_metadata.py ;;
+ heal.disk-replacement-unformatted) driver=unformatted.py; replacement_admission_gate=1 ;;
+ heal.disk-replacement-formatted) driver=versions.py; replacement_admission_gate=1 ;;
+ heal.below-write-quorum) driver=write_quorum.py ;;
+ heal.foreign-format-and-unknown-schema) driver=schema.py ;;
+ heal.encrypted-objects) driver=encrypted.py ;;
+ heal.coordinator-process-crash) driver=coordinator_crash.py ;;
+ heal.disk-and-node-flapping) driver=flapping.py ;;
+ heal.enospc-inodes-and-quota) driver=capacity.py ;;
+ heal.network-partition-and-lock-quorum) driver=network.py ;;
+ heal.multi-pool-target-isolation) driver=multi_pool.py ;;
+ heal.conflicting-subquorum-authority) driver=authority_runner.py ;;
+ heal.dangling-and-orphan-safety) driver=orphan.py ;;
+ heal.admin-nolock-safety) driver=nolock.py ;;
+ heal.race-unversioned-overwrite|heal.race-versioned-put|heal.race-version-delete|heal.race-markers-and-batch-delete|heal.race-multipart-complete-abort|heal.race-copy-and-metadata-update) driver=race.py ;;
+ heal.mixed-version-upgrade-rollback|heal.dedicated-host-forced-restart|heal.race-ilm-expiry-and-transition|heal.race-replication-resync|heal.with-expansion-rebalance|heal.with-rebalance-stop-resume|heal.with-decommission|heal.with-decommission-cancel-clear|heal.after-retired-pool-removal|heal.ec84-multi-set-restart|heal.slow-io-and-eio|heal.kms-and-tier-outage|heal.long-soak)
+ driver=environment_gate.py; environment_gate=1 ;;
+ heal.read-quorum-boundary) driver=quorum_boundary.py ;;
+ heal.historical-versions|heal.repeat-idempotency|heal.versioning-suspended-null|heal.below-read-quorum|heal.missing-or-corrupt-xlmeta|heal.object-metadata-and-tags|heal.multiple-shard-loss|heal.special-keys-and-pagination|heal.object-size-boundaries|heal.object-lock-retention|heal.metadata-payload-mismatch|heal.read-repair-attribution|heal.disk-readonly-and-permission|heal.scanner-cold-object-repair|heal.delete-marker-semantics|heal.stale-version-rejoin|heal.disk-loss-and-repair|heal.replacement-auto-rebuild|heal.target-graceful-restart|heal.target-process-crash) driver=versions.py ;;
+ heal.admin-token-cross-node-status|heal.admin-dry-run|heal.missing-part-file|heal.data-shard-bitrot|heal.parity-shard-bitrot|heal.truncated-and-zeroed-shard|heal.completed-multipart-object|heal.range-head-and-conditions|heal.manual-normal-attribution|heal.manual-deep-attribution)
+ driver=shards.py ;;
+ *) die "automatic Heal executor is not migrated: ${case_id}" ;;
+esac
+if [ "${environment_gate}" -eq 1 ]; then
+ if [ "${DRY_RUN}" -eq 1 ]; then
+ log "${case_id}: environment-gated and disabled by default; set AUTO_TESTING_ENABLE_ENV_GATED_HEAL=1 with a capability manifest to prepare an operator-assisted run"
+ exit 0
+ fi
+ [ -f "${candidate}" ] || die "candidate manifest missing"
+ [ -n "${artifacts}" ] || die "validation artifacts directory required"
+ [ ! -e "${artifacts}" ] || die "fresh artifact directory required"
+ python3 -B "${ROOT}/scripts/validation_heal/${driver}" \
+ --case-id "${case_id}" \
+ --candidate-manifest "${candidate}" \
+ --testing-commit "${testing_commit}" \
+ --validation-artifacts-dir "${artifacts}"
+ exit 0
+fi
+[ "${#NODES[@]}" -ge 4 ] || die "H1/H2 requires four current auto-testing Heal nodes"
+NODES=("${NODES[0]}" "${NODES[1]}" "${NODES[2]}" "${NODES[3]}")
+if [ "${DRY_RUN}" -eq 1 ]; then
+ log "${case_id}: current Heal nodes ${NODES[*]}, immutable deb install, run-owned H1/H2 service and four roots per node"
+ log "Source branches, ACK ledger, candidate-bound physical evidence, exact task outcomes, full S3 verification and cleanup"
+ exit 0
+fi
+for program in python3 ssh curl jq rc; do need_cmd "${program}" "Heal validation"; done
+[ -f "${candidate}" ] || die "candidate manifest missing"
+[ -n "${artifacts}" ] && [ ! -e "${artifacts}" ] || die "fresh artifact directory required"
+[ "${testing_commit}" = "$(git -C "${ROOT}" rev-parse HEAD)" ] || die "testing commit differs from checkout"
+[[ "${GITHUB_RUN_ID:-}" =~ ^[1-9][0-9]*$ ]] || die "Functional Chain workflow identity required"
+PACKAGE_URL="$(jq -er '.package_url' "${candidate}")"
+PACKAGE_SHA256="$(jq -er '.package_sha256' "${candidate}")"
+[[ "${PACKAGE_SHA256}" =~ ^[0-9a-f]{64}$ ]] || die "invalid candidate package SHA-256"
+export RUSTFS_NODES="${NODES[*]}" RUSTFS_SSH_USER="${SSH_USER}" RUSTFS_SSH_PORT="${SSH_PORT}"
+tool_args=()
+if [ -n "${VALIDATION_HEAL_TOOL_DIRECTORY:-}" ]; then
+ tool_args=(--tool-directory "${VALIDATION_HEAL_TOOL_DIRECTORY}")
+fi
+if [ "${case_id}" = heal.lock-timeout-and-caller-abort ]; then
+ diagnostic_branch="${I05_DIAGNOSTIC_BRANCH:-source-plan}"
+ case "${diagnostic_branch}" in
+ timeout|queued-successor|source-plan) ;;
+ *) die "unsupported I05 diagnostic branch: ${diagnostic_branch}" ;;
+ esac
+ i05_args+=(--diagnostic-branch "${diagnostic_branch}")
+ if [ -n "${VALIDATION_HEAL_NATIVE_DIRECTORY:-}" ]; then
+ i05_args+=(--native-directory "${VALIDATION_HEAL_NATIVE_DIRECTORY}")
+ fi
+elif [ "${case_id}" = heal.checkpoint-and-mrf-corruption ] && [ -n "${VALIDATION_HEAL_NATIVE_DIRECTORY:-}" ]; then
+ f07_args+=(--native-directory "${VALIDATION_HEAL_NATIVE_DIRECTORY}")
+fi
+[ "${driver}" = healthy.py ] || [ "${#tool_args[@]}" -gt 0 ] || die "physical Heal cases require candidate-bound bitrot tools"
+evidence="${artifacts}/evidence/${case_id}"
+mkdir -p "${artifacts}/cases" "${evidence}"
+rc_binary="$(command -v rc)"
+if [ "${case_id}" = heal.status-partial-disabled-unknown ]; then
+ rc_binary="$(python3 -B "${ROOT}/scripts/validation_heal/rc_binary.py" --directory "${evidence}/rc-client")"
+fi
+if [ "${driver}" = race.py ] || [ "${driver}" = nolock.py ]; then
+ python3 -B "${ROOT}/scripts/validation_heal/${driver}" preflight \
+ --candidate "${candidate}" --case-id "${case_id}" --testing-commit "${testing_commit}" \
+ > "${evidence}/native-preflight.json"
+fi
+if [ "${driver}" = lock_timeout_runner.py ]; then
+ python3 -B "${ROOT}/scripts/validation_heal/${driver}" preflight \
+ --candidate "${candidate}" --case-id "${case_id}" --testing-commit "${testing_commit}" \
+ "${tool_args[@]}" "${i05_args[@]}" --rc-bin "${rc_binary}" > "${evidence}/timeout-preflight.json"
+fi
+python3 -B "${ROOT}/scripts/validation_result.py" --case-id "${case_id}" --profile cluster-1pool \
+ --result-file "${artifacts}/cases/${case_id}.json" --verdict RUNNING --cleanup-status PENDING
+
+bootstrap() {
+ local node
+ for node in "${NODES[@]}"; do
+ stop_existing "${node}"
+ done
+ step1_download
+ for node in "${NODES[@]}"; do
+ {
+ printf 'set -euo pipefail\n'
+ printf "printf '%%s %%s\\n' %q /home/rustfs/packages/rustfs.deb | sha256sum -c -\n" "${PACKAGE_SHA256}"
+ printf 'sudo -n dpkg -i --force-confold /home/rustfs/packages/rustfs.deb\n'
+ printf 'sudo -n systemctl stop rustfs\n'
+ } | run_remote "${node}"
+ done
+}
+
+stop_existing() {
+ local node="$1"
+ run_remote "${node}" <<'REMOTE'
+set -euo pipefail
+load_state="$(systemctl show rustfs -p LoadState --value)"
+if [ "${load_state}" != not-found ]; then
+ sudo -n systemctl stop rustfs
+fi
+! systemctl is-active --quiet rustfs
+REMOTE
+}
+
+finalize_package() {
+ local node failed=0
+ for node in "${NODES[@]}"; do
+ (stop_existing "${node}") || failed=1
+ ssh "${SSH_OPTS[@]}" "${SSH_USER}@${node}" \
+ 'set -eu
+status=0
+state="$(dpkg-query -W -f="\${db:Status-Status}" rustfs 2>/dev/null)" || status=$?
+case "${status}:${state}" in
+ 0:installed|0:config-files) sudo -n dpkg -P rustfs ;;
+ 1:) ;;
+ *) printf "unexpected rustfs package query state\n" >&2; exit 1 ;;
+esac
+status=0
+state="$(dpkg-query -W -f="\${db:Status-Status}" rustfs 2>/dev/null)" || status=$?
+test "${status}:${state}" = "1:"' || failed=1
+ ssh "${SSH_OPTS[@]}" "${SSH_USER}@${node}" \
+ '! systemctl is-active --quiet rustfs' || failed=1
+ done
+ return "${failed}"
+}
+
+bootstrap_rc=0 case_rc=0 cleanup_status=PASS binary_sha=""
+package_active=0
+on_exit() {
+ local exit_code=$?
+ trap - EXIT
+ if [ "${package_active}" -eq 1 ]; then
+ (finalize_package) >> "${evidence}/cleanup.log" 2>&1 || true
+ fi
+ exit "${exit_code}"
+}
+trap on_exit EXIT
+trap 'exit 130' INT
+trap 'exit 143' TERM
+if [ "${replacement_admission_gate}" -eq 1 ]; then
+ preflight_rc=0
+ python3 -B "${ROOT}/scripts/validation_heal/replacement_preflight.py" \
+ --case-id "${case_id}" --candidate "${candidate}" --testing-commit "${testing_commit}" --evidence "${evidence}" \
+ > "${evidence}/predeployment-preflight.log" 2>&1 || preflight_rc=$?
+ cat "${evidence}/predeployment-preflight.log"
+ if [ "${preflight_rc}" -ne 0 ]; then
+ python3 -B "${ROOT}/scripts/validation_result.py" --case-id "${case_id}" --profile cluster-1pool \
+ --result-file "${artifacts}/cases/${case_id}.json" --verdict INFRA_ERROR --cleanup-status PASS \
+ --candidate-manifest "${candidate}" --testing-commit "${testing_commit}" --rc-bin "${rc_binary}" \
+ --evidence "evidence/${case_id}/predeployment-preflight.log" \
+ --reason "${case_id} read-only predeployment admission could not complete; package and service state were left untouched"
+ exit 1
+ fi
+ preflight_evidence_args=(
+ --evidence "evidence/${case_id}/predeployment-preflight.log"
+ --evidence "evidence/${case_id}/healthy-summary.json"
+ )
+ while IFS= read -r preflight_file; do
+ preflight_relative="${preflight_file#${artifacts}/}"
+ [ "${preflight_relative}" != "${preflight_file}" ] || die "B03 preflight evidence escaped the artifact root"
+ preflight_evidence_args+=(--evidence "${preflight_relative}")
+ done < <(find "${evidence}/replacement-admission" -type f -name '*.json' -print | LC_ALL=C sort)
+ preflight_status="$(jq -er '.status' "${evidence}/healthy-summary.json")"
+ if [ "${preflight_status}" = BLOCKED ]; then
+ python3 -B "${ROOT}/scripts/validation_result.py" --case-id "${case_id}" --profile cluster-1pool \
+ --result-file "${artifacts}/cases/${case_id}.json" --verdict BLOCKED --cleanup-status PASS \
+ --candidate-manifest "${candidate}" --testing-commit "${testing_commit}" --rc-bin "${rc_binary}" \
+ --predeployment-blocked "${preflight_evidence_args[@]}" \
+ --reason "${case_id} exact endpoint mount admission and a reviewed mount-switch adapter are unavailable; the read-only preflight stopped before service or package changes"
+ exit 1
+ elif [ "${preflight_status}" = INFRA_ERROR ]; then
+ python3 -B "${ROOT}/scripts/validation_result.py" --case-id "${case_id}" --profile cluster-1pool \
+ --result-file "${artifacts}/cases/${case_id}.json" --verdict INFRA_ERROR --cleanup-status PASS \
+ --candidate-manifest "${candidate}" --testing-commit "${testing_commit}" --rc-bin "${rc_binary}" \
+ "${preflight_evidence_args[@]}" \
+ --reason "${case_id} read-only admission returned incomplete node evidence; package and service state were left untouched"
+ exit 1
+ elif [ "${preflight_status}" != ADMITTED ]; then
+ die "${case_id} predeployment admission returned an unknown status: ${preflight_status}"
+ fi
+fi
+package_active=1
+(bootstrap) > "${evidence}/setup.log" 2>&1 || bootstrap_rc=$?
+cat "${evidence}/setup.log"
+if [ "${bootstrap_rc}" -eq 0 ]; then
+ for node in "${NODES[@]}"; do
+ node_sha="$(ssh "${SSH_OPTS[@]}" "${SSH_USER}@${node}" sha256sum /usr/bin/rustfs | awk '{print $1}')"
+ [[ "${node_sha}" =~ ^[0-9a-f]{64}$ ]] || die "installed candidate digest unavailable on ${node}"
+ if [ -n "${binary_sha}" ] && [ "${binary_sha}" != "${node_sha}" ]; then die "node candidate digests differ"; fi
+ binary_sha="${node_sha}"
+ done
+ python3 -B "${ROOT}/scripts/validation_heal/${driver}" --candidate "${candidate}" \
+ --case-id "${case_id}" \
+ "${tool_args[@]}" "${i05_args[@]}" "${f07_args[@]}" \
+ --testing-commit "${testing_commit}" --binary-sha "${binary_sha}" --rc-bin "${rc_binary}" \
+ --evidence "${evidence}" > "${evidence}/test.log" 2>&1 || case_rc=$?
+ cat "${evidence}/test.log"
+ jq -e '.cleanup == "PASS"' "${evidence}/healthy-summary.json" >/dev/null 2>&1 || cleanup_status=FAIL
+else
+ case_rc=1
+fi
+(finalize_package) > "${evidence}/cleanup.log" 2>&1 || cleanup_status=FAIL
+package_active=0
+cat "${evidence}/cleanup.log"
+[ -n "${binary_sha}" ] || {
+ python3 -B "${ROOT}/scripts/validation_result.py" --case-id "${case_id}" --profile cluster-1pool \
+ --result-file "${artifacts}/cases/${case_id}.json" --verdict INFRA_ERROR --cleanup-status "${cleanup_status}" \
+ --candidate-manifest "${candidate}" --testing-commit "${testing_commit}" --rc-bin "${rc_binary}" \
+ --evidence "evidence/${case_id}/setup.log" --evidence "evidence/${case_id}/cleanup.log" \
+ --reason "Heal candidate bootstrap failed before a verified binary was installed"
+ exit 1
+}
+verdict=FAIL reason="Heal source assertions did not pass on every required H1/H2 branch"
+if [ "${case_rc}" -eq 0 ] && [ "${cleanup_status}" = PASS ]; then
+ verdict=PASS; reason=""
+elif [ "${case_rc}" -eq 0 ]; then
+ verdict=INFRA_ERROR; reason="Heal environment finalization failed"
+fi
+if [ "${replacement_admission_gate:-0}" -eq 1 ] && [ "${cleanup_status}" = PASS ] && \
+ jq -e '.replacementAdmissionBlocked == true and .complete == false and .canonicalPassClaimed != true' \
+ "${evidence}/healthy-summary.json" >/dev/null 2>&1; then
+ verdict=BLOCKED; reason="${case_id} replacement admission did not permit deployment or fixture seed; the read-only gate or the reservation-time recheck blocked the case"
+fi
+if [ "${case_id}" = heal.bucket-incarnation-recreation ] && [ "${cleanup_status}" = PASS ] && \
+ jq -e '.diagnosticBranchesPassed == true and .complete == false and .canonicalCasePassClaimed == false' \
+ "${evidence}/healthy-summary.json" >/dev/null 2>&1; then
+ verdict=BLOCKED; reason="C11 diagnostic execution remains outside canonical PASS until every H1/H2 branch is independently audited"
+fi
+if [ "${case_id}" = heal.checkpoint-and-mrf-corruption ] && [ "${cleanup_status}" = PASS ] && \
+ jq -e '.diagnosticBranchesPassed == true and .complete == false and .canonicalCasePassClaimed == false' \
+ "${evidence}/healthy-summary.json" >/dev/null 2>&1; then
+ verdict=BLOCKED; reason="F07 diagnostic execution lacks full native cut coverage and independent live audit; partial success cannot certify the canonical case"
+fi
+if [ "${case_id}" = heal.outcome-counters-and-error-truth ] && [ "${cleanup_status}" = PASS ]; then
+ if [ "${case_rc}" -eq 0 ] || jq -e '.diagnosticBranchesPassed == true and .complete == false and .canonicalCasePassClaimed == false' \
+ "${evidence}/healthy-summary.json" >/dev/null 2>&1; then
+ verdict=BLOCKED; reason="J02 manual diagnostics require independent acceptance of all original source procedures; canonical admission remains disabled"
+ fi
+fi
+if [ "${case_id}" = heal.lock-timeout-and-caller-abort ]; then
+ # This route is an explicit diagnostic invocation, never canonical admission.
+ # Source-plan also retains independent native scope and awaits full live audit.
+ if [ "${cleanup_status}" = PASS ] && [ "${case_rc}" -eq 0 ] && \
+ jq -e '.diagnosticComplete == true and .complete == false and .canonicalCasePassClaimed == false' \
+ "${evidence}/healthy-summary.json" >/dev/null 2>&1; then
+ verdict=BLOCKED; reason="I05 ${diagnostic_branch:-source-plan} execution completed; independent complete source-plan live audit remains required"
+ elif [ "${case_rc}" -eq 0 ]; then
+ verdict=INFRA_ERROR; reason="I05 diagnostic summary or cleanup is incomplete; canonical PASS is prohibited"
+ fi
+fi
+args=("${ROOT}/scripts/validation_result.py" --case-id "${case_id}" --profile cluster-1pool
+ --result-file "${artifacts}/cases/${case_id}.json" --verdict "${verdict}" --cleanup-status "${cleanup_status}"
+ --candidate-manifest "${candidate}" --testing-commit "${testing_commit}" --binary-sha256 "${binary_sha}"
+ --rc-bin "${rc_binary}" --evidence "evidence/${case_id}/setup.log"
+ --evidence "evidence/${case_id}/test.log" --evidence "evidence/${case_id}/cleanup.log"
+ --evidence "evidence/${case_id}/healthy-summary.json")
+if [ "${case_id}" = heal.status-partial-disabled-unknown ]; then
+ args+=(--evidence "evidence/${case_id}/rc-client/manifest.json")
+fi
+if [ -n "${reason}" ]; then args+=(--reason "${reason}"); fi
+python3 -B "${args[@]}"
+[ "${verdict}" = PASS ]
diff --git a/scripts/validation_heal/scanner_repair.py b/scripts/validation_heal/scanner_repair.py
new file mode 100644
index 00000000..5d6a10fe
--- /dev/null
+++ b/scripts/validation_heal/scanner_repair.py
@@ -0,0 +1,203 @@
+"""E04: cursor-bounded scanner discovery, source admission and exact task proof."""
+
+from datetime import datetime, timezone
+import json
+import time
+from urllib.parse import parse_qs, quote, urlsplit
+from uuid import UUID
+
+try:
+ from .heal_probe import Probe, require
+except ImportError:
+ from heal_probe import Probe, require
+
+
+BRANCHES = ("cold-missing", "cold-bitrot", "scanner-cycle-boundary")
+BUDGET_SECONDS = 240
+SOURCE_TARGETS = {"rustfs::scanner", "rustfs::scanner::folder", "rustfs::heal::channel",
+ "rustfs::heal::manager", "rustfs::heal::task"}
+
+
+def event_time(event):
+ value = event.get("timestamp")
+ if not isinstance(value, str): return None
+ try: stamp = datetime.fromisoformat(value.replace("Z", "+00:00"))
+ except ValueError: return None
+ return stamp.astimezone(timezone.utc) if stamp.tzinfo is not None else None
+
+
+def null_version(value):
+ return value in {None, "", "null", "None", "00000000-0000-0000-0000-000000000000"}
+
+
+class JournalWindow:
+ def __init__(self, fleet, label, *, event_targets=None, strict_json=False):
+ self.fleet, self.label = fleet, label
+ self.event_targets = event_targets
+ self.strict_json = strict_json
+ self.events = {node: [] for node in fleet.nodes}
+ self.cursors = {}
+ for node in fleet.nodes:
+ observed = fleet.record(label + "-checkpoint", {"node": node, **fleet.remote(node, "logs", checkpoint=True)})
+ require(observed.get("complete") is True and observed.get("afterCursor"), "missing source journal checkpoint")
+ self.cursors[node] = observed["afterCursor"]
+
+ def capture(self):
+ for node in self.fleet.nodes:
+ for page in range(6):
+ observed = self.fleet.record(self.label + "-window", {"node": node,
+ **self.fleet.remote(node, "logs", afterCursor=self.cursors[node])})
+ require(observed.get("complete") is True and observed.get("beforeCursor") == self.cursors[node]
+ and observed.get("afterCursor"), "journal window lost its checkpoint or completeness")
+ require(observed.get("hasMore") is not True or observed["afterCursor"] != self.cursors[node],
+ "journal pagination did not advance")
+ self.cursors[node] = observed["afterCursor"]
+ for line in observed.get("journal", "").splitlines():
+ try: event = json.loads(line)
+ except json.JSONDecodeError:
+ require(not self.strict_json, "journal window contains a malformed JSON event")
+ continue
+ require(not self.strict_json or isinstance(event, dict), "journal window contains a non-object JSON event")
+ # Raw pages remain in immutable evidence; keep only source
+ # attribution events in memory, not unrelated debug noise.
+ if isinstance(event, dict) and (self.event_targets is None
+ or event.get("target") in self.event_targets):
+ self.events[node].append(event)
+ if observed.get("hasMore") is not True: break
+ else:
+ raise ValueError("journal window exceeds its page budget; source attribution is incomplete")
+ require(len(self.events[node]) <= 60000, "scanner evidence exceeded its registered event budget")
+ return self.events
+
+
+def cycle_pairs(events):
+ """Pair actual timestamps on one leader; completed cycle may serialize as 0."""
+ pairs, pending = [], None
+ for event in events:
+ if event.get("target") != "rustfs::scanner" or event.get("event") != "scanner_cycle_state": continue
+ stamp = event_time(event)
+ if stamp is None:
+ pending = None
+ continue
+ state, cycle = event.get("state"), event.get("cycle")
+ if state == "started":
+ pending = (cycle, stamp) if type(cycle) is int and cycle > 0 and event.get("scan_mode") == "Deep" else None
+ elif state == "completed":
+ if pending and type(cycle) is int and cycle in (0, pending[0]) and stamp > pending[1]:
+ pairs.append({"cycle": pending[0], "started": pending[1], "completed": stamp})
+ pending = None
+ elif state in {"failed", "aborted", "budget_exhausted", "deferred"}:
+ pending = None
+ return pairs
+
+
+def discovery_cycle(events, node, discovery):
+ """Bind remote disk discovery to its RPC cycle, retaining the task owner."""
+ stamp = event_time(discovery)
+ spans = discovery.get("spans", [])
+ if stamp is None or not isinstance(spans, list) or any(not isinstance(span, dict) for span in spans): return None
+ remote = [span for span in spans if span.get("name") == "http-request"]
+ if not remote:
+ matches = [(node, pair) for pair in cycle_pairs(events[node])
+ if pair["started"] <= stamp <= pair["completed"]]
+ request = None
+ else:
+ if len(remote) != 1: return None
+ span = remote[0]
+ request_id = discovery.get("request_id")
+ if not isinstance(request_id, str) or not request_id or span.get("request_id") != request_id or span.get("method") != "POST": return None
+ if not isinstance(span.get("uri"), str): return None
+ if not any(s.get("name") == "nsscanner_disk" and s.get("scan_mode") == "Deep" for s in spans): return None
+ try:
+ url = urlsplit(span["uri"])
+ query = parse_qs(url.query, keep_blank_values=True, strict_parsing=True)
+ if url.scheme or url.netloc or url.fragment or url.path != "/rustfs/rpc/ns_scanner": return None
+ cycle, = query["ns_scanner_cycle"]
+ if not cycle.isascii() or not cycle.isdecimal() or int(cycle) <= 0: return None
+ rpc_id, = query["ns_scanner_request_id"]
+ if UUID(rpc_id).int == 0: return None
+ except (KeyError, TypeError, ValueError):
+ return None
+ matches = [(leader, pair) for leader, rows in events.items() for pair in cycle_pairs(rows)
+ if pair["cycle"] == int(cycle) and pair["started"] <= stamp <= pair["completed"]]
+ request = {"requestId": request_id, "scannerRequestId": rpc_id, "uri": span["uri"]}
+ if len(matches) != 1: return None
+ leader, pair = matches[0]
+ return {"node": leader, **{name: value.isoformat() if isinstance(value, datetime) else value
+ for name, value in pair.items()}, "remoteRequest": request}
+
+
+def source_tasks(events, bucket, entry):
+ """Bind discovery -> channel -> accepted Scanner source -> exact object task."""
+ require(entry["versionId"] == "null" and not entry["deleteMarker"], "scanner cold fixture must be unversioned data")
+ found = []
+ for node, rows in events.items():
+ discoveries = [event for event in rows if event.get("target") == "rustfs::scanner::folder"
+ and event.get("event") == "scanner_heal_admission" and event.get("state") == "request_started"
+ and event.get("bucket") == bucket and event.get("object") == entry["key"]
+ and "version_id" in event and null_version(event["version_id"]) and event_time(event) is not None]
+ for start in rows:
+ if not (start.get("target") == "rustfs::heal::task" and start.get("message") == "Heal object started"
+ and start.get("stage") == "start" and start.get("bucket") == bucket
+ and start.get("object") == entry["key"] and "version_id" in start
+ and null_version(start["version_id"])): continue
+ token, stamp = start.get("task_id"), event_time(start)
+ if not isinstance(token, str) or not token or stamp is None: continue
+ received = [event for event in rows if event.get("target") == "rustfs::heal::channel"
+ and event.get("state") == "start_received" and event.get("request_id") == token
+ and event.get("bucket") == bucket and event.get("object_prefix") == entry["key"]]
+ accepted = [event for event in rows if event.get("target") == "rustfs::heal::manager"
+ and event.get("message") == "Heal queue request accepted" and event.get("request_id") == token
+ and event.get("source") == "scanner" and event.get("result") == "accepted"]
+ completed = [event for event in rows if event.get("target") == "rustfs::heal::manager"
+ and event.get("state") == "task_completed" and event.get("task_id") == token]
+ for receipt in received:
+ receipt_time = event_time(receipt)
+ if receipt_time is None: continue
+ discovery = next((event for event in reversed(discoveries) if event_time(event) <= receipt_time), None)
+ if discovery is None: continue
+ admission = next((event for event in accepted if event_time(event) is not None
+ and receipt_time <= event_time(event) <= stamp), None)
+ done = next((event for event in completed if event_time(event) is not None and stamp <= event_time(event)), None)
+ cycle = discovery_cycle(events, node, discovery)
+ if admission is not None and done is not None and cycle is not None:
+ found.append({"node": node, "taskId": token, "discovery": discovery, "received": receipt,
+ "admission": admission, "started": start, "completed": done,
+ "cycle": cycle})
+ break
+ return found
+
+
+def observe_task(probe, fleet, window, bucket, entry, branch, *, healthy=False):
+ require(fleet.runtime_profile == "scanner", "scanner proof requires its isolated runtime profile")
+ deadline = time.monotonic() + BUDGET_SECONDS
+ while True:
+ events = window.capture()
+ tasks = source_tasks(events, bucket, entry)
+ if tasks:
+ if branch != "scanner-cycle-boundary" or healthy or any(len(cycle_pairs(events[task["cycle"]["node"]])) >= 2 for task in tasks):
+ break
+ require(time.monotonic() < deadline, "ATTRIBUTION_NOT_OBSERVED: scanner did not discover, admit and complete the exact target in its cycle")
+ time.sleep(1)
+ # A later healthy scanner pass cannot certify the injected fault. Inspect
+ # tasks in order and select the first exact repaired receipt for this run.
+ observations = []
+ for task in tasks:
+ cycles = len(cycle_pairs(events[task["cycle"]["node"]]))
+ if branch == "scanner-cycle-boundary" and not healthy and cycles < 2: continue
+ endpoint = "node" + str(fleet.nodes.index(task["node"]) + 1)
+ observer = Probe(probe.binding, endpoint)
+ path = "/rustfs/admin/v3/heal/" + quote(bucket, safe="") + "/" + quote(entry["key"], safe="/")
+ observed = fleet.record("scanner-task-status", observer.admin(path, [("clientToken", task["taskId"])], b""))
+ require(observed.get("httpStatus") == 200 and isinstance(observed.get("response"), dict),
+ "scanner task cannot be queried by its exact source identity")
+ status = observed["response"]
+ observations.append({"source": task, "status": status})
+ objects = status.get("outcome", {}).get("objects", [])
+ if len(objects) == 1 and objects[0].get("disposition", {}).get("state") == ("verified_healthy" if healthy else "repaired"):
+ fleet.record("scanner-source-binding", {"source": "scanner", "branch": branch, "healthyPreflight": healthy,
+ "budgetSeconds": BUDGET_SECONDS, "task": task, "targetReadsBeforePhysicalProof": 0,
+ "adminMutations": 0, "observedTasks": observations,
+ "postFaultCycles": cycles})
+ return status
+ raise ValueError("scanner tasks did not provide the required exact healthy/repaired responsibility; inspect source evidence")
diff --git a/scripts/validation_heal/schema.py b/scripts/validation_heal/schema.py
new file mode 100644
index 00000000..985299ac
--- /dev/null
+++ b/scripts/validation_heal/schema.py
@@ -0,0 +1,299 @@
+#!/usr/bin/env python3
+"""C09: refuse witnessed foreign identities and future format/object envelopes."""
+
+from __future__ import annotations
+
+import argparse
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import secrets
+import signal
+import sys
+import time
+
+try:
+ from .heal_probe import Probe, compare_inventories, require, save
+ from .heal_workload import Recorder, seed, verify_seed
+ from .healthy import Fleet, physical_content, validate_topology
+ from .inspector import verify as verify_inspector
+ from .matrix import select
+ from .shards import files, map_target, run_task
+ from .versions import metadata_target, validate_failed_version, validate_version_receipts
+ from .unformatted import binding, foreign_donor, negative_scene, refusal, OBSERVATION_SECONDS, SIZE
+ from .disk_loss import member_state
+ from .cursor import verify_part_manifest
+ from .heal_fault import bounded_fault_id
+except ImportError:
+ from heal_probe import Probe, compare_inventories, require, save
+ from heal_workload import Recorder, seed, verify_seed
+ from healthy import Fleet, physical_content, validate_topology
+ from inspector import verify as verify_inspector
+ from matrix import select
+ from shards import files, map_target, run_task
+ from versions import metadata_target, validate_failed_version, validate_version_receipts
+ from unformatted import binding, foreign_donor, negative_scene, refusal, OBSERVATION_SECONDS, SIZE
+ from disk_loss import member_state
+ from cursor import verify_part_manifest
+ from heal_fault import bounded_fault_id
+
+
+CASE_ID = "heal.foreign-format-and-unknown-schema"
+BRANCHES = ("foreign-disk", "foreign-set", "future-format", "future-object-schema")
+OBJECTS = 128
+CONTRACT = {"candidateCommit": "072b57b37559fbe626aeda8abf61617fdcbdd926",
+ "sourceFiles": {
+ "crates/ecstore/src/layout/format.rs": "a730e6c897de6fca6071a806bcb9ad27a13e292b6c3975dfda6f436187f4784d",
+ "crates/ecstore/src/store/init_format.rs": "b6f5d009eccaeb28f9700f721bbf9a005b289716bfec939309193e06e098263c",
+ "crates/filemeta/src/filemeta/codec.rs": "ef82b3390b0a377e423f99fb384c6a8a96beaf5722b0c907852cc0c904c4f04d"},
+ "futureFormat": {"field": "version", "original": "1", "injected": "999"},
+ "futureObjectSchema": {"envelope": "XL2 ", "majorOffset": 4, "byteOrder": "little", "originalMajor": 1,
+ "injectedMajor": 2, "parserRefusal": "xl file version err", "innerMsgpackSchemaChanged": False},
+ "formatFaultScope": "one witnessed empty owned replacement root; shared formats never mutated"}
+
+
+def exact_get(observed, expected):
+ require(observed.get("acknowledged") is True and observed.get("resultUnknown") is False
+ and observed.get("response", {}).get("ResponseMetadata", {}).get("HTTPStatusCode") == 200
+ and observed.get("payload") == expected["payload"], "C09 surviving/recovered GET differs from its independent ACK")
+ return {"fullPayloadEqualsACK": True}
+
+
+def unsupported_get(observed):
+ response = observed.get("response") or {}
+ require(observed.get("acknowledged") is False and observed.get("resultUnknown") is False
+ and "payload" not in observed and response.get("ResponseMetadata", {}).get("HTTPStatusCode") in {500, 503}
+ and response.get("Error", {}).get("Code") in {"InternalError", "SlowDownRead"},
+ "future-schema GET returned bytes, absence, a transport error or an unreviewed refusal")
+ return {"structuredServerRefusal": response["Error"]["Code"], "unknownTreatedAsMissing": False}
+
+
+def metadata_scene(before, after, targets, receipts):
+ """Every unknown record survives byte-for-byte; every control remains frozen."""
+ left, right = files(before), files(after)
+ require(len(targets) == len(receipts) in {4, 16} and set(left) == set(right), "C09 unknown record was deleted or duplicated")
+ changed = {}
+ for target, receipt in zip(targets, receipts, strict=True):
+ identity = target["node"], target["disk"], target["relativePath"]
+ require(identity not in changed and identity in left
+ and receipt.get("kind") == "future-xl-schema" and receipt.get("relativePath") == identity[2]
+ and receipt.get("before") == {name: left[identity][name] for name in ("sha256", "size")}
+ and isinstance(receipt.get("fault"), dict) and receipt["fault"]["size"] == left[identity]["size"]
+ and receipt["fault"]["sha256"] != left[identity]["sha256"], "future envelope receipt differs from the exact original")
+ require({name: right[identity][name] for name in ("sha256", "size")} == receipt["fault"]
+ and "metadata" not in right[identity]
+ and right[identity].get("metadataError", {}).get("exactWitnessedFault") is True
+ and right[identity].get("metadataError", {}).get("validMetadataClaimed") is False
+ and CONTRACT["futureObjectSchema"]["parserRefusal"] in right[identity].get("metadataError", {}).get("message", ""),
+ "unknown metadata was rewritten or did not actually reach the reviewed parser refusal")
+ changed[identity] = receipt["fault"]
+ require({key: value for key, value in left.items() if key not in changed}
+ == {key: value for key, value in right.items() if key not in changed}
+ and all(before[node]["formats"] == after[node]["formats"] for node in before),
+ "future-schema test changed a payload, control object or disk format")
+ return {"unknownRecordsRetainedExactly": len(changed), "allPayloadsControlsAndFormatsUnchanged": True,
+ "innerMsgpackSchemaChanged": False, "productRepairClaimed": False}
+
+
+def format_outcome(status, bucket, expected, target):
+ # A quarantined disk may leave the existing object verifiable on surviving
+ # authorities. Its exact disk refusal is proved independently, never inferred
+ # from a healthy object receipt or from a startup failure.
+ execution = (status.get("outcome") or {}).get("execution", {}).get("state")
+ if execution == "completed":
+ proof = validate_version_receipts(status, bucket, expected, 2)
+ else:
+ proof = validate_failed_version(status, bucket, expected, target)
+ require((status.get("outcome") or {}).get("counters", {}).get("healed") == 0,
+ "foreign/future format was reported reconstructed")
+ return {**proof, "refusedSlotValidatedSeparately": True, "productRepairClaimed": False}
+
+
+def run_branch(args, topology, nodes, branch, donor):
+ directory = args.evidence / f"{topology}-{branch}"
+ directory.mkdir()
+ run_id = f"gh-{os.environ['GITHUB_RUN_ID']}-{os.environ.get('GITHUB_RUN_ATTEMPT', '1')}-{topology.lower()}"
+ fleet = Fleet(nodes, directory, run_id, args.tool_directory, runtime_profile="schema-refusal")
+ candidate = json.loads(args.candidate.read_text())
+ fleet.record("inspector-build", verify_inspector(args.tool_directory, candidate))
+ require(candidate["source_sha"] == CONTRACT["candidateCommit"], "unreviewed C09 parser/refusal contract")
+ fleet.record("candidate-schema-contract", CONTRACT)
+ bucket = "rv-heal-" + hashlib.sha256(run_id.encode()).hexdigest()[:16]
+ access, secret = secrets.token_hex(16), secrets.token_hex(24)
+ os.environ.update(HEAL_ACCESS_KEY=access, HEAL_SECRET_KEY=secret)
+ lab = binding(args, candidate, nodes, run_id, bucket)
+ fleet.record("binding", lab)
+ probe, active, metadata_faults = Probe(lab), [], {}
+ replacement_node, replacement_dirty = None, False
+ result = {"topology": topology, "branch": branch, "verdict": "RUNNING", "cleanup": "PENDING"}
+ try:
+ fleet.deploy(args.binary_sha, bucket, access, secret)
+ expected = seed(Recorder(probe, directory / "fixture"), bucket, "DB", OBJECTS, [SIZE])
+ baseline = fleet.record("S3-baseline", probe.inventory(bucket, unversioned_current_tags=True))
+ fleet.record("fixture-ACK-manifest", {"expected": expected, **verify_seed(expected, baseline)})
+ fleet.stop()
+ before = fleet.snapshot("original-complete-physical", quiescent=True)
+ fleet.record("actual-original-topology", validate_topology(before, topology))
+ cohorts = [map_target(before, bucket, entry["key"], "data", 1) for entry in expected]
+ fleet.record("all-original-streaming-proof", verify_part_manifest(fleet, cohorts, "original-part", count=OBJECTS))
+ target = cohorts[8]
+ selected = [expected[8]]
+ require(target["key"] == selected[0]["key"], "C09 target lost its registered ACK order")
+ if branch == "future-object-schema":
+ targets = [metadata_target(before, {**target, **shard}) for shard in target["cohort"]]
+ receipts = []
+ for index, fault in enumerate(targets):
+ params = {"disk": fault["disk"], "faultId": bounded_fault_id("C09", branch, index)}
+ # Register before mutation so a lost SSH response still restores
+ # the exact intent and retains an unresolved namespace on failure.
+ fleet.faults.append((fault["node"], params))
+ receipt = fleet.record("future-envelope-injection", {"node": fault["node"], **fleet.remote(fault["node"],
+ "apply-fault", **params, relativePath=fault["relativePath"], kind="future-xl-schema", expectedSha256=fault["sha256"])})
+ receipts.append(receipt)
+ metadata_faults.setdefault(fault["node"], {})[f"{fault['disk']}:{fault['relativePath']}"] = receipt["fault"]
+ faulted = fleet.snapshot("actual-future-envelope-scene", quiescent=True, metadata_faults=metadata_faults)
+ fleet.record("future-envelope-witness", metadata_scene(before, faulted, targets, receipts))
+ fleet.start()
+ for endpoint in lab["endpoints"]:
+ observed = fleet.record("unknown-target-GET", Probe(lab, endpoint).s3("get_object", {"Bucket": bucket, "Key": target["key"]}))
+ fleet.record("unknown-target-refusal", unsupported_get(observed))
+ control = fleet.record("unaffected-control-GET", Probe(lab, endpoint).s3("get_object", {"Bucket": bucket, "Key": expected[0]["key"]}))
+ fleet.record("unaffected-control-ACK", exact_get(control, expected[0]))
+ status = run_task(probe, fleet, target, 2, active)
+ fleet.record("explicit-unknown-schema-responsibility", validate_failed_version(status, bucket, selected, target))
+ fleet.stop()
+ scene = fleet.snapshot("refused-unknown-records-preserved", quiescent=True, metadata_faults=metadata_faults)
+ fleet.record("negative-full-physical-proof", metadata_scene(before, scene, targets, receipts))
+ require(physical_content(faulted) == physical_content(scene), "GET/deep Heal changed an unknown record or control")
+ for node, params in tuple(fleet.faults):
+ fleet.record("negative-record-rescue", {"node": node, **fleet.remote(node, "recover-fault", **params)})
+ fleet.faults.remove((node, params))
+ metadata_faults.clear()
+ else:
+ member = next(shard for shard in target["cohort"] if shard["node"] == nodes[-1])
+ target = {**target, **member}
+ node, disk = member["node"], member["disk"]
+ original_format = next(item for item in before[node]["formats"] if item["disk"] == disk)
+ fleet.start()
+ original = member_state(fleet.record("original-member-Admin", probe.admin("/rustfs/admin/v3/info", [], b"", method="GET")),
+ target, len(target["cohort"]))
+ fleet.stop()
+ replacement_node = node
+ fleet.record("one-owned-original-root-quarantined", fleet.remote(node, "replace-drive", disk=disk,
+ expectedFormatSha256=original_format["sha256"]))
+ if branch != "future-format": fleet.record("released-foreign-donor", donor)
+ injected = fleet.record("exact-schema-format-injection", fleet.remote(node, "format-schema-fault", disk=disk,
+ branch=branch, donor=donor if branch != "future-format" else None))
+ foreign = {"sha256": injected["injectedSha256"], "value": injected["injectedValue"]}
+ fleet.record("registered-format-refusal-window", {"seconds": OBSERVATION_SECONDS,
+ "automaticHealDisabled": True, "directoryTestRootFallback": True, "actualDeviceReplacementClaimed": False,
+ "startupFailureAloneEarnsPass": False})
+ fleet.start()
+ fleet.record("original-root-inaccessible-to-candidate", fleet.remote(node, "replacement-probe"))
+ deadline, samples = time.monotonic() + OBSERVATION_SECONDS, []
+ while True:
+ state = fleet.record("injected-format-window", fleet.remote(node, "replacement-state"))
+ current = next(item for item in state["formats"] if item["disk"] == disk)
+ require(state.get("originalArchiveUnchanged") is True and current.get("sha256") == foreign["sha256"]
+ and current.get("value") == foreign["value"], "candidate altered or reidentified foreign/future format")
+ samples.append(refusal(fleet.record("structured-format-refusal", probe.admin("/rustfs/admin/v3/info", [], b"", method="GET")),
+ target, original["endpoint"], foreign))
+ if time.monotonic() >= deadline: break
+ time.sleep(5)
+ require(len(samples) >= 3, "format refusal did not span its registered window")
+ status = run_task(probe, fleet, target, 2, active)
+ fleet.record("deep-Heal-format-responsibility", format_outcome(status, bucket, selected, target))
+ for endpoint in lab["endpoints"]:
+ observed = fleet.record("surviving-authority-target-GET", Probe(lab, endpoint).s3("get_object", {"Bucket": bucket, "Key": target["key"]}))
+ fleet.record("surviving-authority-ACK-proof", exact_get(observed, selected[0]))
+ # GET and deep Heal must both preserve the injected identity and all
+ # survivors. Only then can the original root be used for rescue.
+ fleet.stop()
+ scene = fleet.snapshot("refused-format-complete-physical", quiescent=True)
+ fleet.record("format-negative-full-physical-proof", negative_scene(before, scene, (node, disk), "foreign-format", foreign))
+ restored = fleet.record("original-identity-negative-rescue", fleet.remote(node, "restore-replacement"))
+ require(restored.get("originalRestoredExactly") is True and restored.get("productRepairClaimed") is False,
+ "C09 rescue differs from the original or claimed product repair")
+ replacement_node = None
+ result["refusalSamples"] = len(samples)
+ recovered = fleet.snapshot("exact-original-identity-recovered", quiescent=True)
+ require(physical_content(before) == physical_content(recovered), "C09 rescue changed any original record or payload")
+ fleet.record("all-recovered-streaming-proof", verify_part_manifest(fleet, cohorts, "recovered-part", count=OBJECTS))
+ fleet.start()
+ for endpoint in lab["endpoints"]:
+ inventory = fleet.record("all-peer-complete-S3", {"endpoint": endpoint,
+ **Probe(lab, endpoint).inventory(bucket, unversioned_current_tags=True)})
+ fleet.record("all-peer-independent-ACK-proof", {**verify_seed(expected, inventory), **compare_inventories(baseline, inventory)})
+ fleet.stop()
+ require(physical_content(before) == physical_content(fleet.snapshot("after-recovery-S3-physical", quiescent=True)),
+ "recovery verification repaired records that the negative proof had not preserved")
+ result.update(verdict="PASS", all128ACKsPreserved=True, fullPhysicalAndStreamingAndS3Verified=True,
+ backupRestorationUsed=True, productRepairClaimed=False)
+ except (Exception, KeyboardInterrupt) as exc:
+ result.update(verdict="FAIL", failure={"type": type(exc).__name__, "message": str(exc)})
+ if fleet.reserved:
+ try:
+ fleet.stop()
+ fleet.snapshot("C09-failure-physical", quiescent=True, metadata_faults=metadata_faults)
+ if replacement_node:
+ fleet.record("C09-failure-original-archive", fleet.remote(replacement_node, "replacement-state"))
+ except Exception as evidence_error:
+ result["failureEvidenceError"] = str(evidence_error)
+ finally:
+ for path, token in active:
+ try: fleet.record("stop-unresolved", probe.admin(path, [("clientToken", token), ("forceStop", "true")], b""))
+ except Exception as exc: fleet.record("stop-error", {"error": str(exc)})
+ if replacement_node:
+ try:
+ fleet.stop()
+ fleet.record("failed-format-original-rescue", fleet.remote(replacement_node, "restore-replacement"))
+ except Exception as exc:
+ replacement_dirty = True
+ fleet.record("failed-format-recovery-DIRTY", {"error": str(exc), "namespacesRetained": True})
+ result["cleanup"] = "PASS" if not replacement_dirty and fleet.cleanup() else "FAIL"
+ fleet.record("branch-result", result)
+ return result
+
+
+def main():
+ def interrupted(signum, frame): raise KeyboardInterrupt(f"Heal workflow interrupted by signal {signum}")
+ signal.signal(signal.SIGTERM, interrupted)
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--candidate", type=Path, required=True)
+ parser.add_argument("--case-id", choices=(CASE_ID,), required=True)
+ parser.add_argument("--testing-commit", required=True)
+ parser.add_argument("--binary-sha", required=True)
+ parser.add_argument("--rc-bin", type=Path, required=True)
+ parser.add_argument("--evidence", type=Path, required=True)
+ parser.add_argument("--tool-directory", type=Path, required=True)
+ args = parser.parse_args()
+ nodes = os.environ.get("RUSTFS_NODES", "vm000 vm001 vm002 vm003").split()
+ require(len(nodes) >= 4 and len(nodes) == len(set(nodes))
+ and all(re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", node) for node in nodes), "four current Heal nodes required")
+ require(bool(re.fullmatch(r"[0-9a-f]{64}", args.binary_sha))
+ and bool(re.fullmatch(r"[1-9][0-9]*", os.environ.get("GITHUB_RUN_ID", ""))), "verified workflow candidate required")
+ require(select(CASE_ID)[0]["variants"] == list(BRANCHES), "C09 executor omitted a canonical source branch")
+ results, donor_failure = [], None
+ try:
+ donor = foreign_donor(args, nodes[0])
+ for topology, selected in (("H1", nodes[:1]), ("H2", nodes[:4])):
+ for branch in BRANCHES:
+ results.append(run_branch(args, topology, selected, branch, donor))
+ if results[-1]["cleanup"] != "PASS": break
+ if results[-1]["cleanup"] != "PASS": break
+ except (Exception, KeyboardInterrupt) as exc:
+ donor_failure = {"type": type(exc).__name__, "message": str(exc)}
+ finally:
+ complete = len(results) == 8 and all(item["verdict"] == "PASS" and item["cleanup"] == "PASS" for item in results)
+ save(args.evidence / "healthy-summary.json", {"caseId": CASE_ID, "results": results, "donorFailure": donor_failure,
+ "cleanup": "PASS" if results and all(item["cleanup"] == "PASS" for item in results) else "FAIL", "complete": complete})
+ require(complete, "C09 requires all four branches on both H1/H2, a released donor and complete recovery")
+
+
+if __name__ == "__main__":
+ try: main()
+ except Exception as exc:
+ print(f"Heal foreign/future schema validation failed: {exc}", file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_heal/schema_faults.py b/scripts/validation_heal/schema_faults.py
new file mode 100644
index 00000000..f6b64440
--- /dev/null
+++ b/scripts/validation_heal/schema_faults.py
@@ -0,0 +1,90 @@
+"""C09 format faults in one witnessed empty replacement root, never shared format."""
+
+import base64
+import copy
+import hashlib
+import json
+import os
+import uuid
+
+
+def require(value, message):
+ if not value:
+ raise ValueError(message)
+
+
+def reviewed_format(value):
+ require(isinstance(value, dict) and value.get("version") == "1" and value.get("format") == "xl"
+ and value.get("xl", {}).get("version") == "3", "format is outside the reviewed V1/XL V3 contract")
+ require(str(uuid.UUID(value["id"])) == value["id"] and uuid.UUID(value["id"]).int != 0, "invalid deployment UUID")
+ erasure = value["xl"]
+ groups = erasure["sets"]
+ require(isinstance(groups, list) and len(groups) == 1 and len(groups[0]) in {4, 16}
+ and erasure.get("distributionAlgo") == "SIPMOD+PARITY", "unreviewed donor/set geometry")
+ members = groups[0]
+ require(len(members) == len(set(members)) and erasure["this"] in members
+ and all(str(uuid.UUID(member)) == member and uuid.UUID(member).int != 0 for member in members),
+ "format lacks distinct canonical member identities")
+ return set(members)
+
+
+def derive(original_raw, branch, donor=None):
+ require(branch in {"foreign-disk", "foreign-set", "future-format"}, "unsupported C09 format branch")
+ original = json.loads(original_raw)
+ original_members = reviewed_format(original)
+ changed = copy.deepcopy(original)
+ if branch == "future-format":
+ changed["version"] = "999"
+ raw = json.dumps(changed, sort_keys=True, separators=(",", ":")).encode()
+ detail = {"futureField": "version", "before": "1", "after": "999", "otherSemanticFieldsUnchanged": True}
+ else:
+ require(isinstance(donor, dict) and donor.get("readOnly") is True, "released-process donor evidence required")
+ donor_raw = base64.b64decode(donor["rawBase64"], validate=True)
+ require(len(donor_raw) < 65536, "donor format exceeds its exact byte budget")
+ require(hashlib.sha256(donor_raw).hexdigest() == donor["sha256"], "donor raw format digest differs")
+ foreign = json.loads(donor_raw)
+ foreign_members = reviewed_format(foreign)
+ require(donor.get("value") == foreign and foreign["id"] != original["id"]
+ and not foreign_members & original_members, "donor reused the original deployment or member UUIDs")
+ if branch == "foreign-disk":
+ changed["xl"]["this"] = foreign["xl"]["this"]
+ raw = json.dumps(changed, sort_keys=True, separators=(",", ":")).encode()
+ detail = {"changedField": "xl.this", "actualForeignDiskUUID": foreign["xl"]["this"],
+ "originalDeploymentAndSetPreserved": True, "derivedIdentityFixture": True}
+ else:
+ # Preserve the real foreign cluster/set bytes without fabricating
+ # set membership, disk UUIDs, deployment IDs or format versions.
+ changed, raw = foreign, donor_raw
+ detail = {"actualForeignDeploymentId": foreign["id"], "actualForeignSet": foreign["xl"]["sets"],
+ "releasedDonorBytesCopiedUnchanged": True, "derivedIdentityFixture": False}
+ return raw, changed, detail
+
+
+def install(root_fd, replacement, owner, request, fault):
+ require(request.get("disk") == replacement["disk"] and replacement.get("originalArchiveUnchanged") is True
+ and replacement.get("originalStillAtDataRoot") is False, "C09 requires its exact isolated original member")
+ branch = request["branch"]
+ donor = request.get("donor")
+ if donor is not None:
+ require(donor.get("binarySha256") == owner["binarySha256"], "donor candidate binary differs")
+ with fault.parent(root_fd, ["quarantine", "old-disk", ".rustfs.sys"]) as saved:
+ original = fault.read_file(saved, "format.json", 65536)
+ require(fault.digest(original) == replacement["originalFormatSha256"], "original format archive differs")
+ raw, changed, detail = derive(original, branch, donor)
+ require(len(raw) < 65536, "derived format exceeds its byte budget")
+ receipt = {"schemaVersion": 1, "runId": owner["runId"], "disk": replacement["disk"], "branch": branch,
+ "relativePath": ".rustfs.sys/format.json", "originalSha256": fault.digest(original),
+ "injectedSha256": fault.digest(raw), "injectedValue": changed, "detail": detail,
+ "backend": "one-exact-empty-owned-replacement-format", "credentialsRead": False,
+ "sharedFormatTouched": False, "backupRestorationIsProductRepair": False}
+ with fault.parent(root_fd, ["quarantine"]) as archive, fault.parent(root_fd, ["data"]) as data:
+ require(not os.listdir(data), "format mutation requires the witnessed empty replacement root")
+ fault.new_file(archive, "C09-format-intent.json", fault.encode(receipt))
+ fault.new_file(archive, "C09-injected-format.original", raw)
+ os.mkdir(".rustfs.sys", 0o700, dir_fd=data)
+ with fault.parent(root_fd, ["data", ".rustfs.sys"]) as target:
+ fault.new_file(target, "format.json", raw)
+ require(fault.read_file(target, "format.json", 65536) == raw, "format injection was not witnessed")
+ os.fsync(data)
+ fault.new_file(archive, "C09-format-applied.json", fault.encode(receipt))
+ return receipt
diff --git a/scripts/validation_heal/scopes.py b/scripts/validation_heal/scopes.py
new file mode 100644
index 00000000..13d16dc3
--- /dev/null
+++ b/scripts/validation_heal/scopes.py
@@ -0,0 +1,349 @@
+#!/usr/bin/env python3
+"""A02: exact root/bucket/prefix/object and genuine erasure-set isolation."""
+
+from __future__ import annotations
+
+import argparse
+import copy
+from datetime import datetime, timedelta, timezone
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import secrets
+import signal
+import sys
+import time
+from urllib.parse import quote
+
+try:
+ from .heal_probe import Probe, compare_inventories, payload, require, save
+ from .heal_workload import Recorder, verify_seed
+ from .healthy import BASE_OPTIONS, Fleet, physical_content, validate_topology
+ from .inspector import verify as verify_inspector
+ from .matrix import select
+ from .heal_fault import bounded_fault_id
+ from .shards import files, map_target, validate_fault, validate_repaired_physical
+ from .versions import public_version_id
+ from .stop import idle_fleet
+except ImportError:
+ from heal_probe import Probe, compare_inventories, payload, require, save
+ from heal_workload import Recorder, verify_seed
+ from healthy import BASE_OPTIONS, Fleet, physical_content, validate_topology
+ from inspector import verify as verify_inspector
+ from matrix import select
+ from heal_fault import bounded_fault_id
+ from shards import files, map_target, validate_fault, validate_repaired_physical
+ from versions import public_version_id
+ from stop import idle_fleet
+
+
+CASE_ID = "heal.admin-scope-isolation"
+BRANCHES = ("root", "bucket", "prefix", "object", "pool-set")
+SIZE = 1024**2 + 73
+KEYS = ("a/target.bin", "a/target.bin-suffix", "a/child/neighbor.bin", "a/中文 空%2F.bin",
+ "a/literal/slash.bin", "a/literal%2Fslash.bin", "a/06.bin", "a/07.bin",
+ "ab/control.bin", "ab/09.bin", "ab/10.bin", "ab/11.bin",
+ "b/control.bin", "b/13.bin", "b/14.bin", "b/15.bin")
+REFERENCE = {"candidateCommit": "072b57b37559fbe626aeda8abf61617fdcbdd926",
+ "sourceFiles": {"crates/heal/src/heal/channel.rs": "7f669ca43b9559db9b97fba6b419a6c76a94d76acd8526db132d74b7c534d6b0",
+ "crates/heal/src/heal/task.rs": "81e8e2ec8807d1094095e444d0aaf8913f94114836bc1bc9f7953cbeff98d855",
+ "crates/ecstore/src/core/sets.rs": "06543b1d083f489346171ba489b1eb1e0b7899c844f0ebf15e16ad467cf5e2f1",
+ "crates/ecstore/src/layout/disks_layout.rs": "b143aa9fa97b2cd32f8a2cd548779e0656cab0f197988483ee8a92b5ab3f17f8"},
+ "nullableIdentityScope": "unrestricted admitted selectors; exact ACK and physical set binding still required",
+ "h2": "four original nodes, two 8-drive sets, two original mounts per node per set, disk-major endpoints",
+ "h1": "original single-node four-drive set; no invented unselected erasure set"}
+
+
+def seed_fixture(recorder, buckets, branch):
+ expected = {}
+ for bucket in buckets:
+ recorder.call("create_bucket", {"Bucket": bucket})
+ expected[bucket] = []
+ for key in KEYS:
+ body = payload(f"A02/{branch}/{bucket}/{key}", SIZE)
+ response = recorder.call("put_object", {"Bucket": bucket, "Key": key,
+ "Metadata": {"suite": "A02", "branch": branch}, "Tagging": "suite=heal&fixture=DS"}, body)
+ require(response.get("VersionId") in {None, "null"}, "scope fixture must remain never-versioned")
+ expected[bucket].append({"key": key, "versionId": "null", "deleteMarker": False, "isLatest": True,
+ "payload": {"size": len(body), "sha256": hashlib.sha256(body).hexdigest()}})
+ return expected
+
+
+def object_mapping(snapshot, expected, topology):
+ validate_topology(snapshot, topology, expected_sets=2 if topology == "H2" else 1)
+ indexed = files(snapshot)
+ mapped = {}
+ for bucket, entries in expected.items():
+ for index, entry in enumerate(entries):
+ identity = bucket, entry["key"]
+ scopes = set()
+ for node, observation in snapshot.items():
+ for record in observation["formats"]:
+ if (node, record["disk"], f"{bucket}/{entry['key']}/xl.meta") not in indexed: continue
+ layout = record["value"]["xl"]
+ members = [number for number, group in enumerate(layout["sets"]) if layout["this"] in group]
+ require(len(members) == 1, "object member belongs to ambiguous erasure sets")
+ scopes.add(members[0])
+ require(len(scopes) == 1, "object metadata lacks one exact physical set authority")
+ set_index = next(iter(scopes))
+ mapped[identity] = map_target(snapshot, bucket, entry["key"], "data" if index % 2 == 0 else "parity", 1,
+ multi_set=topology == "H2", set_index=set_index)
+ require(len(mapped) == sum(len(entries) for entries in expected.values()), "physical scope manifest omitted an ACK")
+ if topology == "H2": require({target["setIndex"] for target in mapped.values()} == {0, 1}, "fixture did not populate both actual sets")
+ return mapped
+
+
+def selection(branch, primary, mapped):
+ require(branch in BRANCHES and mapped, "invalid scope case or manifest")
+ selected = {pair for pair, target in mapped.items() if branch == "root"
+ or branch == "bucket" and pair[0] == primary
+ or branch == "prefix" and pair[0] == primary and pair[1].startswith("a/")
+ or branch == "object" and pair == (primary, "a/target.bin")
+ or branch == "pool-set" and target["poolIndex"] == 0 and target["setIndex"] == 0}
+ require(selected, "selector has no acknowledged object scope")
+ return selected
+
+
+def request_spec(branch, primary):
+ options = {**BASE_OPTIONS, "recursive": branch != "object", "pool": None, "set": None}
+ path = "/rustfs/admin/v3/heal/"
+ if branch not in {"root", "pool-set"}:
+ path += quote(primary, safe="")
+ if branch == "prefix": path += "/" + quote("a/", safe="/")
+ elif branch == "object": path += "/" + quote("a/target.bin", safe="/")
+ if branch == "pool-set": options.update(pool=0, set=0)
+ require(branch in BRANCHES, "unknown selector branch")
+ return path, options
+
+
+def fault_targets(primary, secondary, mapped, topology):
+ pairs = [(primary, key) for key in ("a/target.bin", "a/child/neighbor.bin", "ab/control.bin", "b/control.bin")]
+ pairs.append((secondary, "a/target.bin"))
+ if topology == "H2":
+ for number in (0, 1):
+ if not any(mapped[pair]["setIndex"] == number for pair in pairs):
+ pair = next(pair for pair, target in mapped.items() if target["setIndex"] == number)
+ pairs.append(pair)
+ require(len(pairs) == len(set(pairs)), "scope fault list reused an object")
+ return [mapped[pair] for pair in pairs]
+
+
+def responsibility(current, mapped, selected, repaired, options):
+ outcome = current.get("outcome") or {}
+ require(current.get("summary") == "finished" and current.get("truncated", False) is False
+ and outcome.get("execution", {}).get("state") == "completed" and outcome.get("coverage") == "complete"
+ and outcome.get("objectsTruncated") is False, "scope task lacks complete traversal and diagnostics")
+ require(repaired <= selected <= set(mapped) and repaired, "scope repair plan lacks selected faults or escaped ACKs")
+ objects = outcome.get("objects")
+ require(isinstance(objects, list) and len(objects) == len(selected), "scope receipts omitted or added an object")
+ seen, incarnations, resolved = set(), {}, []
+ for receipt in objects:
+ item = receipt.get("identity", {})
+ pair = item.get("bucket"), item.get("object")
+ require(pair in selected and pair not in seen and item.get("kind") == "object"
+ and public_version_id(item.get("versionId")) == "null", "scope receipt has a wrong/duplicate ACK identity")
+ target = mapped[pair]
+ scope = item.get("poolIndex"), item.get("setIndex")
+ requested = options["pool"], options["set"]
+ if requested != (None, None):
+ require(all(type(value) is int for value in scope) and scope == requested == (target["poolIndex"], target["setIndex"]),
+ "selected pool/set receipt lost or changed explicit scope")
+ elif scope != (None, None):
+ require(all(type(value) is int for value in scope) and scope == (target["poolIndex"], target["setIndex"]),
+ "unrestricted receipt contradicts independently observed physical set")
+ incarnation = item.get("bucketIncarnationId")
+ require(isinstance(incarnation, str) and incarnation and incarnations.setdefault(pair[0], incarnation) == incarnation,
+ "scope receipts lack or mix bucket incarnations")
+ state = "repaired" if pair in repaired else "verified_healthy"
+ require(receipt.get("disposition", {}).get("state") == state, "scope receipt claimed unknown, false repair or incomplete integrity")
+ seen.add(pair)
+ resolved.append({"bucket": pair[0], "key": pair[1], "rawSelectorScope": list(scope),
+ "physicalPoolIndex": target["poolIndex"], "physicalSetIndex": target["setIndex"],
+ "originalSetDiskUUIDs": [member["diskUUID"] for member in target["cohort"]]})
+ counts = {"processed": len(selected), "healed": len(repaired), "unchanged": len(selected) - len(repaired),
+ "failed": 0, "skipped": 0, "unknown": 0, "attemptFailures": 0, "overflowed": False}
+ require(all(type(outcome.get("counters", {}).get(name)) is type(value) and outcome["counters"][name] == value
+ for name, value in counts.items()), "scope counters differ from exact physical/ACK selection")
+ return {"exactSelectedResponsibility": True, "rawIdentitiesNotRewritten": True, "physicalScopeBindings": resolved}
+
+
+def physical_selection(original, repaired, targets, selected):
+ repairs = [target for target in targets if (target["bucket"], target["key"]) in selected]
+ controls = [target for target in targets if (target["bucket"], target["key"]) not in selected]
+ expected = copy.deepcopy(original)
+ missing = {(target["node"], target["disk"], target["relativePath"]) for target in controls}
+ actual = files(repaired)
+ for identity in missing: require(identity not in actual, "unselected control was repaired before the physical proof")
+ for node, value in expected.items():
+ value["files"] = [entry for entry in value["files"] if (node, entry["disk"], entry["path"]) not in missing]
+ require(repairs, "physical isolation has no selected repair target")
+ proof = validate_repaired_physical(expected, repaired, repairs[0], repair_targets=repairs)
+ return {**proof, "selectedReconstructedParts": len(repairs), "unselectedStillMissingParts": len(controls),
+ "beforeAnyTargetGETOrBackupRescue": True}
+
+
+def verify_parts(fleet, mapped, label):
+ members = [member for target in mapped.values() for member in target["cohort"]]
+ identities = {(member["node"], member["disk"], member["relativePath"]) for member in members}
+ require(len(identities) == len(members) and members, "part manifest duplicated or omitted a physical identity")
+ for node in fleet.nodes:
+ local = [member for member in members if member["node"] == node]
+ for offset in range(0, len(local), 128):
+ batch = local[offset:offset + 128]
+ raw = fleet.record(label, {"node": node, **fleet.remote(node, "verify-parts", parts=[{
+ name: member[name] for name in ("disk", "relativePath", "versionId", "partNumber")} for member in batch])})
+ require(raw.get("complete") is True and len(raw.get("parts", [])) == len(batch), "scope streaming verifier omitted a shard")
+ for member, proof in zip(batch, raw["parts"], strict=True):
+ require(proof.get("physicalBitrotVerified") is True and proof.get("physicalSize") == member["size"]
+ and all(proof.get(name) == member[name] for name in ("disk", "relativePath", "versionId", "partNumber", "sha256")),
+ "scope part failed exact identity/hash/streaming verification")
+ return {"exactStreamingParts": len(members), "exactObjects": len(mapped)}
+
+
+def run_branch(args, topology, nodes, branch):
+ directory = args.evidence / topology / branch
+ directory.mkdir(parents=True)
+ run_id = f"gh-{os.environ['GITHUB_RUN_ID']}-{os.environ.get('GITHUB_RUN_ATTEMPT', '1')}-{topology.lower()}"
+ fleet = Fleet(nodes, directory, run_id, args.tool_directory, runtime_profile="scope-isolation" if topology == "H2" else "manual")
+ bucket = "rv-heal-" + hashlib.sha256(run_id.encode()).hexdigest()[:16]
+ buckets = [bucket, bucket + "-other"]
+ fleet.snapshot_buckets = buckets
+ candidate = json.loads(args.candidate.read_text())
+ require(candidate["source_sha"] == REFERENCE["candidateCommit"], "scope/nullable selector contract is not reviewed for this candidate")
+ access, secret = secrets.token_hex(16), secrets.token_hex(24)
+ os.environ.update(HEAL_ACCESS_KEY=access, HEAL_SECRET_KEY=secret)
+ binding = {"schemaVersion": 1, "runId": run_id, "jumpHostOnly": True, "exclusiveHealService": True,
+ "leaseExpiresAt": (datetime.now(timezone.utc) + timedelta(minutes=30)).isoformat(), "bucketPrefix": bucket,
+ "endpoints": {f"node{index}": f"http://rustfs-node{index}:9000" for index in range(1, len(nodes) + 1)},
+ "defaultEndpoint": "node1", "candidateCommit": candidate["source_sha"], "harnessCommit": args.testing_commit,
+ "candidatePackageSha256": candidate["package_sha256"], "candidateBinarySha256": args.binary_sha,
+ "rcBinarySha256": hashlib.sha256(args.rc_bin.read_bytes()).hexdigest(), "maxInventoryObjects": 128,
+ "maxInventoryBytes": 256 * 1024**2}
+ fleet.record("source-bound-scope-contract", REFERENCE)
+ fleet.record("inspector-build", verify_inspector(args.tool_directory, candidate))
+ fleet.record("binding", binding)
+ probe, active = Probe(binding), []
+ result = {"topology": topology, "branch": branch, "verdict": "RUNNING", "cleanup": "PENDING"}
+ try:
+ fleet.deploy(args.binary_sha, bucket, access, secret)
+ expected = seed_fixture(Recorder(probe, directory / "fixture"), buckets, branch)
+ baselines = {name: fleet.record("s3-baseline", probe.inventory(name, unversioned_current_tags=True)) for name in buckets}
+ for name in buckets: fleet.record("ACK-manifest", {"bucket": name, "expected": expected[name], **verify_seed(expected[name], baselines[name])})
+ fleet.stop()
+ original = fleet.snapshot("physical-baseline", quiescent=True)
+ mapped = object_mapping(original, expected, topology)
+ fleet.record("complete-physical-object-mapping", [{"bucket": pair[0], "key": pair[1], "pool": target["poolIndex"],
+ "set": target["setIndex"], "members": [{key: member[key] for key in ("node", "disk", "diskUUID", "setSlot", "index")}
+ for member in target["cohort"]]} for pair, target in mapped.items()])
+ verify_parts(fleet, mapped, "streaming-baseline")
+ selected = selection(branch, bucket, mapped)
+ targets = fault_targets(*buckets, mapped, topology)
+ selected_faults = {(target["bucket"], target["key"]) for target in targets} & selected
+ path, options = request_spec(branch, bucket)
+ fleet.record("exact-selector-plan", {"branch": branch, "path": path, "options": options,
+ "selectedACKs": [list(pair) for pair in sorted(selected)], "selectedFaults": [list(pair) for pair in sorted(selected_faults)],
+ "unselectedSetControl": "actual set 1" if topology == "H2" else "NOT_APPLICABLE: source H1 has one original four-drive set"})
+ prior, registrations = original, []
+ for index, target in enumerate(targets):
+ params = {"disk": target["disk"], "faultId": bounded_fault_id("A02", branch, index)}
+ fleet.faults.append((target["node"], params))
+ registrations.append((target["node"], params, target))
+ raw = fleet.record("fault", fleet.remote(target["node"], "apply-fault", **params,
+ relativePath=target["relativePath"], expectedSha256=target["sha256"], kind="missing"))
+ scene = fleet.snapshot("physical-after-fault", quiescent=True)
+ validate_fault(prior, scene, target, raw)
+ prior = scene
+ fleet.start()
+ require(physical_content(prior) == physical_content(fleet.snapshot("physical-before-trigger")),
+ "ATTRIBUTION_NOT_OBSERVED: selector fault changed before admission")
+ start = fleet.record("selector-admission", probe.admin(path, [], json.dumps(options).encode()))
+ require(start.get("httpStatus") == 200 and isinstance(start.get("response"), dict), "selector admission failed")
+ token = start["response"].get("clientToken")
+ require(isinstance(token, str) and token, "selector admission omitted original token")
+ active.append((path, token))
+ deadline = time.monotonic() + 180
+ while True:
+ raw = fleet.record("selector-status", probe.admin(path, [("clientToken", token)], b""))
+ require(raw.get("httpStatus") == 200 and isinstance(raw.get("response"), dict), "original selector task status failed")
+ current = raw["response"]
+ if current.get("summary") != "running": break
+ require(time.monotonic() < deadline, "scope task exceeded registered deadline")
+ time.sleep(.2)
+ active.remove((path, token))
+ outcome_proof = responsibility(current, mapped, selected, selected_faults, options)
+ idle_fleet(fleet, binding)
+ fleet.stop()
+ repaired = fleet.snapshot("physical-after-selector-before-GET", quiescent=True)
+ physical_proof = physical_selection(original, repaired, targets, selected)
+ streaming_proof = verify_parts(fleet, {pair: mapped[pair] for pair in selected}, "selected-streaming-proof")
+ # The unselected control backups are recovered only after preserving
+ # the complete scope isolation proof, and never count as task repair.
+ for node, params, target in registrations:
+ if (target["bucket"], target["key"]) not in selected:
+ raw = fleet.record("unselected-control-backup-rescue", fleet.remote(node, "recover-fault", **params))
+ require(raw.get("physicalRepairPassAllowed") is False, "control restoration claimed product repair")
+ fleet.faults.remove((node, params))
+ recovered = fleet.snapshot("physical-after-control-rescue", quiescent=True)
+ validate_repaired_physical(original, recovered, targets[0], repair_targets=targets)
+ verify_parts(fleet, mapped, "complete-streaming-after")
+ fleet.start()
+ for endpoint in binding["endpoints"]:
+ for name in buckets:
+ actual = fleet.record("all-peer-S3-after", {"endpoint": endpoint, **Probe(binding, endpoint).inventory(name, unversioned_current_tags=True)})
+ fleet.record("full-ACK-S3-proof", {"endpoint": endpoint, **verify_seed(expected[name], actual), **compare_inventories(baselines[name], actual)})
+ fleet.stop()
+ require(physical_content(recovered) == physical_content(fleet.snapshot("physical-after-GET", quiescent=True)), "S3 verification changed the proven selector result")
+ result.update(verdict="PASS", taskId=token, responsibility=outcome_proof, physical=physical_proof, streaming=streaming_proof,
+ controlBackupRescueExcludedFromRepair=True)
+ except (Exception, KeyboardInterrupt) as exc:
+ result.update(verdict="FAIL", failure={"type": type(exc).__name__, "message": str(exc)})
+ try:
+ fleet.stop()
+ fleet.snapshot("physical-failure", quiescent=True)
+ except Exception as error: result["failureEvidenceError"] = str(error)
+ finally:
+ for path, token in active:
+ try: fleet.record("stop-unresolved", probe.admin(path, [("clientToken", token), ("forceStop", "true")], b""))
+ except Exception as exc: fleet.record("stop-error", {"error": str(exc)})
+ result["cleanup"] = "PASS" if fleet.cleanup() else "FAIL"
+ fleet.record("branch-result", result)
+ return result
+
+
+def main():
+ def interrupted(signum, frame): raise KeyboardInterrupt(f"Heal scope interrupted by signal {signum}")
+ signal.signal(signal.SIGTERM, interrupted)
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--candidate", type=Path, required=True)
+ parser.add_argument("--case-id", choices=(CASE_ID,), required=True)
+ parser.add_argument("--testing-commit", required=True)
+ parser.add_argument("--binary-sha", required=True)
+ parser.add_argument("--rc-bin", type=Path, required=True)
+ parser.add_argument("--evidence", type=Path, required=True)
+ parser.add_argument("--tool-directory", type=Path, required=True)
+ args = parser.parse_args()
+ nodes = os.environ.get("RUSTFS_NODES", "vm000 vm001 vm002 vm003").split()
+ require(len(nodes) >= 4 and len(nodes) == len(set(nodes)) and all(re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", node) for node in nodes), "four current Heal nodes required")
+ require(re.fullmatch(r"[1-9][0-9]*", os.environ.get("GITHUB_RUN_ID", "")) and re.fullmatch(r"[0-9a-f]{64}", args.binary_sha), "immutable workflow/candidate identities required")
+ require(select(CASE_ID)[0]["variants"] == list(BRANCHES), "scope executor omitted a source selector")
+ results = []
+ try:
+ for topology, selected_nodes in (("H1", nodes[:1]), ("H2", nodes[:4])):
+ for branch in BRANCHES:
+ results.append(run_branch(args, topology, selected_nodes, branch))
+ if results[-1]["cleanup"] != "PASS": break
+ if results[-1]["cleanup"] != "PASS": break
+ finally:
+ complete = len(results) == 10 and all(row["verdict"] == "PASS" and row["cleanup"] == "PASS" for row in results)
+ save(args.evidence / "healthy-summary.json", {"caseId": CASE_ID, "results": results,
+ "cleanup": "PASS" if results and all(row["cleanup"] == "PASS" for row in results) else "FAIL", "complete": complete})
+ require(complete, "A02 requires every real selector on H1/H2, exact physical scope isolation and cleanup")
+
+
+if __name__ == "__main__":
+ try: main()
+ except Exception as exc:
+ print(f"Heal scope validation failed: {exc}", file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_heal/shard_boundaries.py b/scripts/validation_heal/shard_boundaries.py
new file mode 100644
index 00000000..b809c72b
--- /dev/null
+++ b/scripts/validation_heal/shard_boundaries.py
@@ -0,0 +1,84 @@
+"""Supplement C02/C03 DB controls with actual default-class size boundaries."""
+
+try:
+ from .boundaries import boundary_sizes, seed_boundaries, verify_boundary_ranges, verify_size_contract
+ from .heal_probe import require
+except ImportError:
+ from boundaries import boundary_sizes, seed_boundaries, verify_boundary_ranges, verify_size_contract
+ from heal_probe import require
+
+
+MPU_MINIMUM = 5 * 1024**2
+# Exact reviewed GLOBAL_MIN_PART_SIZE and is_min_allowed_part_size source.
+MPU_SOURCES = {
+ "1c7fc818dd858247c45b39542abd8291b1e6e31b": "6975b05976e88667065dd4fd2d7649ff61ce0887504e2790bc68fc73c9ba048a",
+ "85c706cac09d53d0649487b8f1865f18beb53225": "6975b05976e88667065dd4fd2d7649ff61ce0887504e2790bc68fc73c9ba048a",
+}
+
+
+def parameters(manifest, candidate, calibration, processes, recorder, original):
+ contract = verify_size_contract(manifest, candidate)
+ require(candidate["source_sha"] in MPU_SOURCES and
+ contract["sourceFiles"]["crates/ecstore/src/set_disk/mod.rs"] == MPU_SOURCES[candidate["source_sha"]],
+ "shard DB multipart size controls require an exact reviewed candidate source")
+ require({row["key"] for row in original if row["key"].startswith("objects/")} ==
+ {f"objects/{index:04d}.bin" for index in range(128)},
+ "size controls must retain every original default DB object")
+ plan = boundary_sizes(contract, calibration, processes)
+ plan["groups"]["multipart-minimum-minus-exact-plus"] = [MPU_MINIMUM - 1, MPU_MINIMUM, MPU_MINIMUM + 1]
+ sizes = [size for group in plan["groups"].values() for size in group]
+ require(max(sizes) <= 64 * 1024**2 and recorder.bytes_written + sum(sizes) <= recorder.probe.binding["maxInventoryBytes"]
+ and len(original) + len(sizes) <= recorder.probe.binding["maxInventoryObjects"],
+ "supplemental DB controls exceed the original complete fixture budget")
+ return {**plan, "originalDefaultDBCount": 128, "supplementalObjectCount": len(sizes),
+ "supplementalPayloadBytes": sum(sizes), "multipartMinimumPartBytes": MPU_MINIMUM,
+ "multipartBoundarySourceSha256": MPU_SOURCES[candidate["source_sha"]],
+ "multipartBoundaryIsObjectSizeControl": True, "completedMultipartProtocolClaimed": False,
+ "faultBranchMatrixChanged": False}
+
+
+def seed_controls(recorder, bucket, plan):
+ expected, _ = seed_boundaries(recorder, bucket, plan)
+ return expected
+
+
+def map_controls(snapshot, bucket, expected, plan):
+ # Lazy imports avoid the existing versions -> shards dependency at startup.
+ try:
+ from .shards import map_target
+ from .versions import map_inline_target
+ except ImportError:
+ from shards import map_target
+ from versions import map_inline_target
+ sizes = [size for group in plan["groups"].values() for size in group]
+ require(len(expected) == len(sizes) == plan["supplementalObjectCount"],
+ "supplemental DB manifest omitted a required boundary")
+ targets = []
+ for index, (entry, size) in enumerate(zip(expected, sizes)):
+ require(entry["key"] == f"boundaries/{index:04d}-{size}.bin" and entry["versionId"] == "null"
+ and entry["deleteMarker"] is False and entry["isLatest"] is True and entry["payload"]["size"] == size,
+ "supplemental DB ACK identity or size differs from the frozen plan")
+ shape = "zero-metadata" if size == 0 else "inline" if size <= plan["actualLogicalInlineLimit"] else "external"
+ target = (map_target(snapshot, bucket, entry["key"], "data", 1) if shape == "external" else
+ map_inline_target(snapshot, bucket, entry, allow_empty=size == 0))
+ geometry = target["logical"]["erasure"]
+ require(target["logical"]["size"] == size and target["logical"]["usesLegacyChecksum"] is False
+ and geometry["data_blocks"] == plan["actualDataBlocks"]
+ and geometry["block_size"] == plan["actualECBlockSize"]
+ and len(target["cohort"]) == geometry["data_blocks"] + geometry["parity_blocks"],
+ "supplemental DB control differs from the actual calibrated full-set geometry")
+ targets.append({"expected": entry, "shape": shape, "target": target})
+ return targets
+
+
+def prove_controls(fleet, targets, label):
+ try:
+ from .versions import verify_proof
+ except ImportError:
+ from versions import verify_proof
+ return [{"key": row["expected"]["key"], "size": row["expected"]["payload"]["size"], "shape": row["shape"],
+ "physical": verify_proof(fleet, row["target"], label)} for row in targets]
+
+
+def verify_ranges(probe, fleet, bucket, expected, plan):
+ return verify_boundary_ranges(probe, fleet, bucket, expected, plan["actualECBlockSize"])
diff --git a/scripts/validation_heal/shards.py b/scripts/validation_heal/shards.py
new file mode 100644
index 00000000..54284a27
--- /dev/null
+++ b/scripts/validation_heal/shards.py
@@ -0,0 +1,840 @@
+#!/usr/bin/env python3
+"""Run external-shard faults with frozen identities and physical repair oracles."""
+
+from __future__ import annotations
+
+import argparse
+from datetime import datetime, timedelta, timezone
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import secrets
+import signal
+import sys
+import time
+from urllib.parse import quote
+import uuid
+
+try:
+ from .heal_probe import Probe, canonical, compare_inventories, payload, require, save
+ from .heal_workload import Recorder, seed, verify_seed
+ from .healthy import BASE_OPTIONS, Fleet, physical_content, validate_topology
+ from .inspector import verify as verify_inspector
+ from .matrix import select
+except ImportError:
+ from heal_probe import Probe, canonical, compare_inventories, payload, require, save
+ from heal_workload import Recorder, seed, verify_seed
+ from healthy import BASE_OPTIONS, Fleet, physical_content, validate_topology
+ from inspector import verify as verify_inspector
+ from matrix import select
+
+
+# Every branch uses a distinct object. Positions refer to physical bytes,
+# including the checksum header; multipart numbers come from decoded metadata.
+BRANCHES = {
+ "heal.range-head-and-conditions": [
+ ("head", "objects/0008.bin", "data", 1, "missing", None, 2),
+ ("range-valid", "multipart/0000.bin", "data", 2, "bitflip", "middle", 2),
+ ("range-outside", "objects/0017.bin", "data", 1, "missing", None, 2),
+ ("if-match", "objects/0026.bin", "data", 1, "bitflip", "middle", 2),
+ ("if-none-match", "objects/0035.bin", "data", 1, "missing", None, 2),
+ ("if-modified-since", "objects/0044.bin", "data", 1, "bitflip", "middle", 2),
+ ("if-unmodified-since", "objects/0053.bin", "data", 1, "missing", None, 2)],
+ "heal.admin-dry-run": [
+ ("missing-remove0-recreate0", "objects/0008.bin", "data", 1, "missing", None, 2),
+ ("missing-remove1-recreate0", "objects/0017.bin", "data", 1, "missing", None, 2),
+ ("bitflip-remove0-recreate1", "objects/0026.bin", "data", 1, "bitflip", "middle", 2),
+ ("bitflip-remove1-recreate1", "objects/0035.bin", "data", 1, "bitflip", "middle", 2),
+ ("versioned-missing", "objects/0000.bin", "data", 1, "missing", None, 2),
+ ("versioned-bitflip", "objects/0001.bin", "data", 1, "bitflip", "middle", 2)],
+ "heal.admin-token-cross-node-status": [
+ ("coordinator-to-peer", "objects/0008.bin", "data", 1, "missing", None, 2),
+ ("coordinator-to-stable-endpoint", "objects/0017.bin", "data", 1, "missing", None, 2)],
+ "heal.missing-part-file": [
+ ("data-single-part", "objects/0008.bin", "data", 1, "missing", None, 2),
+ ("parity-single-part", "objects/0017.bin", "parity", 1, "missing", None, 2),
+ ("multipart-first", "multipart/0000.bin", "data", 1, "missing", None, 2),
+ ("multipart-middle", "multipart/0001.bin", "data", 2, "missing", None, 2),
+ ("multipart-last", "multipart/0002.bin", "data", 3, "missing", None, 2)],
+ "heal.data-shard-bitrot": [
+ ("data-first-byte", "objects/0008.bin", "data", 1, "bitflip", "first", 2),
+ ("data-middle-byte", "objects/0017.bin", "data", 1, "bitflip", "middle", 2),
+ ("data-last-byte", "objects/0026.bin", "data", 1, "bitflip", "last", 2),
+ ("multipart-data", "multipart/0000.bin", "data", 2, "bitflip", "middle", 2)],
+ "heal.parity-shard-bitrot": [
+ ("parity-first-byte", "objects/0008.bin", "parity", 1, "bitflip", "first", 2),
+ ("parity-middle-byte", "objects/0017.bin", "parity", 1, "bitflip", "middle", 2),
+ ("parity-last-byte", "objects/0026.bin", "parity", 1, "bitflip", "last", 2)],
+ "heal.truncated-and-zeroed-shard": [
+ ("truncate-half", "objects/0008.bin", "data", 1, "truncate", None, 2),
+ ("truncate-near-tail", "objects/0017.bin", "data", 1, "truncate", "last", 2),
+ ("zero-all", "objects/0026.bin", "data", 1, "zero", None, 2),
+ ("checksum-header", "objects/0035.bin", "data", 1, "header", None, 2)],
+ "heal.completed-multipart-object": [
+ ("first-part", "multipart/0000.bin", "data", 1, "missing", None, 2),
+ ("middle-part", "multipart/0001.bin", "data", 2, "missing", None, 2),
+ ("last-part", "multipart/0002.bin", "data", 3, "missing", None, 2),
+ ("cross-part-range", "multipart/0003.bin", "data", 2, "bitflip", "middle", 2)],
+ "heal.manual-normal-attribution": [
+ ("normal-missing-data", "objects/0008.bin", "data", 1, "missing", None, 1),
+ ("normal-missing-parity", "objects/0017.bin", "parity", 1, "missing", None, 1)],
+ "heal.manual-deep-attribution": [
+ ("deep-parity-bitrot", "objects/0008.bin", "parity", 1, "bitflip", "middle", 2),
+ ("deep-data-bitrot", "objects/0017.bin", "data", 1, "bitflip", "middle", 2)],
+}
+SIZES = [0, 1, 4095, 4096, 4097, 1024**2 - 1, 1024**2, 1024**2 + 1, 5 * 1024**2 + 123]
+MULTIPART_SIZES = (5 * 1024**2 + 17, 5 * 1024**2 + 93, 1024**2 + 47)
+COORDINATION_REJECTION_BODY = (
+ 'InternalError'
+ 'cluster heal coordination unavailable')
+
+
+def validate_coordination_rejection(observation, *, path=None):
+ require(observation.get("httpStatus") == 500
+ and observation.get("response") == COORDINATION_REJECTION_BODY,
+ "Heal admission refusal differs from the reviewed coordinator failure")
+ require(path is None or observation.get("path") == path,
+ "Heal admission refusal belongs to another request path")
+ return {"admissionState": "coordination-rejected", "path": observation.get("path"),
+ "httpStatus": 500, "response": COORDINATION_REJECTION_BODY,
+ "errorCode": "InternalError", "errorMessage": "cluster heal coordination unavailable",
+ "clientToken": None, "taskAdmitted": False, "responsibilityReported": False}
+
+
+def validate_coordination_rejection_logs(observations):
+ require(isinstance(observations, dict) and observations, "coordinator failure logs are missing")
+ request = observations.get("request")
+ peers = observations.get("peers")
+ require(isinstance(request, dict) and isinstance(peers, dict),
+ "coordinator rejection lacks its request window and peer logs")
+ require(request.get("method") == "POST" and request.get("httpStatus") == 500
+ and isinstance(request.get("path"), str) and request["path"].startswith("/rustfs/admin/v3/heal/")
+ and request.get("response") == COORDINATION_REJECTION_BODY,
+ "coordinator rejection request identity differs from the reviewed Heal request")
+ require(isinstance(request.get("requestId"), str) and request["requestId"]
+ and isinstance(request.get("unitFragment"), str) and request["unitFragment"],
+ "coordinator rejection request lacks its HTTP identity or run unit")
+ try:
+ started = datetime.fromisoformat(request["startedAt"])
+ finished = datetime.fromisoformat(request["finishedAt"])
+ except (KeyError, TypeError, ValueError):
+ require(False, "coordinator rejection request has no valid client time window")
+ require(started.tzinfo is not None and finished.tzinfo is not None and started <= finished,
+ "coordinator rejection request has an invalid client time window")
+ expected_node = request.get("serviceNode")
+ require(isinstance(expected_node, str) and expected_node in peers,
+ "coordinator rejection logs omit the request-serving node")
+
+ service = peers[expected_node]
+ require(isinstance(service, dict) and service.get("node") == expected_node
+ and isinstance(service.get("unit"), str)
+ and request["unitFragment"] in service["unit"],
+ "coordinator rejection log unit does not belong to the active test run")
+ journal = service.get("journal")
+ require(isinstance(journal, str), "request-serving node has no service journal")
+ matches = []
+ for line in journal.splitlines():
+ offset = line.find("{")
+ if offset < 0:
+ continue
+ try:
+ event = json.loads(line[offset:])
+ raw_timestamp = event.get("timestamp")
+ if not isinstance(raw_timestamp, str):
+ continue
+ timestamp = datetime.fromisoformat(raw_timestamp.replace("Z", "+00:00"))
+ except (json.JSONDecodeError, KeyError, TypeError, ValueError):
+ continue
+ if timestamp.tzinfo is None:
+ continue
+ timestamp = timestamp.astimezone(timezone.utc)
+ # Client and service clocks can differ slightly; the bounded margin is
+ # still narrow enough to distinguish this short POST from nearby Heal work.
+ if not started - timedelta(seconds=1) <= timestamp <= finished + timedelta(seconds=1):
+ continue
+ if (event.get("level") != "WARN" or event.get("message") != "cluster heal coordination failed"
+ or event.get("event") != "admin_request_failed" or event.get("component") != "admin_api"
+ or event.get("subsystem") != "heal_admin" or event.get("operation") != "heal_control"
+ or event.get("result") != "failed" or event.get("target") != "rustfs::admin::handlers::heal"):
+ continue
+ reason, error = event.get("reason"), event.get("error")
+ if reason == "local_coordinator_failed" and isinstance(error, str) \
+ and 'message: "heal control start admission failed"' in error:
+ matches.append((timestamp, reason, "heal control start admission failed"))
+ elif reason == "cluster_capability_unavailable" and isinstance(error, str) \
+ and "remote rpc client unavailable: can not get heal control client" in error \
+ and "transport error" in error:
+ matches.append((timestamp, reason, "heal control capability preflight failed"))
+ require(matches, "coordinator rejection is not attributed to this request's exact pre-admission failure in service logs")
+ require(len(matches) == 1, "coordinator rejection has ambiguous matching service failures in its request window")
+ timestamp, reason, failure = matches[0]
+ return {"coordinatorFailureLogged": True, "serviceNode": expected_node,
+ "serviceUnit": service["unit"], "requestPath": request["path"],
+ "requestId": request["requestId"], "eventTimestamp": timestamp.isoformat(),
+ "reason": reason, "preAdmissionFailure": failure,
+ "correlation": "request-serving node + active run unit + bounded client request window + exact structured RustFS event"}
+
+
+def files(snapshot):
+ entries = {(node, entry["disk"], entry["path"]): entry
+ for node, observation in snapshot.items() for entry in observation["files"]}
+ require(len(entries) == sum(len(value["files"]) for value in snapshot.values()),
+ "duplicate physical file identity")
+ return entries
+
+
+def map_target(snapshot, bucket, key, role, part_number, version_id="null", *, preferred_nodes=None, latest_required=True,
+ multi_set=False, set_index=0, pool_index=None):
+ return _map_external_target(snapshot, bucket, key, role, part_number, version_id,
+ preferred_nodes=preferred_nodes, latest_required=latest_required, multi_set=multi_set, set_index=set_index, pool_index=pool_index)
+
+
+def _map_external_target(snapshot, bucket, key, role, part_number, version_id="null", *, preferred_nodes=None,
+ latest_required=True, multi_set=False, set_index=0, part_policy=None, pool_index=None):
+ """Share set/role authority checks while keeping transformed part policy explicit."""
+ require(pool_index is None or type(pool_index) is int and 0 <= pool_index < 2 and multi_set is False,
+ "invalid explicit pool mapping")
+ require(type(multi_set) is bool and type(set_index) is int and 0 <= set_index < (2 if multi_set or pool_index is not None else 1),
+ "invalid explicit object set mapping")
+ if pool_index is None:
+ validate_topology(snapshot, "H1" if len(snapshot) == 1 else "H2", expected_sets=2 if multi_set else 1)
+ else:
+ try: from .multi_pool_layout import validate as validate_pools
+ except ImportError: from multi_pool_layout import validate as validate_pools
+ validate_pools(snapshot)
+ indexed = files(snapshot)
+ cohort, logical = [], None
+ for node, observation in snapshot.items():
+ for format_entry in observation["formats"]:
+ disk = format_entry["disk"]
+ identity = (node, disk, f"{bucket}/{key}/xl.meta")
+ layout = format_entry["value"]["xl"]
+ if pool_index is not None and disk // 2 != pool_index:
+ other_versions = indexed.get(identity, {}).get("metadata", {}).get("versions", [])
+ require(not any((version.get("versionId") or "null") == version_id for version in other_versions),
+ "ACK version has an additional authority in an unselected pool")
+ continue
+ if layout["this"] not in layout["sets"][set_index]:
+ require(not any(owner == node and drive == disk and path.startswith(f"{bucket}/{key}/")
+ for owner, drive, path in indexed), "object files span the selected and unselected erasure sets")
+ continue
+ metadata = indexed.get(identity, {}).get("metadata", {})
+ require(metadata.get("bucket") == bucket and metadata.get("key") == key,
+ "missing authoritative metadata identity on a baseline drive")
+ versions = [version for version in metadata.get("versions", []) if (version.get("versionId") or "null") == version_id]
+ require(len(versions) == 1,
+ "baseline metadata version differs from acknowledged fixture")
+ version = versions[0]
+ require((not latest_required or version.get("isLatest") is True) and version.get("deleted") is False
+ and version.get("inline") is None and version.get("dataDir"), "expected live external object")
+ data_dir = version["dataDir"]
+ require(str(uuid.UUID(data_dir)) == data_dir, "noncanonical data directory")
+ erasure = version["erasure"]
+ data, parity = erasure["data_blocks"], erasure["parity_blocks"]
+ distribution = erasure["distribution"]
+ require(type(data) is int and type(parity) is int and data > 0 and parity > 0
+ and data + parity == len(distribution)
+ and data + parity == len(layout["sets"][set_index])
+ and sorted(distribution) == list(range(1, data + parity + 1)), "invalid erasure geometry")
+ slot = layout["sets"][set_index].index(layout["this"])
+ require(erasure["index"] == distribution[slot], "shard role does not match the disk's actual set slot")
+ actual_role = "data" if erasure["index"] <= data else "parity"
+ require(version.get("role") == actual_role, "decoded shard role differs from erasure index")
+ logical_entry = {name: version.get(name) for name in
+ ("versionId", "isLatest", "deleted", "dataDir", "size", "parts", "usesLegacyChecksum")}
+ logical_entry["erasure"] = {name: erasure[name] for name in
+ ("data_blocks", "parity_blocks", "block_size", "distribution", "algorithm")}
+ require(logical is None or logical == logical_entry, "baseline versions or part geometry lack one full-set authority")
+ logical = logical_entry
+ parts = [part for part in version["parts"] if part["number"] == part_number]
+ require(len(parts) == 1, "ambiguous fixture part")
+ if part_policy is None:
+ require(parts[0]["size"] == parts[0]["actualSize"] > 0,
+ "unsupported compressed/encrypted or ambiguous fixture part")
+ else:
+ part_policy(version, parts[0])
+ relative = f"{bucket}/{key}/{data_dir}/part.{part_number}"
+ entry = indexed.get((node, disk, relative))
+ require(entry and entry["size"] > 32, "external part is missing from the healthy baseline")
+ cohort.append({"node": node, "disk": disk, "relativePath": relative, "role": actual_role,
+ "index": erasure["index"], "versionId": version_id, "partNumber": part_number,
+ "dataDir": data_dir, "diskUUID": layout["this"], "setSlot": slot,
+ "sha256": entry["sha256"], "size": entry["size"], "metadata": metadata,
+ "baselineMetadataSha256": indexed[identity]["sha256"]})
+ require(logical is not None and len(cohort) == logical["erasure"]["data_blocks"] + logical["erasure"]["parity_blocks"]
+ and len({entry["index"] for entry in cohort}) == len(cohort)
+ and len({entry["sha256"] for entry in cohort}) == len(cohort),
+ "independent full-set shard identity or nonrepeating content is missing")
+ target = next((entry for entry in cohort if entry["role"] == role
+ and (preferred_nodes is None or entry["node"] in preferred_nodes)), None)
+ require(target is not None, "requested shard role is not present")
+ return {**target, "bucket": bucket, "key": key, "poolIndex": 0 if pool_index is None else pool_index, "setIndex": set_index,
+ "cohort": cohort, "logical": logical}
+
+
+def verify_cohort(fleet, target, label):
+ proofs = []
+ for entry in target["cohort"]:
+ proof = fleet.record(label, {"node": entry["node"], **fleet.remote(entry["node"], "verify-part",
+ disk=entry["disk"], relativePath=entry["relativePath"],
+ versionId=entry["versionId"], partNumber=entry["partNumber"])})
+ require(proof["sha256"] == entry["sha256"] and proof["physicalSize"] == entry["size"],
+ "physical part differs from the frozen same-role reconstruction oracle")
+ proofs.append(proof)
+ return {"allShardBitrotVerified": True, "sameRoleOriginalShardBytesRestored": True,
+ "proof": "complete pre-fault shard oracle, distinct shards, verified streaming checksums", "shards": proofs}
+
+
+def validate_fault(before, after, target, receipt):
+ left, right = files(before), files(after)
+ identity = (target["node"], target["disk"], target["relativePath"])
+ require(receipt.get("relativePath") == target["relativePath"]
+ and receipt.get("before") == {"sha256": target["sha256"], "size": target["size"]},
+ "fault receipt differs from the frozen target")
+ for node in before:
+ require(before[node]["formats"] == after[node]["formats"], "file fault changed disk format")
+ expected = receipt.get("fault")
+ if expected is None:
+ require(identity not in right, "missing target was not physically witnessed")
+ else:
+ require(identity in right and {name: right[identity][name] for name in ("sha256", "size")} == expected
+ and expected != receipt["before"], "changed shard was not physically witnessed")
+ require({key: value for key, value in left.items() if key != identity}
+ == {key: value for key, value in right.items() if key != identity},
+ "file fault changed metadata, controls or another shard")
+
+
+def validate_repaired_physical(before, after, target, *, repair_targets=None):
+ left, right = files(before), files(after)
+ repair_targets = repair_targets or [target]
+ meta_paths = {f"{item['bucket']}/{item['key']}/xl.meta" for item in repair_targets}
+ backups = {(item["node"], item["disk"], f"{item['bucket']}/{item['key']}/{item['dataDir']}/xl.meta.bkp"):
+ left.get((item["node"], item["disk"], f"{item['bucket']}/{item['key']}/xl.meta"))
+ for item in repair_targets}
+ completed_sidecars = {(item["node"], item["disk"],
+ f"{item['bucket']}/{item['key']}/{item['dataDir']}/part.{part['number']}.meta")
+ for item in repair_targets for part in item["logical"]["parts"]}
+ def exact_backup(identity, actual):
+ original = backups.get(identity)
+ return bool(original and actual and actual["sha256"] == original["sha256"] and actual["size"] == original["size"])
+ for node in before:
+ require(before[node]["formats"] == after[node]["formats"], "repair changed disk format")
+ for identity, entry in left.items():
+ actual = right.get(identity)
+ # Complete MPU commits the authoritative part manifest into xl.meta.
+ # A healed target directory can discard only its stale per-part sidecars;
+ # other versions, drives, pending uploads and payload files stay exact.
+ if actual is None and identity in completed_sidecars:
+ continue
+ require(actual is not None, "repair lost a baseline file or control")
+ if identity in backups and actual != entry:
+ # Repeated repair replaces only this target's rollback metadata
+ # with the exact authoritative bytes observed before this repair.
+ require(exact_backup(identity, actual), "target rollback backup is not the exact pre-repair metadata")
+ elif identity[2] not in meta_paths:
+ require(actual == entry, "repair did not restore exact part bytes or changed a control")
+ else:
+ def semantics(metadata):
+ return {"bucket": metadata["bucket"], "key": metadata["key"], "versions": [
+ {**version, "inline": {name: version["inline"].get(name) for name in ("length", "sha256")}
+ if version.get("inline") else None,
+ "erasure": {name: value for name, value in version["erasure"].items() if name != "checksums"}}
+ for version in metadata["versions"]]}
+ require(actual.get("metadata") and semantics(actual["metadata"]) == semantics(entry["metadata"]),
+ "repair changed decoded version/part identity")
+ for identity in set(right) - set(left):
+ require(exact_backup(identity, right[identity]),
+ "repair created an unexpected physical file")
+ return {"targetLengthAndFullSha256Restored": True, "controlFilesUnchanged": True,
+ "decodedVersionsUnchanged": True, "formatUnchanged": True, "readBeforeRepair": False}
+
+
+def validate_repaired_status(status, target):
+ outcome = status.get("outcome") or {}
+ require(status.get("summary") == "finished" and status.get("truncated", False) is False
+ and outcome.get("execution", {}).get("state") == "completed"
+ and outcome.get("coverage") == "complete" and outcome.get("objectsTruncated") is False,
+ "repair did not prove complete terminal coverage")
+ expected = {"processed": 1, "healed": 1, "unchanged": 0, "skipped": 0,
+ "failed": 0, "unknown": 0, "attemptFailures": 0, "overflowed": False}
+ counters = outcome.get("counters", {})
+ require(all(type(counters.get(name)) is type(value) and counters[name] == value
+ for name, value in expected.items()), "repair counters do not prove exactly one repaired object")
+ objects = outcome.get("objects", [])
+ require(len(objects) == 1, "repair lacks one complete object receipt")
+ identity = objects[0].get("identity", {})
+ require(identity.get("bucket") == target["bucket"] and identity.get("object") == target["key"]
+ and (identity.get("versionId") or "null") == target["versionId"]
+ and type(identity.get("poolIndex")) is int and identity["poolIndex"] == target["poolIndex"]
+ and type(identity.get("setIndex")) is int and identity["setIndex"] == target["setIndex"]
+ and bool(identity.get("bucketIncarnationId")), "repair responsibility identity is incomplete or different")
+ require(objects[0].get("disposition", {}).get("state") == "repaired",
+ "task finish or unknown receipt does not prove repair")
+
+
+def validate_dry_run_status(status, target, expected):
+ outcome = status.get("outcome") or {}
+ identities = {(entry["key"], entry["versionId"]) for entry in expected if entry["key"] == target["key"]}
+ require(status.get("summary") == "finished" and status.get("truncated", False) is False
+ and outcome.get("execution", {}).get("state") == "completed" and outcome.get("coverage") == "complete"
+ and outcome.get("objectsTruncated") is False and identities, "dry-run did not prove complete terminal coverage")
+ objects = outcome.get("objects", [])
+ require(len(objects) == len(identities), "dry-run omitted versions or markers")
+ actual = set()
+ for entry in objects:
+ identity = entry.get("identity", {})
+ actual.add((identity.get("object"), identity.get("versionId") or "null"))
+ require(identity.get("bucket") == target["bucket"]
+ and type(identity.get("poolIndex")) is int and identity["poolIndex"] == 0
+ and type(identity.get("setIndex")) is int and identity["setIndex"] == 0
+ and entry.get("disposition", {}).get("state") == "dry_run_observed",
+ "dry-run admitted a different responsibility or claimed repair")
+ require(actual == identities, "dry-run responsibility versions differ from acknowledged fixture")
+ wanted = {"processed": len(identities), "healed": 0, "unchanged": 0, "skipped": len(identities),
+ "failed": 0, "unknown": 0, "attemptFailures": 0, "overflowed": False}
+ counters = outcome.get("counters", {})
+ require(all(type(counters.get(name)) is type(value) and counters[name] == value for name, value in wanted.items()),
+ "dry-run counters do not match the complete dry_run_observed responsibility set")
+
+
+def peer_status_equal(coordinator, peer, *, task, peer_task):
+ # The candidate renders startTime at response serialization; it is not a
+ # persistent task epoch. Bind the actual signed queries to admission instead.
+ # Convergence also applies to a truthful negative result. The case oracle,
+ # rather than this comparison, decides whether that result is expected.
+ outcome = coordinator.get("outcome") or {}
+ summary = coordinator.get("summary")
+ execution = outcome.get("execution", {}).get("state")
+ terminal = (summary == "finished" and execution == "completed"
+ or summary == "stopped" and execution == "completed_with_errors"
+ and type(outcome.get("counters", {}).get("failed")) is int
+ and outcome["counters"]["failed"] > 0)
+ return (terminal and peer.get("summary") == summary
+ and isinstance(task, tuple) and len(task) == 2 and all(task) and task == peer_task
+ and canonical(coordinator.get("outcome")) == canonical(peer.get("outcome"))
+ and canonical(coordinator.get("settings")) == canonical(peer.get("settings"))
+ and outcome.get("coverage") == "complete" and outcome.get("objectsTruncated") is False
+ and coordinator.get("truncated", False) is False and peer.get("truncated", False) is False)
+
+
+def validate_absent_request(status, bucket, key, options):
+ """Require the exact retained failure of an absent, nonrecursive object."""
+ outcome = status.get("outcome") or {}
+ require(options.get("recursive") is False and status.get("summary") == "stopped"
+ and status.get("settings") == options and status.get("truncated", False) is False
+ and outcome.get("execution", {}).get("state") == "completed_with_errors"
+ and outcome.get("coverage") == "complete" and outcome.get("objectsTruncated") is False,
+ "absent request lacks its complete original failed task/settings")
+ expected = {"processed": 1, "healed": 0, "unchanged": 0, "skipped": 0, "failed": 1,
+ "unknown": 0, "attemptFailures": 1, "overflowed": False}
+ counters = outcome.get("counters") or {}
+ require(all(type(counters.get(name)) is type(value) and counters[name] == value
+ for name, value in expected.items()), "absent request lost or falsely repaired its failure responsibility")
+ objects = outcome.get("objects")
+ require(isinstance(objects, list) and len(objects) == 1, "absent request omitted or duplicated its failed receipt")
+ receipt = objects[0]
+ identity = receipt.get("identity") or {}
+ require(set(identity) == {"kind", "bucket", "object", "versionId", "bucketIncarnationId", "poolIndex", "setIndex"}
+ and identity.get("kind") == "object" and identity.get("bucket") == bucket
+ and identity.get("object") == key
+ and identity.get("versionId") in {None, "", "null", "00000000-0000-0000-0000-000000000000"}
+ and identity.get("bucketIncarnationId") is None
+ and all(type(identity.get(field)) is type(options.get(option))
+ and identity.get(field) == options.get(option)
+ for field, option in (("poolIndex", "pool"), ("setIndex", "set")))
+ and receipt.get("disposition") == {"state": "failed", "details": "permanent"},
+ "absent request changed object/version/selector identity or claimed a different disposition")
+ detail = "Object not found: " + bucket + "/" + key
+ require(isinstance(status.get("detail"), str) and status["detail"].endswith(detail)
+ and isinstance(receipt.get("detail"), str) and receipt["detail"].endswith(detail),
+ "absent request did not retain its exact not-found error")
+ return {"failedRequest": {"bucket": bucket, "key": key}, "canonicalCounters": counters,
+ "completeResponsibility": True, "repairPassClaimed": False}
+
+
+def run_task(probe, fleet, target, mode, active, *, status_endpoints=(), option_overrides=None, admissions=None,
+ allow_coordination_rejection=False):
+ path = "/rustfs/admin/v3/heal/" + quote(target["bucket"], safe="")
+ if target["key"]:
+ path += "/" + quote(target["key"], safe="/")
+ started = fleet.record("trigger", probe.admin(path, [], json.dumps(
+ {**BASE_OPTIONS, "scanMode": mode, **(option_overrides or {})}).encode()))
+ if allow_coordination_rejection and started.get("httpStatus") != 200:
+ rejection = validate_coordination_rejection(started, path=path)
+ endpoint = started.get("endpoint")
+ match = re.fullmatch(r"node([1-9][0-9]*)", endpoint) if isinstance(endpoint, str) else None
+ require(match is not None and int(match.group(1)) <= len(fleet.nodes),
+ "rejected Heal request endpoint is not bound to a test service node")
+ request_id = (started.get("responseHeaders") or {}).get("x-request-id")
+ request_window = {name: started.get(name) for name in
+ ("method", "httpStatus", "path", "startedAt", "finishedAt", "response")}
+ request_window.update(requestId=request_id, endpoint=endpoint,
+ serviceNode=fleet.nodes[int(match.group(1)) - 1], unitFragment=fleet.run_id)
+ return {**rejection, "requestWindow": request_window}
+ require(started.get("httpStatus") == 200 and isinstance(started.get("response"), dict), "repair admission failed")
+ token = started["response"].get("clientToken")
+ require(isinstance(token, str) and token, "repair admission omitted token")
+ if admissions is not None:
+ admissions.append({"path": path, "clientToken": token})
+ active.append((path, token))
+ deadline = time.monotonic() + 180
+ while True:
+ observation = fleet.record("control", probe.admin(path, [("clientToken", token)], b""))
+ require(observation.get("httpStatus") == 200 and isinstance(observation.get("response"), dict), "repair status failed")
+ status = observation["response"]
+ if status.get("summary") != "running":
+ break
+ require(time.monotonic() < deadline, "repair task deadline exceeded")
+ time.sleep(0.2)
+ active.remove((path, token))
+ for _ in range(2):
+ repeat = fleet.record("control-repeat", probe.admin(path, [("clientToken", token)], b""))
+ require(repeat.get("httpStatus") == 200 and repeat.get("response", {}).get("summary") == status.get("summary")
+ and canonical(repeat["response"].get("outcome")) == canonical(status.get("outcome")),
+ "terminal repair responsibility changed")
+ convergence_deadline = time.monotonic() + 30
+ for endpoint in status_endpoints:
+ remote_probe = Probe(probe.binding, endpoint)
+ require(remote_probe.endpoint != probe.endpoint, "peer status must query another actual endpoint")
+ while True:
+ observed = fleet.record("cross-node-control", remote_probe.admin(path, [("clientToken", token)], b""))
+ require(observed.get("httpStatus") == 200 and isinstance(observed.get("response"), dict),
+ "cross-node status is missing; 404/unknown cannot certify task completion")
+ if peer_status_equal(status, observed["response"], task=(path, token), peer_task=(path, token)):
+ break
+ require(time.monotonic() < convergence_deadline, "peer responsibility did not converge within 30 seconds")
+ time.sleep(0.2)
+ if status_endpoints:
+ fleet.record("cross-node-convergence", {"path": path, "clientToken": token,
+ "coordinator": probe.endpoint_name, "queriedEndpoints": list(status_endpoints), "budgetSeconds": 30,
+ "sameOriginalPathAndToken": True, "completeOutcomeEqual": True,
+ "responseStartTimeIsTaskIdentity": False})
+ return status
+
+
+def pending_inventory(probe, bucket):
+ response = probe.s3("list_multipart_uploads", {"Bucket": bucket})
+ require(response["acknowledged"] and not response["response"].get("IsTruncated"), "MPU control listing failed")
+ entries = []
+ for upload in response["response"].get("Uploads", []):
+ require(upload["Key"] in {"pending-a", "pending-b"}, "unexpected pending MPU")
+ parts = probe.s3("list_parts", {"Bucket": bucket, "Key": upload["Key"], "UploadId": upload["UploadId"]})
+ require(parts["acknowledged"] and not parts["response"].get("IsTruncated"), "pending MPU part listing failed")
+ entries.append({"key": upload["Key"], "uploadId": upload["UploadId"], "parts": [
+ {name: part[name] for name in ("PartNumber", "ETag", "Size")} for part in parts["response"].get("Parts", [])]})
+ require(len(entries) == 2 and {entry["key"] for entry in entries} == {"pending-a", "pending-b"}
+ and all(len(entry["parts"]) == 1 for entry in entries), "acknowledged pending MPU controls are missing")
+ return sorted(entries, key=lambda entry: entry["key"])
+
+
+def verify_ranges(probe, fleet, bucket, key, *, version_id="null", expected_body=None):
+ if key.startswith("multipart/"):
+ body = b"".join(payload(f"{bucket}/{key}/{number}", size) for number, size in enumerate(MULTIPART_SIZES, 1))
+ boundaries = (MULTIPART_SIZES[0], sum(MULTIPART_SIZES[:2]))
+ else:
+ body = expected_body if expected_body is not None else payload(
+ f"{bucket}/{key}/0", SIZES[int(key.rsplit("/", 1)[1].split(".")[0]) % len(SIZES)])
+ boundaries = (1024**2, len(body) // 2)
+ proofs = []
+ require(len(body) > 0, "positive ranges require a nonempty fixture")
+ intervals = [(max(0, point - 17), min(len(body) - 1, point + 29))
+ for point in boundaries if 0 < point < len(body)]
+ intervals.append((max(0, len(body) - 47), len(body) - 1))
+ for start, end in intervals:
+ require(0 <= start <= end < len(body), "invalid fixture range")
+ observed = fleet.record("s3-range", probe.s3("get_object", {"Bucket": bucket, "Key": key,
+ "VersionId": version_id, "Range": f"bytes={start}-{end}"}))
+ wanted = body[start:end + 1]
+ require(observed["acknowledged"] and observed["response"]["ResponseMetadata"]["HTTPStatusCode"] == 206
+ and observed["response"].get("ContentRange") == f"bytes {start}-{end}/{len(body)}"
+ and observed["payload"] == {"size": len(wanted), "sha256": hashlib.sha256(wanted).hexdigest()},
+ "range content or part ordering differs from independent fixture")
+ proofs.append({"start": start, "end": end, "sha256": hashlib.sha256(wanted).hexdigest()})
+ return proofs
+
+
+def validate_conditional_response(observed, status, baseline, *, code=None, full_body=False):
+ response = observed.get("response") or {}
+ require(response.get("ResponseMetadata", {}).get("HTTPStatusCode") == status,
+ f"conditional request expected HTTP {status}")
+ if status >= 300:
+ require(observed.get("acknowledged") is False and observed.get("resultUnknown") is False
+ and "payload" not in observed, "conditional denial returned success data or unknown transport outcome")
+ if code is not None:
+ require(response.get("Error", {}).get("Code") == code, "conditional response has the wrong structured error")
+ else:
+ require(observed.get("acknowledged") is True and response.get("ETag") == baseline["properties"]["ETag"],
+ "conditional request returned the wrong object ETag")
+ if full_body:
+ require(observed.get("payload") == baseline["payload"], "conditional GET returned wrong full content")
+
+
+def verify_conditions(probe, fleet, branch, baseline, head, label):
+ name, key = branch[:2]
+ entry = next(entry for entry in baseline["entries"] if entry["key"] == key)
+ params = {"Bucket": baseline["bucket"], "Key": key, "VersionId": entry["versionId"]}
+ etag, modified = head["ETag"], head["LastModified"]
+ require(isinstance(modified, datetime) and modified.tzinfo is not None, "baseline LastModified must be server time")
+ requests = {
+ "head": [("head_object", {}, 200, None)],
+ "range-outside": [("get_object", {"Range": f"bytes={entry['payload']['size']}-"}, 416, "InvalidRange")],
+ "if-match": [("get_object", {"IfMatch": etag}, 200, None),
+ ("get_object", {"IfMatch": '"00000000000000000000000000000000"'}, 412, "PreconditionFailed")],
+ "if-none-match": [("get_object", {"IfNoneMatch": etag}, 304, None),
+ ("get_object", {"IfNoneMatch": '"00000000000000000000000000000000"'}, 200, None)],
+ "if-modified-since": [("get_object", {"IfModifiedSince": modified + timedelta(seconds=2)}, 304, None),
+ ("get_object", {"IfModifiedSince": modified - timedelta(seconds=2)}, 200, None)],
+ "if-unmodified-since": [("get_object", {"IfUnmodifiedSince": modified - timedelta(seconds=2)}, 412, "PreconditionFailed"),
+ ("get_object", {"IfUnmodifiedSince": modified + timedelta(seconds=2)}, 200, None)]}
+ if name == "range-valid":
+ return verify_ranges(probe, fleet, baseline["bucket"], key)
+ results = []
+ for operation, extra, status, code in requests[name]:
+ observed = fleet.record(label + "-" + name, probe.s3(operation, {**params, **extra}))
+ validate_conditional_response(observed, status, entry, code=code, full_body=operation == "get_object" and status == 200)
+ if operation == "head_object":
+ require(observed["response"].get("LastModified") == modified
+ and all(observed["response"].get(field) == value for field, value in entry["properties"].items()),
+ "HEAD content properties or server LastModified changed")
+ results.append({"operation": operation, "status": status, "condition": list(extra)})
+ return results
+
+
+def run_topology(args, topology, nodes, *, fixture="DB"):
+ directory = args.evidence / topology
+ directory.mkdir()
+ run_id = f"gh-{os.environ['GITHUB_RUN_ID']}-{os.environ.get('GITHUB_RUN_ATTEMPT', '1')}-{topology.lower()}"
+ fleet = Fleet(nodes, directory, run_id, args.tool_directory)
+ candidate = json.loads(args.candidate.read_text())
+ tool_manifest = fleet.record("inspector-build", verify_inspector(args.tool_directory, candidate))
+ bucket = "rv-heal-" + hashlib.sha256(run_id.encode()).hexdigest()[:16]
+ access, secret = secrets.token_hex(16), secrets.token_hex(24)
+ os.environ.update(HEAL_ACCESS_KEY=access, HEAL_SECRET_KEY=secret)
+ binding = {"schemaVersion": 1, "runId": run_id, "jumpHostOnly": True, "exclusiveHealService": True,
+ "leaseExpiresAt": (datetime.now(timezone.utc) + timedelta(minutes=30)).isoformat(), "bucketPrefix": bucket,
+ "endpoints": {f"node{index}": f"http://rustfs-node{index}:9000" for index in range(1, len(nodes) + 1)},
+ "defaultEndpoint": "node1", "candidateCommit": candidate["source_sha"], "harnessCommit": args.testing_commit,
+ "candidatePackageSha256": candidate["package_sha256"], "candidateBinarySha256": args.binary_sha,
+ "rcBinarySha256": hashlib.sha256(args.rc_bin.read_bytes()).hexdigest(),
+ "maxInventoryObjects": 256, "maxInventoryBytes": 512 * 1024**2}
+ fleet.record("binding", binding)
+ recipe = select(args.case_id)[0]
+ require([branch[0] for branch in BRANCHES[args.case_id]] == recipe["variants"], "executor omitted a source branch")
+ result = {"topology": topology, "fixture": fixture, "verdict": "RUNNING", "branches": {}, "cleanup": "PENDING"}
+ active = []
+ probe = Probe(binding)
+ try:
+ fleet.deploy(args.binary_sha, bucket, access, secret)
+ fleet.record("topology-proof", validate_topology(fleet.snapshot("deployed-topology"), topology))
+ recorder = Recorder(probe, directory / "fixture")
+ expected = seed(recorder, bucket, fixture, 128 if fixture == "DB" else 4,
+ SIZES if fixture == "DB" else [5 * 1024**2 + 123])
+ needs_multipart = "DM" in recipe["contract"]["probes"]["fixture"].split(",")
+ if needs_multipart:
+ expected += seed(recorder, bucket, "DM", 4, SIZES, existing_bucket=True)
+ boundary_plan, boundary_expected, boundary_targets = None, [], []
+ if args.case_id in {"heal.data-shard-bitrot", "heal.parity-shard-bitrot"}:
+ try:
+ from . import shard_boundaries
+ except ImportError:
+ import shard_boundaries
+ calibration = fleet.snapshot("DB-size-calibration-live")
+ target = map_target(calibration, bucket, "objects/0008.bin", "data", 1)
+ boundary_plan = fleet.record("DB-source-size-plan", shard_boundaries.parameters(
+ tool_manifest, candidate, target, calibration, recorder, expected))
+ boundary_expected = shard_boundaries.seed_controls(recorder, bucket, boundary_plan)
+ expected += boundary_expected
+ baseline = fleet.record("s3-baseline", probe.inventory(bucket, unversioned_current_tags=fixture == "DB"))
+ fleet.record("fixture-manifest", {"expected": expected, **verify_seed(expected, baseline)})
+ pending = fleet.record("mpu-baseline", pending_inventory(probe, bucket)) if needs_multipart else None
+ heads = {}
+ for entry in baseline["entries"]:
+ if entry["deleteMarker"]:
+ continue
+ head = recorder.call("head_object", {"Bucket": bucket, "Key": entry["key"], "VersionId": entry["versionId"]})
+ heads[entry["key"]] = head
+ require(all(head.get(name) == value for name, value in entry["properties"].items()), "HEAD differs from GET baseline")
+ fleet.stop()
+ original = fleet.snapshot("physical-baseline", quiescent=True)
+ if boundary_plan is not None:
+ boundary_targets = shard_boundaries.map_controls(original, bucket, boundary_expected, boundary_plan)
+ fleet.record("DB-boundary-original-proofs", shard_boundaries.prove_controls(
+ fleet, boundary_targets, "DB-boundary-original-bitrot"))
+ cross_node = args.case_id == "heal.admin-token-cross-node-status"
+ if cross_node:
+ fleet.record("cross-node-binding", {"coordinator": "node1", "peer": "node2", "stableEndpoint": "node3",
+ "endpoints": binding["endpoints"], "stableEndpointKind": "fixed current auto-testing node address",
+ "faultTargetExcludesCoordinator": True})
+ dry_run = args.case_id == "heal.admin-dry-run"
+ conditions = args.case_id == "heal.range-head-and-conditions"
+ selected_branches = [branch for branch in BRANCHES[args.case_id]
+ if not dry_run or branch[0].startswith("versioned-") == (fixture == "DV")]
+ if conditions:
+ fleet.start()
+ for branch in selected_branches:
+ verify_conditions(probe, fleet, branch, baseline, heads[branch[1]], "baseline-condition")
+ fleet.stop()
+ targets = []
+ for branch in selected_branches:
+ version = next(entry for entry in expected if entry["key"] == branch[1] and not entry["deleteMarker"])
+ targets.append((branch, map_target(original, bucket, branch[1], branch[2], branch[3], version["versionId"],
+ preferred_nodes=nodes[1:] if cross_node else None, latest_required=fixture != "DV")))
+ fleet.record("fault-plan", {"caseId": args.case_id, "topology": topology,
+ "branches": [{"variant": branch[0], "faultBackend": "logical-file-fault", "target": target,
+ "kind": branch[4], "position": branch[5], "scanMode": branch[6]} for branch, target in targets]})
+ for branch, target in targets:
+ name, key, role, number, kind, position, mode = branch
+ fault_id = recipe["code"] + "-" + name
+ recovery = {"disk": target["disk"], "faultId": fault_id}
+ registered = False
+ try:
+ fleet.stop()
+ before = fleet.snapshot("physical-before-" + name, quiescent=True)
+ verify_cohort(fleet, target, "baseline-bitrot-" + name)
+ offset = {"first": 0, "middle": target["size"] // 2, "last": target["size"] - 1}.get(position)
+ fleet.faults.append((target["node"], recovery))
+ registered = True
+ receipt = fleet.record("fault-" + name, fleet.remote(target["node"], "apply-fault",
+ **recovery, relativePath=target["relativePath"], expectedSha256=target["sha256"], kind=kind, offset=offset))
+ after_fault = fleet.snapshot("physical-fault-" + name, quiescent=True)
+ validate_fault(before, after_fault, target, receipt)
+ fleet.start()
+ witness = fleet.snapshot("physical-before-trigger-" + name)
+ require(physical_content(after_fault) == physical_content(witness),
+ "ATTRIBUTION_NOT_OBSERVED: fault changed before the manual task")
+ status = run_task(probe, fleet, target, mode, active,
+ status_endpoints=("node2", "node3") if cross_node else (),
+ option_overrides={"dryRun": True, "recursive": fixture == "DV",
+ "remove": "remove1" in name, "recreate": "recreate1" in name}
+ if dry_run else None)
+ listing = fleet.record("dry-run-version-list", probe.s3("list_object_versions", {"Bucket": bucket})) if dry_run else None
+ fleet.stop()
+ repaired = fleet.snapshot("physical-after-" + name, quiescent=True)
+ if dry_run:
+ require(physical_content(after_fault) == physical_content(repaired),
+ "dry-run changed object shards, metadata or delete markers")
+ validate_dry_run_status(status, target, expected)
+ require(listing["acknowledged"] and not listing["response"].get("IsTruncated"), "dry-run version listing incomplete")
+ observed_versions = {(entry["Key"], entry["VersionId"], entry["IsLatest"], deleted)
+ for field, deleted in (("Versions", False), ("DeleteMarkers", True))
+ for entry in listing["response"].get(field, [])}
+ require(observed_versions == {(entry["key"], entry["versionId"], entry["isLatest"], entry["deleteMarker"])
+ for entry in expected}, "dry-run changed version or delete-marker manifest")
+ fleet.record("dry-run-rescue", fleet.remote(target["node"], "recover-fault", **recovery))
+ fleet.faults.remove((target["node"], recovery))
+ registered = False
+ recovered = fleet.snapshot("physical-recovered-" + name, quiescent=True)
+ require(physical_content(before) == physical_content(recovered), "dry-run fault recovery differs from baseline")
+ physical = {"dryRunPhysicalUnchanged": True, "repairedAck": False, "backupUsedOnlyAfterNoMutationProof": True}
+ else:
+ physical = validate_repaired_physical(before, repaired, target)
+ if boundary_plan is not None:
+ shard_boundaries.map_controls(repaired, bucket, boundary_expected, boundary_plan)
+ fleet.record("DB-boundary-repaired-proofs-" + name, shard_boundaries.prove_controls(
+ fleet, boundary_targets, "DB-boundary-preserved-bitrot-" + name))
+ bitrot = verify_cohort(fleet, target, ("recovered-bitrot-" if dry_run else "repaired-bitrot-") + name)
+ fleet.start()
+ current = fleet.record("s3-after-" + name, probe.inventory(bucket, unversioned_current_tags=fixture == "DB"))
+ comparison = compare_inventories(baseline, current)
+ if cross_node:
+ for endpoint in binding["endpoints"]:
+ peer_s3 = fleet.record("s3-entry-" + endpoint, Probe(binding, endpoint).inventory(
+ bucket, unversioned_current_tags=True))
+ compare_inventories(baseline, peer_s3)
+ ranges = verify_ranges(probe, fleet, bucket, key, version_id=target["versionId"],
+ expected_body=payload(f"{bucket}/{key}/0", 5 * 1024**2 + 123) if fixture == "DV" else None)
+ boundary_ranges = (shard_boundaries.verify_ranges(probe, fleet, bucket, boundary_expected, boundary_plan)
+ if boundary_plan is not None else None)
+ condition_proof = verify_conditions(probe, fleet, branch, baseline, heads[key], "repaired-condition") if conditions else None
+ if pending is not None:
+ require(fleet.record("mpu-after-" + name, pending_inventory(probe, bucket)) == pending,
+ "repair changed pending MPU identities or parts")
+ if not dry_run:
+ validate_repaired_status(status, target)
+ result["branches"][name] = {"verdict": "PASS", "physical": physical, "bitrot": bitrot,
+ "s3": comparison, "ranges": ranges, "trigger": "manual-normal" if mode == 1 else "manual-deep"}
+ if boundary_plan is not None:
+ result["branches"][name]["DBBoundaryControls"] = {
+ "originalDefaultDBCount": 128, "supplementalObjectCount": len(boundary_expected),
+ "allBoundaryShapesPhysicallyVerified": True, "ranges": boundary_ranges,
+ "completedMultipartProtocolClaimed": False}
+ if condition_proof is not None:
+ result["branches"][name]["conditions"] = condition_proof
+ except (Exception, KeyboardInterrupt) as exc:
+ result["branches"][name] = {"verdict": "FAIL", "failure": {"type": type(exc).__name__, "message": str(exc)}}
+ try:
+ fleet.stop()
+ fleet.snapshot("physical-failure-" + name, quiescent=True)
+ if registered:
+ rescue = fleet.record("rescue-" + name, fleet.remote(target["node"], "recover-fault", **recovery))
+ fleet.faults.remove((target["node"], recovery))
+ result["branches"][name]["rescue"] = rescue
+ except Exception as recovery_error:
+ result["branches"][name]["recoveryError"] = str(recovery_error)
+ raise
+ if isinstance(exc, KeyboardInterrupt) or active:
+ raise
+ result["verdict"] = "PASS" if all(value["verdict"] == "PASS" for value in result["branches"].values()) else "FAIL"
+ except (Exception, KeyboardInterrupt) as exc:
+ result.update(verdict="FAIL", failure={"type": type(exc).__name__, "message": str(exc)})
+ finally:
+ for path, token in active:
+ try:
+ fleet.record("stop-unresolved", probe.admin(path, [("clientToken", token), ("forceStop", "true")], b""))
+ except Exception as exc:
+ fleet.record("stop-error", {"error": str(exc)})
+ result["cleanup"] = "PASS" if fleet.cleanup() else "FAIL"
+ fleet.record("topology-result", result)
+ return result
+
+
+def main():
+ def interrupted(signum, frame):
+ raise KeyboardInterrupt(f"Heal workflow interrupted by signal {signum}")
+ signal.signal(signal.SIGTERM, interrupted)
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--candidate", type=Path, required=True)
+ parser.add_argument("--case-id", choices=tuple(BRANCHES), required=True)
+ parser.add_argument("--testing-commit", required=True)
+ parser.add_argument("--binary-sha", required=True)
+ parser.add_argument("--rc-bin", type=Path, required=True)
+ parser.add_argument("--evidence", type=Path, required=True)
+ parser.add_argument("--tool-directory", type=Path, required=True)
+ args = parser.parse_args()
+ nodes = os.environ.get("RUSTFS_NODES", "vm000 vm001 vm002 vm003").split()
+ require(len(nodes) >= 4 and len(set(nodes)) == len(nodes)
+ and all(re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", node) for node in nodes), "four current Heal nodes required")
+ require(bool(re.fullmatch(r"[0-9a-f]{64}", args.binary_sha)), "invalid candidate binary digest")
+ require(bool(re.fullmatch(r"[1-9][0-9]*", os.environ.get("GITHUB_RUN_ID", ""))), "workflow fleet identity required")
+ results = []
+ topologies = set(select(args.case_id)[0]["contract"]["probes"]["topology"].split(","))
+ fixtures = ("DB", "DV") if args.case_id == "heal.admin-dry-run" else ("DB",)
+ try:
+ for topology, selected in (("H1", nodes[:1]), ("H2", nodes[:4])):
+ if topology not in topologies:
+ continue
+ for fixture in fixtures:
+ scoped_args = argparse.Namespace(**vars(args))
+ if len(fixtures) > 1:
+ scoped_args.evidence = args.evidence / fixture
+ scoped_args.evidence.mkdir(exist_ok=True)
+ results.append(run_topology(scoped_args, topology, selected, fixture=fixture))
+ if results[-1]["cleanup"] != "PASS":
+ break
+ if results[-1]["cleanup"] != "PASS":
+ break
+ finally:
+ complete = len(results) == len(topologies) * len(fixtures) and all(value["verdict"] == "PASS" and value["cleanup"] == "PASS" for value in results)
+ save(args.evidence / "healthy-summary.json", {"caseId": args.case_id, "results": results,
+ "cleanup": "PASS" if results and all(value["cleanup"] == "PASS" for value in results) else "FAIL", "complete": complete})
+ require(complete, "Heal case requires every source H1/H2 branch; inspect topology-result evidence")
+
+
+if __name__ == "__main__":
+ try:
+ main()
+ except Exception as exc:
+ print(f"Heal shard validation failed: {exc}", file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_heal/shared_fixture.py b/scripts/validation_heal/shared_fixture.py
new file mode 100644
index 00000000..92cdc5b8
--- /dev/null
+++ b/scripts/validation_heal/shared_fixture.py
@@ -0,0 +1,75 @@
+#!/usr/bin/env python3
+"""Verify a separately built C10 encoder; ordinary execution needs no checkout."""
+
+import argparse
+import hashlib
+import json
+from pathlib import Path
+import re
+import stat
+
+try:
+ from .authority import SOURCE_TREE, CARGO_LOCK
+ from .heal_probe import require
+ from .native_regressions import CANDIDATE
+ from .prepare_heal_shared_fixture import ROLE
+except ImportError:
+ from authority import SOURCE_TREE, CARGO_LOCK
+ from heal_probe import require
+ from native_regressions import CANDIDATE
+ from prepare_heal_shared_fixture import ROLE
+
+
+TESTS = ('tests::exact_existing_versions_share_one_directory_without_changing_other_metadata',
+ 'tests::malformed_absent_same_and_already_shared_versions_are_refused',
+ 'tests::incompatible_part_and_deleted_destination_are_refused')
+
+
+def sha(path):
+ return hashlib.sha256(path.read_bytes()).hexdigest()
+
+
+def verify(directory, candidate):
+ directory = Path(directory)
+ paths = {name: directory / name for name in ('heal-shared-fixture', 'heal-shared-fixture.json',
+ 'shared-Cargo.lock', 'shared-fixture-tests.log', 'tool-run.json')}
+ for path in paths.values():
+ info = path.lstat()
+ require(stat.S_ISREG(info.st_mode) and info.st_nlink == 1 and 0 < info.st_size <= 256 * 1024**2,
+ 'C10 fixture artifact must contain bounded original regular files')
+ manifest = json.loads(paths['heal-shared-fixture.json'].read_text())
+ run = json.loads(paths['tool-run.json'].read_text())
+ require(type(run.get('id')) is int and run['id'] > 0 and type(run.get('run_attempt')) is int and run['run_attempt'] > 0
+ and run.get('status') == 'completed' and run.get('conclusion') == 'success'
+ and run.get('path') in {'.github/workflows/rustfs-functional-chain.yml', '.github/workflows/rustfs-validation-tools.yml'}
+ and manifest.get('runId') == str(run['id']) and manifest.get('runAttempt') == str(run['run_attempt']),
+ 'C10 encoder is not bound to one pinned successful source build attempt')
+ helper = Path(__file__).with_name('tools') / 'heal_shared_version_fixture.rs'
+ require(candidate['source_sha'] == CANDIDATE and manifest.get('schemaVersion') == 1
+ and manifest.get('candidateCommit') == CANDIDATE and manifest.get('candidateSourceTree') == SOURCE_TREE
+ and manifest.get('candidateLockSha256') == CARGO_LOCK and manifest.get('built') is True
+ and manifest.get('sourceUnmodified') is True and manifest.get('target') == 'x86_64-unknown-linux-gnu'
+ and manifest.get('role') == ROLE and manifest.get('filesystemMutationPerformed') is False
+ and manifest.get('physicalByteEqualityClaimed') is False
+ and manifest.get('helperSourceSha256') == sha(helper)
+ and manifest.get('binarySha256') == sha(paths['heal-shared-fixture'])
+ and manifest.get('sharedLockSha256') == sha(paths['shared-Cargo.lock']),
+ 'C10 encoder changed its source/codec/build/artifact or claimed unobserved physical equality')
+ with paths['heal-shared-fixture'].open('rb') as stream:
+ header = stream.read(20)
+ require(header[:6] == b'\x7fELF\x02\x01' and header[18:20] == b'\x3e\x00', 'C10 encoder is not Linux x86_64 ELF')
+ log = paths['shared-fixture-tests.log'].read_text()
+ require(re.search(r'(?m)^running 3 tests\s*$', log)
+ and re.search(r'test result: ok\. 3 passed; 0 failed; 0 ignored;', log)
+ and all(len(re.findall(r'(?m)^test ' + re.escape(name) + r' \.\.\. ok\s*$', log)) == 1 for name in TESTS),
+ 'C10 codec build did not execute all three original transformation/refusal tests')
+ return {**manifest, 'toolsBuildRun': run['id'], 'toolsBuildAttempt': run['run_attempt'],
+ 'exactCodecTestsPassed': True, 'fleetPassClaimed': False}
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--directory', type=Path, required=True)
+ parser.add_argument('--candidate', type=Path, required=True)
+ args = parser.parse_args()
+ print(json.dumps(verify(args.directory, json.loads(args.candidate.read_text())), indent=2))
diff --git a/scripts/validation_heal/source.json b/scripts/validation_heal/source.json
new file mode 100644
index 00000000..cf75fd8a
--- /dev/null
+++ b/scripts/validation_heal/source.json
@@ -0,0 +1,55 @@
+{
+ "source_commit": "524eb5b32c3236497fdd8a9271df0494fa3d9974",
+ "files": [
+ {
+ "source_path": "scenarios/heal-recipes.json",
+ "target_path": "scripts/validation_heal/recipes.json",
+ "source_sha256": "4d81c83fa63c5b0720ff5f031ee595ea3c1f9e39f712e6d131bc50b9b12b164f"
+ },
+ {
+ "source_path": "scripts/heal_probe.py",
+ "target_path": "scripts/validation_heal/heal_probe.py",
+ "source_sha256": "3262c6744c6ae387c3f56095817740b3a34de7a1b9d34cb463b9167df68cef43"
+ },
+ {
+ "source_path": "scripts/heal_fault.py",
+ "target_path": "scripts/validation_heal/heal_fault.py",
+ "source_sha256": "c38ba8f9d129b67eacb8dd7920e534f6bbd40ec261870ebdde6823f067b98fd7"
+ },
+ {
+ "source_path": "scripts/heal_workload.py",
+ "target_path": "scripts/validation_heal/heal_workload.py",
+ "source_sha256": "216e9ca52545f1d52dd6f8fd63a7d634209e0780198355597d2489df61ece383"
+ },
+ {
+ "source_path": "scripts/heal_workload_journal.py",
+ "target_path": "scripts/validation_heal/heal_workload_journal.py",
+ "source_sha256": "a07fe6c130c60b523cca71d8f9d2c2fb4bd270eb052619a5a58d15373370041c"
+ },
+ {
+ "source_path": "docs/heal-operations.md",
+ "target_path": "docs/validation-heal-operations.md",
+ "source_sha256": "23f561ba81ff06c6450533ce1f8f9a3182a576504f5e6d6aca5494127d324bad"
+ },
+ {
+ "source_path": "scripts/prepare_heal_meta_inspector.py",
+ "target_path": "scripts/validation_heal/prepare_heal_meta_inspector.py",
+ "source_sha256": "e940b807f2ce37b8a04270913577221038808ff9e19454048251361fd9d16211"
+ },
+ {
+ "source_path": "tools/heal_meta_inspect.rs",
+ "target_path": "scripts/validation_heal/tools/heal_meta_inspect.rs",
+ "source_sha256": "f102a00ce5690fb6a286315b76f925cd27e0d2143c250a949e43da10d03faaab"
+ },
+ {
+ "source_path": "scripts/heal_signing.py",
+ "target_path": "scripts/validation_heal/heal_signing.py",
+ "source_sha256": "179394da660abad9c9a2f03b3eed7d4d46f5114940244f0ab8bbdb5e48f15537"
+ },
+ {
+ "source_path": "docs/heal-validation.md",
+ "target_path": "docs/validation-heal-contract.md",
+ "source_sha256": "c82cf8142e6400431eeeb58c936494ba2614a194d5bf92cde806cccf4aed3a85"
+ }
+ ]
+}
diff --git a/scripts/validation_heal/stale_rejoin.py b/scripts/validation_heal/stale_rejoin.py
new file mode 100644
index 00000000..ccc679ee
--- /dev/null
+++ b/scripts/validation_heal/stale_rejoin.py
@@ -0,0 +1,272 @@
+"""C06: real offline member, acknowledged transitions and corrected authority."""
+
+import copy
+import hashlib
+
+try:
+ from .heal_probe import Probe, canonical, payload, require
+ from .heal_workload import verify_seed
+ from .healthy import physical_content
+ from .fleet_node import PROCESS_FIELDS
+ from .shards import files, map_target, run_task, verify_cohort
+ from .markers import add_marker, verify_markers
+ from .scanner_repair import JournalWindow
+except ImportError:
+ from heal_probe import Probe, canonical, payload, require
+ from heal_workload import verify_seed
+ from healthy import physical_content
+ from fleet_node import PROCESS_FIELDS
+ from shards import files, map_target, run_task, verify_cohort
+ from markers import add_marker, verify_markers
+ from scanner_repair import JournalWindow
+
+
+BRANCHES = ("unversioned-overwrite", "delete-historical-version", "delete-marker")
+
+
+def version_id(value):
+ return "null" if value in {None, "", "null", "00000000-0000-0000-0000-000000000000"} else value
+
+
+def logical_metadata(metadata):
+ result = copy.deepcopy(metadata)
+ for version in result.get("versions", []):
+ version["versionId"] = version_id(version.get("versionId"))
+ version.pop("role", None)
+ for name in ("index", "checksums"):
+ version.get("erasure", {}).pop(name, None)
+ return result
+
+
+def identities(metadata):
+ return {(version_id(entry.get("versionId")), entry.get("deleted"), entry.get("isLatest"))
+ for entry in metadata.get("versions", [])}
+
+
+def transition(recorder, bucket, branch, key, expected):
+ manifest = copy.deepcopy(expected)
+ data = [entry for entry in manifest if entry["key"] == key and not entry["deleteMarker"]]
+ require(data and not any(entry["deleteMarker"] for entry in manifest if entry["key"] == key),
+ "stale transition must begin from acknowledged data without a current marker")
+ if branch == "unversioned-overwrite":
+ require(len(data) == 1 and data[0]["versionId"] == "null", "overwrite requires a never-versioned object")
+ nonce, size = 1, data[0]["payload"]["size"]
+ body = payload(f"{bucket}/{key}/{nonce}", size)
+ response = recorder.call("put_object", {"Bucket": bucket, "Key": key,
+ "ContentType": "application/octet-stream", "Metadata": {"heal-fixture": "DB", "nonce": "offline-overwrite"},
+ "Tagging": "suite=heal&fixture=DB"}, body)
+ require(response.get("VersionId") in {None, "null"}, "unversioned overwrite created an unexpected version")
+ data[0].update(payload={"size": size, "sha256": hashlib.sha256(body).hexdigest()}, payloadNonce=nonce)
+ return manifest, {"operation": "put_object", "newPayload": data[0]["payload"], "backupRestorationAllowed": False}
+ if branch == "delete-historical-version":
+ require(len(data) == 3 and not data[0]["isLatest"] and data[0]["versionId"] != "null", "delete requires a precise historical version")
+ removed = data[0]
+ response = recorder.call("delete_object", {"Bucket": bucket, "Key": key, "VersionId": removed["versionId"]})
+ require(response.get("VersionId") == removed["versionId"] and response.get("DeleteMarker") in {None, False}
+ and response.get("ResponseMetadata", {}).get("HTTPStatusCode") == 204, "historical DELETE lost its exact data-version ACK")
+ return [entry for entry in manifest if entry != removed], {"operation": "delete_object_version",
+ "deletedVersionId": removed["versionId"], "deletedDataDirFromOriginalRequired": True, "backupRestorationAllowed": False}
+ require(branch == "delete-marker" and all(entry["versionId"] != "null" for entry in data), "marker branch requires enabled versions")
+ response = recorder.call("delete_object", {"Bucket": bucket, "Key": key})
+ return add_marker(manifest, key, response), {"operation": "delete_object", "deleteMarkerId": response.get("VersionId"),
+ "backupRestorationAllowed": False}
+
+
+def authority_witness(before, authority, bucket, key, expected, target):
+ left, right = files(before), files(authority)
+ member = target["node"], target["disk"]
+ path, prefix = f"{bucket}/{key}/xl.meta", f"{bucket}/{key}/"
+ require({node: value["formats"] for node, value in before.items()} ==
+ {node: value["formats"] for node, value in authority.items()}, "stale transition changed disk UUID/slot/format")
+ require({identity: entry for identity, entry in left.items() if identity[:2] == member} ==
+ {identity: entry for identity, entry in right.items() if identity[:2] == member}, "offline member changed during the acknowledged transition")
+ require({identity: entry for identity, entry in left.items() if not identity[2].startswith(prefix)} ==
+ {identity: entry for identity, entry in right.items() if not identity[2].startswith(prefix)}, "transition changed a control or another key")
+ selected = [entry for entry in expected if entry["key"] == key]
+ wanted = {(entry["versionId"], entry["deleteMarker"], entry["isLatest"]) for entry in selected}
+ donors, normalized = [], None
+ for shard in target["cohort"]:
+ identity = shard["node"], shard["disk"], path
+ if identity[:2] == member: continue
+ metadata = right.get(identity, {}).get("metadata")
+ require(metadata and metadata.get("bucket") == bucket and metadata.get("key") == key
+ and identities(metadata) == wanted and len(metadata["versions"]) == len(wanted),
+ "online member lacks the full acknowledged version/marker authority")
+ logical = logical_metadata(metadata)
+ require(normalized is None or canonical(logical) == canonical(normalized), "online same-version metadata does not form one full authority")
+ normalized = logical
+ donors.append(identity[:2])
+ geometry = target["logical"]["erasure"]
+ read = geometry["data_blocks"]
+ write = read + int(read == geometry["parity_blocks"])
+ require(len(donors) == len(target["cohort"]) - 1 >= max(read, write), "one member loss cannot admit this layout's read/write quorum")
+ stale = right.get((*member, path), {}).get("metadata")
+ require(stale and canonical(logical_metadata(stale)) != canonical(normalized), "NOT_OBSERVED: rejoin member was not genuinely stale")
+ return {"readQuorum": read, "writeQuorum": write, "onlineAuthorities": len(donors), "member": list(member),
+ "staleMetadataSha256": right[(*member, path)]["sha256"], "logicalAuthority": normalized,
+ "offlineMemberAllOriginalFilesPreserved": True, "originalFormatsAndControlFilesPreserved": True}
+
+
+def physical_rejoin(authority, after, bucket, key, expected, target, witness):
+ left, right = files(authority), files(after)
+ path, prefix, member = f"{bucket}/{key}/xl.meta", f"{bucket}/{key}/", (target["node"], target["disk"])
+ require({node: value["formats"] for node, value in authority.items()} ==
+ {node: value["formats"] for node, value in after.items()}, "rejoin changed original format/disk/set identities")
+ # Only the stale member's selected key may gain/remove payload files. All
+ # existing quorum shards and every control remain exact independent oracles.
+ for identity, entry in left.items():
+ if identity[:2] == member and identity[2].startswith(prefix): continue
+ actual = right.get(identity)
+ if identity[2] == path:
+ require(actual and canonical(logical_metadata(actual.get("metadata", {}))) == canonical(witness["logicalAuthority"]),
+ "repair changed the committed online metadata authority")
+ else:
+ require(actual == entry, "stale repair lost or changed a quorum shard or control")
+ for identity in set(right) - set(left):
+ require(identity[:2] == member and identity[2].startswith(prefix), "stale repair created a file outside its exact member/key")
+ current = right.get((*member, path), {})
+ require(current.get("metadata") and current.get("sha256") != witness["staleMetadataSha256"]
+ and canonical(logical_metadata(current["metadata"])) == canonical(witness["logicalAuthority"]),
+ "stale member did not converge to the frozen ACK authority")
+ live_directories = {entry.get("dataDir") for entry in witness["logicalAuthority"]["versions"] if not entry.get("deleted")}
+ for identity, entry in left.items():
+ if identity[:2] == member and any(identity[2].startswith(prefix + data_dir + "/")
+ for data_dir in live_directories if data_dir) and identity[2].rsplit("/", 1)[-1].startswith("part."):
+ require(right.get(identity) == entry, "stale repair changed an original retained same-drive data shard")
+ cohorts = []
+ for entry in expected:
+ if entry["key"] != key or entry["deleteMarker"]: continue
+ cohort = map_target(after, bucket, key, "data", 1, entry["versionId"], latest_required=False)
+ require(cohort["logical"]["size"] == entry["payload"]["size"], "rejoined payload metadata differs from its ACK length")
+ cohorts.append(cohort)
+ require(cohorts, "rejoined key lost every retained acknowledged data version")
+ return cohorts, {"staleMemberMatchesFrozenAuthority": True, "sameDiskUUIDAndSetSlot": True,
+ "controlsAndExistingQuorumShardBytesUnchanged": True, "backupRestoreUsed": False,
+ "newGenerationProof": "frozen quorum metadata, streaming protected-shard checks and independent ACK plaintext"}
+
+
+def responsibility(status, bucket, key, expected, *, deleted=None):
+ outcome = status.get("outcome") or {}
+ wanted = {(entry["key"], entry["versionId"]): entry for entry in expected if entry["key"] == key}
+ require(wanted and status.get("summary") == "finished" and status.get("truncated", False) is False
+ and outcome.get("execution", {}).get("state") == "completed" and outcome.get("coverage") == "complete"
+ and outcome.get("objectsTruncated") is False, "stale task did not preserve complete terminal responsibility")
+ allowed = set(wanted) | ({(key, deleted)} if deleted else set())
+ seen, repairs, incarnation = set(), set(), None
+ objects = outcome.get("objects", [])
+ for receipt in objects:
+ identity = receipt.get("identity", {})
+ pair = identity.get("object"), version_id(identity.get("versionId"))
+ require(pair in allowed and pair not in seen and identity.get("bucket") == bucket
+ and type(identity.get("poolIndex")) is int and identity["poolIndex"] == 0
+ and type(identity.get("setIndex")) is int and identity["setIndex"] == 0
+ and isinstance(identity.get("bucketIncarnationId"), str) and identity["bucketIncarnationId"],
+ "stale task mixed wrong/duplicate version, incarnation or pool/set responsibility")
+ incarnation = incarnation or identity["bucketIncarnationId"]
+ require(incarnation == identity["bucketIncarnationId"], "stale receipts mixed bucket incarnations")
+ state = receipt.get("disposition", {}).get("state")
+ if pair == (key, deleted):
+ require(state in {"repaired", "authoritatively_absent"}, "deleted historical version was falsely reported live or unknown")
+ else:
+ require(state in {"repaired", "metadata_healthy" if wanted[pair]["deleteMarker"] else "verified_healthy"},
+ "stale version lacks an explicit correct repair/healthy disposition")
+ if state == "repaired": repairs.add(pair)
+ seen.add(pair)
+ require(set(wanted) <= seen and repairs, "NOT_OBSERVED: original task did not cover and repair the stale ACK namespace")
+ counters = outcome.get("counters", {})
+ counts = {"processed": len(seen), "healed": len(repairs), "unchanged": len(seen) - len(repairs),
+ "failed": 0, "skipped": 0, "unknown": 0, "attemptFailures": 0, "overflowed": False}
+ require(all(type(counters.get(name)) is type(value) and counters[name] == value for name, value in counts.items()),
+ "stale outcome counters hid errors or misrepresented actual responsibility")
+ return {"completeRetainedAndOptionalDeletedVersionReceipts": True, "repairedIdentities": sorted(repairs),
+ "bucketIncarnationId": incarnation, "counters": counters}
+
+
+def return_original_member(fleet, node, prefix):
+ """Release the owned permission gate without triggering startup recovery."""
+ processes = {peer: fleet.remote(peer, "process") for peer in fleet.nodes}
+ release = fleet.record(prefix + "-original-member-rejoin", fleet.remote(node, "drive-gate-release-live"))
+ require(release.get("sameOriginalProcess") is True and release.get("serviceRestarted") is False,
+ "member rejoin restarted or replaced the original process")
+ fleet.drive_gates.remove(node)
+ rejoined = {peer: fleet.remote(peer, "process") for peer in fleet.nodes}
+ require(all(all(processes[peer].get(name) == rejoined[peer].get(name) for name in PROCESS_FIELDS) for peer in fleet.nodes),
+ "live rejoin changed an original peer process/unit/candidate/environment")
+ fleet.record(prefix + "-live-rejoin-process-proof", {"before": processes, "after": rejoined,
+ "allOriginalProcessesPreserved": True})
+
+
+def run_branch(recorder, fleet, bucket, branch, expected, active, admissions, contract):
+ # Lazy imports keep the shared version executor's dependency graph acyclic.
+ try:
+ from .versions import verify_version_s3, public_version_id
+ except ImportError:
+ from versions import verify_version_s3, public_version_id
+ require(contract.get("recognized") is True and contract.get("writeQuorumRule") == "data-plus-one-if-equal-parity",
+ "stale rejoin requires a recognized candidate read/write policy")
+ key = "objects/0003.bin" if branch == "delete-marker" else "objects/0001.bin"
+ selected = [entry for entry in expected if entry["key"] == key and not entry["deleteMarker"]]
+ require(selected, "stale branch lost its target ACK")
+ before = fleet.snapshot("stale-original-" + branch, quiescent=True)
+ target = map_target(before, bucket, key, "data", 1, public_version_id(selected[0]["versionId"]), latest_required=False)
+ # Pick an actual member from decoded geometry, keeping coordinator node1
+ # separate on H2. Four disks are one lock host, not four independent peers.
+ shard = next(item for item in reversed(target["cohort"]) if item["node"] == fleet.nodes[-1])
+ target = {**target, **shard}
+ verify_cohort(fleet, target, "stale-baseline-bitrot-" + branch)
+ journal = JournalWindow(fleet, "stale-authority-" + branch)
+ node = target["node"]
+ fleet.drive_gates.add(node)
+ fleet.record("stale-member-isolated", fleet.remote(node, "drive-gate", disk=target["disk"], mode="permission-denied"))
+ fleet.start()
+ denial = fleet.record("stale-member-service-denial", fleet.remote(node, "drive-gate-probe"))
+ require(denial.get("readProbe", {}).get("errno") == 13 and denial.get("errno") == 13
+ and denial.get("dacBypassRemoved") is True, "offline member lacks actual service-namespace read and write refusal")
+ manifest, ack = transition(recorder, bucket, branch, key, expected)
+ fleet.record("stale-transition-contract", {"branch": branch, **ack})
+ baseline = fleet.record("stale-online-ack-inventory", recorder.probe.inventory(bucket, unversioned_current_tags=branch == "unversioned-overwrite"))
+ fleet.record("stale-online-ack-proof", {"expected": manifest, **verify_seed(manifest, baseline)})
+ # Restarting the fleet here can perform startup reconciliation before the
+ # manual token. ACKs have settled; freeze two unchanged live observations,
+ # then release only the exact permission gate in the original process.
+ authority = fleet.snapshot("stale-online-authorities")
+ require(physical_content(authority) == physical_content(fleet.snapshot("stale-online-authorities-stable")),
+ "NOT_OBSERVED: offline ACK authority is not physically stable before live rejoin")
+ witness = fleet.record("stale-generation-authority-proof", authority_witness(before, authority, bucket, key, manifest, target))
+ return_original_member(fleet, node, "stale")
+ require(physical_content(authority) == physical_content(fleet.snapshot("stale-before-manual")),
+ "ATTRIBUTION_NOT_OBSERVED: stale namespace changed before its manual token")
+ status = run_task(recorder.probe, fleet, {"bucket": bucket, "key": key}, 2, active,
+ option_overrides={"recursive": True}, admissions=admissions)
+ fleet.stop()
+ after = fleet.snapshot("stale-after-manual", quiescent=True)
+ cohorts, physical = physical_rejoin(authority, after, bucket, key, manifest, target, witness)
+ streaming = [verify_cohort(fleet, cohort, "stale-repaired-streaming") for cohort in cohorts]
+ receipts = responsibility(status, bucket, key, manifest, deleted=ack.get("deletedVersionId"))
+ journal.capture()
+ fleet.start()
+ s3 = verify_version_s3(recorder.probe, fleet, bucket, manifest, baseline, "stale-s3-after", fixture="DB" if branch == "unversioned-overwrite" else "DV")
+ deleted_probes = []
+ for endpoint in recorder.probe.binding["endpoints"]:
+ peer = Probe(recorder.probe.binding, endpoint)
+ inventory = fleet.record("stale-all-peer-inventory", {"endpoint": endpoint, **peer.inventory(bucket,
+ unversioned_current_tags=branch == "unversioned-overwrite")})
+ verify_seed(manifest, inventory)
+ if ack.get("deletedVersionId"):
+ denied = fleet.record("stale-deleted-version-check", peer.s3("get_object", {"Bucket": bucket, "Key": key, "VersionId": ack["deletedVersionId"]}))
+ response = denied.get("response") or {}
+ require(denied.get("acknowledged") is False and denied.get("resultUnknown") is False and "payload" not in denied
+ and response.get("ResponseMetadata", {}).get("HTTPStatusCode") == 404
+ and response.get("Error", {}).get("Code") in {"NoSuchKey", "NoSuchVersion"}, "deleted historical version was resurrected or its absence is unproven")
+ deleted_probes.append({"endpoint": endpoint, "deletedVersionId": ack["deletedVersionId"], "absent": True})
+ if branch == "unversioned-overwrite":
+ current = fleet.record("stale-current-get", peer.s3("get_object", {"Bucket": bucket, "Key": key}))
+ wanted = next(entry for entry in manifest if entry["key"] == key)
+ require(current.get("acknowledged") is True and current.get("payload") == wanted["payload"], "latest overwrite regressed to the old plaintext")
+ if branch == "delete-marker": s3["markerIdentity"] = verify_markers(recorder.probe, fleet, bucket, key, manifest)
+ fleet.stop()
+ require(physical_content(after) == physical_content(fleet.snapshot("stale-after-s3", quiescent=True)), "S3 reads caused repair after manual convergence proof")
+ fleet.start()
+ return manifest, baseline, {"verdict": "PASS", "transition": ack, "authority": witness, "physical": physical,
+ "streaming": streaming, "responsibility": receipts, "s3": s3, "deletedVersionProbes": deleted_probes,
+ "generationSelectionJournalRetained": True, "manualTaskId": admissions[-1]["clientToken"]}
diff --git a/scripts/validation_heal/status.py b/scripts/validation_heal/status.py
new file mode 100644
index 00000000..45c7405c
--- /dev/null
+++ b/scripts/validation_heal/status.py
@@ -0,0 +1,338 @@
+#!/usr/bin/env python3
+"""J01: real startup/disabled/missing-peer truth through raw API and rc."""
+
+import argparse
+from concurrent.futures import ThreadPoolExecutor
+from datetime import datetime, timedelta, timezone
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import secrets
+import signal
+import subprocess
+import sys
+import tempfile
+import threading
+import time
+from urllib import request
+from urllib.error import URLError
+
+try:
+ from .heal_probe import Probe, NoRedirect, require, save, compare_inventories
+ from .heal_workload import Recorder, seed, verify_seed
+ from .healthy import Fleet, physical_content, validate_topology
+ from .inspector import verify as verify_inspector
+ from .matrix import select
+ from .shards import SIZES
+except ImportError:
+ from heal_probe import Probe, NoRedirect, require, save, compare_inventories
+ from heal_workload import Recorder, seed, verify_seed
+ from healthy import Fleet, physical_content, validate_topology
+ from inspector import verify as verify_inspector
+ from matrix import select
+ from shards import SIZES
+
+
+CASE = "heal.status-partial-disabled-unknown"
+BRANCHES = ("enabled-idle", "disabled", "uninitialized", "missing-peer", "recovered-complete")
+
+
+class TimedProbe(Probe):
+ def __init__(self, *args, timeout=8, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.timeout = timeout
+
+ def _open_admin(self, outgoing):
+ return request.build_opener(request.ProxyHandler({}), NoRedirect()).open(outgoing, timeout=self.timeout)
+
+
+def validate_background(value, branch, peers):
+ coverage = value.get("coverage") or {}
+ require(all(type(coverage.get(name)) is int and 0 <= coverage[name] <= peers
+ for name in ("expected", "responded", "unknown")) and coverage["expected"] == peers
+ and coverage["responded"] + coverage["unknown"] == peers,
+ "background status lacks balanced exact peer coverage")
+ state = value.get("state")
+ if branch == "missing-peer":
+ require(value.get("clusterStatusComplete") is False and coverage["responded"] == peers - 1
+ and coverage["unknown"] == 1 and state in {"degraded", "active"}
+ and isinstance(coverage.get("reasons"), list) and coverage["reasons"],
+ "missing peer was hidden as complete/idle or lacks its reason")
+ else:
+ wanted = "disabled" if branch == "disabled" else "uninitialized" if branch == "uninitialized" else "idle"
+ require(state == wanted and value.get("clusterStatusComplete") is True and coverage["responded"] == peers
+ and coverage["unknown"] == 0 and coverage.get("reasons") == [],
+ "background runtime state or complete coverage differs from the actual fixture")
+ for name in ("healQueueLength", "healActiveTasks"):
+ require(type(value.get(name)) is int and value[name] == 0, "status fixture has unresolved Heal work")
+ operations = value.get("healOperations") or {}
+ require(all(type(operations.get(name)) is int and operations[name] == 0
+ for name in ("queueLength", "activeTasks", "retryingTasks")), "status fixture still has queued, active or retrying responsibility")
+
+
+def validate_replacement(value, branch):
+ cluster = value.get("cluster") or {}
+ require(value.get("contractVersion") == 2 and value.get("scope") == "cluster_survivor_disks"
+ and isinstance(cluster.get("records"), list) and not cluster["records"],
+ "fresh status fixture lacks replacement contract or has residual records")
+ uncertain = branch in {"missing-peer", "disabled", "uninitialized"}
+ require(cluster.get("definitive") is (not uncertain)
+ and (isinstance(cluster.get("reason"), str) and bool(cluster["reason"]) if uncertain else cluster.get("reason") is None),
+ "replacement status erased uncertainty or claimed a definitive cluster")
+
+
+def validate_cli(json_result, text_result, background, branch, peers):
+ require(json_result.get("exitCode") == 0 and text_result.get("exitCode") == 0,
+ "rc status did not complete successfully")
+ value = json_result.get("json")
+ require(isinstance(value, dict), "rc omitted parseable JSON status")
+ allowed = {"degraded", "active", "unknown"} if branch == "missing-peer" else {background["state"]}
+ require(value.get("state") in allowed, "rc converted actual runtime state to a different completion claim")
+ if branch == "missing-peer":
+ require(value.get("clusterStatusComplete") is not True, "rc falsely completed a partial cluster")
+ # When rc exposes coverage, all counters must agree with the raw API.
+ if "coverage" in value:
+ require(value["coverage"] == background["coverage"], "rc peer coverage differs from raw Admin")
+ if "clusterStatusComplete" in value:
+ require(value["clusterStatusComplete"] is background["clusterStatusComplete"], "rc completeness differs from raw Admin")
+ text = text_result.get("stdout", "")
+ if branch in {"missing-peer", "disabled", "uninitialized"}:
+ require("Heal Status: Idle" not in text and not re.search(r"(?im)^\s*(?:status|heal status)\s*:\s*(?:completed|complete|idle)\s*$", text),
+ "rc text presented partial/disabled/uninitialized status as completed/idle")
+ require(bool(text.strip()), "rc omitted its human-readable status")
+ return {"rawAndRcRuntimeAgree": True, "expectedPeers": peers, "rawState": background["state"], "rcState": value["state"]}
+
+
+class Cli:
+ def __init__(self, binary, binding, access, secret):
+ self.binary, self.binding = binary, binding
+ self.folder = tempfile.TemporaryDirectory(prefix="validation-heal-rc-")
+ root = Path(self.folder.name)
+ root.chmod(0o700)
+ text = "schema_version = 1\n"
+ for alias, endpoint in binding["endpoints"].items():
+ text += "[[aliases]]\n" + f"name = {json.dumps(alias)}\nendpoint = {json.dumps(endpoint)}\n"
+ text += f"access_key = {json.dumps(access)}\nsecret_key = {json.dumps(secret)}\nbucket_lookup = \"path\"\n"
+ text += "[aliases.retry]\nmax_attempts = 0\ninitial_backoff_ms = 100\nmax_backoff_ms = 1000\n"
+ descriptor = os.open(root / "config.toml", os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
+ with os.fdopen(descriptor, "w") as stream: stream.write(text)
+ self.env = {**os.environ, "RC_CONFIG_DIR": str(root), "NO_PROXY": "*", "HTTP_PROXY": "", "HTTPS_PROXY": "", "ALL_PROXY": ""}
+
+ def read(self, endpoint, *, as_json, timeout=10):
+ args = [str(self.binary), *(["--json"] if as_json else []), "--no-color", "admin", "heal", "status", endpoint]
+ try:
+ result = subprocess.run(args, env=self.env, capture_output=True, text=True, timeout=timeout)
+ except subprocess.TimeoutExpired as exc:
+ return {"exitCode": None, "timeout": True, "stdout": (exc.stdout or b"").decode(errors="replace") if isinstance(exc.stdout, bytes) else exc.stdout,
+ "stderr": "rc status deadline", "json": None}
+ try: value = json.loads(result.stdout)
+ except ValueError: value = None
+ return {"exitCode": result.returncode, "stdout": result.stdout, "stderr": result.stderr, "json": value}
+
+ def close(self):
+ self.folder.cleanup()
+ require(not Path(self.folder.name).exists(), "private rc credential configuration remains")
+
+
+def observations(fleet, binding, cli, endpoints, *, timeout=8):
+ observed = {}
+ for endpoint in endpoints:
+ probe = TimedProbe(binding, endpoint, timeout=timeout)
+ def raw(path, method="POST"):
+ try: return probe.admin(path, [], b"", method=method)
+ except Exception as exc: return {"httpStatus": None, "error": str(exc)}
+ observed[endpoint] = {"background": raw("/rustfs/admin/v3/background-heal/status"),
+ "replacement": raw("/rustfs/admin/v4/heal/replacement-recovery", "GET"),
+ "rcJson": cli.read(endpoint, as_json=True, timeout=timeout),
+ "rcText": cli.read(endpoint, as_json=False, timeout=timeout)}
+ return fleet.record("status-observations", observed)
+
+
+def validate_observations(observed, branch, peers):
+ proofs = {}
+ for endpoint, value in observed.items():
+ for name in ("background", "replacement"):
+ require(value[name].get("httpStatus") == 200 and isinstance(value[name].get("response"), dict),
+ "status observation lacks a successful raw Admin response")
+ background = value["background"]["response"]
+ validate_background(background, branch, peers)
+ validate_replacement(value["replacement"]["response"], branch)
+ proofs[endpoint] = validate_cli(value["rcJson"], value["rcText"], background, branch, peers)
+ require(bool(proofs), "status observation has no actual endpoint")
+ return proofs
+
+
+def reject_unsafe_completion(observed):
+ for value in observed.values():
+ background = value["background"].get("response")
+ if not isinstance(background, dict): continue
+ partial = background.get("clusterStatusComplete") is False
+ uncertain = partial or background.get("state") in {"disabled", "uninitialized", "degraded"}
+ require(not partial or background.get("state") != "idle", "partial cluster falsely reported idle during convergence")
+ if not uncertain: continue
+ cli = value["rcJson"].get("json")
+ if isinstance(cli, dict):
+ require(cli.get("state") not in {"idle", "complete", "completed"}, "rc falsely reported uncertain status as completed/idle")
+ require(not partial or cli.get("clusterStatusComplete") is not True, "rc falsely completed a partial cluster")
+ text = value["rcText"].get("stdout") or ""
+ require("Heal Status: Idle" not in text and not re.search(r"(?im)^\s*(?:status|heal status)\s*:\s*(?:completed|complete|idle)\s*$", text),
+ "rc text erased status uncertainty during convergence")
+
+
+def stable_status(fleet, binding, cli, branch, endpoints):
+ deadline = time.monotonic() + 180
+ while True:
+ observed = observations(fleet, binding, cli, endpoints)
+ reject_unsafe_completion(observed)
+ try: return validate_observations(observed, branch, len(binding["endpoints"]))
+ except ValueError:
+ if time.monotonic() >= deadline: raise
+ time.sleep(0.5)
+
+
+def watch_startup(fleet, binding, cli, done):
+ deadline, result = time.monotonic() + 30, None
+ while not done.is_set() and time.monotonic() < deadline:
+ # Bracket CLI/replacement reads with the same raw uninitialized state;
+ # a transport failure or a sample from an already initialized service
+ # cannot serve as startup-window evidence.
+ observed = observations(fleet, binding, cli, ["node1"], timeout=0.7)
+ reject_unsafe_completion(observed)
+ try:
+ proof = validate_observations(observed, "uninitialized", len(binding["endpoints"]))
+ after = fleet.record("startup-after-cli", TimedProbe(binding, "node1", timeout=0.7).admin(
+ "/rustfs/admin/v3/background-heal/status", [], b""))
+ require(after.get("httpStatus") == 200 and isinstance(after.get("response"), dict), "startup after-CLI raw status unavailable")
+ validate_background(after["response"], "uninitialized", len(binding["endpoints"]))
+ result = {"verdict": "PASS", "proof": proof, "rawBracketedCliWindow": True}
+ break
+ except Exception:
+ time.sleep(0.1)
+ return result or {"verdict": "NOT_OBSERVED", "reason": "full raw/rc uninitialized startup window was not witnessed; transport failure is not proof"}
+
+
+def run_topology(args, topology, nodes, profile):
+ directory = args.evidence / topology / profile
+ directory.mkdir(parents=True)
+ run_id = f"gh-{os.environ['GITHUB_RUN_ID']}-{os.environ.get('GITHUB_RUN_ATTEMPT', '1')}-{topology.lower()}"
+ fleet = Fleet(nodes, directory, run_id, args.tool_directory, runtime_profile=profile)
+ candidate = json.loads(args.candidate.read_text())
+ if args.tool_directory: fleet.record("inspector-build", verify_inspector(args.tool_directory, candidate))
+ bucket = "rv-heal-" + hashlib.sha256(run_id.encode()).hexdigest()[:16]
+ access, secret = secrets.token_hex(16), secrets.token_hex(24)
+ os.environ.update(HEAL_ACCESS_KEY=access, HEAL_SECRET_KEY=secret)
+ binding = {"schemaVersion": 1, "runId": run_id, "jumpHostOnly": True, "exclusiveHealService": True,
+ "leaseExpiresAt": (datetime.now(timezone.utc) + timedelta(minutes=30)).isoformat(), "bucketPrefix": bucket,
+ "endpoints": {f"node{index}": f"http://rustfs-node{index}:9000" for index in range(1, len(nodes) + 1)},
+ "defaultEndpoint": "node1", "candidateCommit": candidate["source_sha"], "harnessCommit": args.testing_commit,
+ "candidatePackageSha256": candidate["package_sha256"], "candidateBinarySha256": args.binary_sha,
+ "rcBinarySha256": hashlib.sha256(args.rc_bin.read_bytes()).hexdigest(), "maxInventoryObjects": 512,
+ "maxInventoryBytes": 512 * 1024**2}
+ fleet.record("binding", binding)
+ result = {"topology": topology, "runtimeProfile": profile, "branches": {}, "verdict": "RUNNING", "cleanup": "PENDING"}
+ cli, pool, future, done = None, None, None, threading.Event()
+ try:
+ cli = Cli(args.rc_bin, binding, access, secret)
+ if profile == "manual":
+ pool = ThreadPoolExecutor(max_workers=1)
+ def before_start():
+ nonlocal future
+ future = pool.submit(watch_startup, fleet, binding, cli, done)
+ fleet.deploy(args.binary_sha, bucket, access, secret, before_start=before_start)
+ done.set()
+ result["branches"]["uninitialized"] = future.result(timeout=10)
+ else:
+ fleet.deploy(args.binary_sha, bucket, access, secret)
+ physical = fleet.snapshot("deployed-runtime-and-topology")
+ fleet.record("topology-proof", validate_topology(physical, topology))
+ recorder = Recorder(Probe(binding), directory / "fixture")
+ expected = seed(recorder, bucket, "DB", 128, SIZES)
+ baseline = fleet.record("s3-baseline", recorder.probe.inventory(bucket, unversioned_current_tags=True))
+ fleet.record("fixture-manifest", {"expected": expected, **verify_seed(expected, baseline)})
+ before = fleet.snapshot("physical-baseline")
+ branch = "disabled" if profile == "disabled" else "enabled-idle"
+ result["branches"][branch] = {"verdict": "PASS", "proof": stable_status(fleet, binding, cli, branch, binding["endpoints"])}
+ if profile == "manual":
+ if topology == "H2":
+ node = nodes[-1]
+ stopped = fleet.record("missing-peer-stopped", fleet.remote(node, "stop"))
+ require(stopped.get("quiescent") is True and stopped.get("service", {}).get("MainPID") == "0", "peer stop lacks actual process exit")
+ try:
+ with request.build_opener(request.ProxyHandler({})).open(binding["endpoints"]["node4"] + "/health/ready", timeout=2):
+ raise ValueError("stopped peer still accepts requests")
+ except URLError as exc:
+ require(isinstance(exc.reason, ConnectionRefusedError), "peer outage is not a witnessed endpoint refusal")
+ fleet.record("missing-peer-refused", {"connectionRefused": True, "endpoint": "node4"})
+ result["branches"]["missing-peer"] = {"verdict": "PASS", "proof": stable_status(fleet, binding, cli, "missing-peer", ["node1", "node2", "node3"])}
+ fleet.record("same-peer-restarted", fleet.remote(node, "start"))
+ fleet.wait_ready()
+ else:
+ result["branches"]["missing-peer"] = {"verdict": "NOT_APPLICABLE", "reason": "source J01 step 2 explicitly requires H2 for stopping one peer while querying survivors"}
+ fleet.stop()
+ fleet.start()
+ result["branches"]["recovered-complete"] = {"verdict": "PASS", "proof": stable_status(fleet, binding, cli, "recovered-complete", binding["endpoints"])}
+ fleet.stop()
+ require(physical_content(before) == physical_content(fleet.snapshot("physical-after-status", quiescent=True)), "status/process observations changed object files or format identities")
+ fleet.start()
+ after = fleet.record("s3-after-status", recorder.probe.inventory(bucket, unversioned_current_tags=True))
+ fleet.record("status-full-fixture-proof", compare_inventories(baseline, after))
+ wanted = {"disabled"} if profile == "disabled" else set(BRANCHES) - {"disabled"}
+ result["verdict"] = "PASS" if set(result["branches"]) == wanted and all(
+ value["verdict"] == "PASS" or branch == "missing-peer" and topology == "H1" and value["verdict"] == "NOT_APPLICABLE"
+ for branch, value in result["branches"].items()) else "FAIL"
+ except (Exception, KeyboardInterrupt) as exc:
+ result.update(verdict="FAIL", failure={"type": type(exc).__name__, "message": str(exc)})
+ finally:
+ done.set()
+ if pool: pool.shutdown(wait=True)
+ cli_clean = True
+ if cli:
+ try: cli.close()
+ except Exception as exc:
+ cli_clean = False
+ fleet.record("rc-config-cleanup-error", {"error": str(exc)})
+ clean = fleet.cleanup()
+ result["cleanup"] = "PASS" if clean and cli_clean else "FAIL"
+ fleet.record("topology-result", result)
+ return result
+
+
+def main():
+ def interrupted(signum, frame):
+ raise KeyboardInterrupt(f"status workflow interrupted by signal {signum}")
+ signal.signal(signal.SIGTERM, interrupted)
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--candidate", type=Path, required=True)
+ parser.add_argument("--case-id", choices=[CASE], required=True)
+ parser.add_argument("--testing-commit", required=True)
+ parser.add_argument("--binary-sha", required=True)
+ parser.add_argument("--rc-bin", type=Path, required=True)
+ parser.add_argument("--evidence", type=Path, required=True)
+ parser.add_argument("--tool-directory", type=Path)
+ args = parser.parse_args()
+ require(bool(re.fullmatch(r"[0-9a-f]{64}", args.binary_sha)), "invalid candidate binary digest")
+ require(bool(re.fullmatch(r"[1-9][0-9]*", os.environ.get("GITHUB_RUN_ID", ""))), "workflow fleet identity required")
+ require(select(CASE)[0]["variants"] == list(BRANCHES), "status executor omitted a source branch")
+ nodes = os.environ.get("RUSTFS_NODES", "vm000 vm001 vm002 vm003").split()
+ require(len(nodes) >= 4 and len(set(nodes)) == len(nodes) and all(re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", node) for node in nodes), "four current Heal nodes required")
+ results = []
+ try:
+ for topology, selected in (("H1", nodes[:1]), ("H2", nodes[:4])):
+ for profile in ("manual", "disabled"):
+ results.append(run_topology(args, topology, selected, profile))
+ if results[-1]["cleanup"] != "PASS": break
+ if results[-1]["cleanup"] != "PASS": break
+ finally:
+ complete = len(results) == 4 and all(value["verdict"] == "PASS" and value["cleanup"] == "PASS" for value in results)
+ save(args.evidence / "healthy-summary.json", {"caseId": CASE, "results": results, "complete": complete,
+ "cleanup": "PASS" if results and all(value["cleanup"] == "PASS" for value in results) else "FAIL"})
+ require(complete, "J01 requires every applicable raw/rc startup, disabled and peer-recovery branch")
+
+
+if __name__ == "__main__":
+ try: main()
+ except Exception as exc:
+ print(f"Heal status validation failed: {exc}", file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_heal/status_keepalive.py b/scripts/validation_heal/status_keepalive.py
new file mode 100644
index 00000000..30f714dd
--- /dev/null
+++ b/scripts/validation_heal/status_keepalive.py
@@ -0,0 +1,155 @@
+#!/usr/bin/env python3
+"""Opt-in same-token status transport; no redirects, proxy or request retries."""
+from contextlib import contextmanager
+import http.client
+import threading
+from urllib.parse import parse_qsl, urlsplit, quote
+import uuid
+
+try:
+ from .heal_probe import CLOCK_DOMAIN, Probe, observation_phase, require
+except ImportError:
+ from heal_probe import CLOCK_DOMAIN, Probe, observation_phase, require
+
+
+MAX_STATUS_OBSERVATIONS = 1025 # One warmup plus the existing 1024-sample race budget.
+
+
+class KeepAliveStatusProbe(Probe):
+ @contextmanager
+ def status_session(self, token, path):
+ require(isinstance(token, str) and str(uuid.UUID(token)) == token,
+ 'canonical status token required')
+ require(path == '/rustfs/admin/v3/heal/' + quote(self.binding['bucketPrefix'], safe=''),
+ 'status session requires the original owned bucket path')
+ require(getattr(self, '_status', None) is None, 'nested status session prohibited')
+ endpoint = urlsplit(self.endpoint)
+ connection_type = http.client.HTTPSConnection if endpoint.scheme == 'https' else http.client.HTTPConnection
+ state = dict(token=token, path=path, owner=threading.get_ident(), poisoned=False, requests=0,
+ transport='direct-http-keepalive', implicitRetriesAllowed=False,
+ foregroundDelayInjected=False, observationSchemaVersion=1,
+ sessionId=str(uuid.uuid4()), clockDomain=CLOCK_DOMAIN,
+ observationBudget=MAX_STATUS_OBSERVATIONS, observations=[],
+ connectionAttempts=0, connectionsOpened=0)
+ probe = self
+
+ class ObservedConnection(connection_type):
+ def connect(connection_self):
+ state['connectionAttempts'] += 1
+ with observation_phase(probe._status_observation, 'connectionEstablishment'):
+ super().connect()
+ state['connectionsOpened'] += 1
+ connection_self.observed_socket = connection_self.sock
+ connection_self.observed_id = f"{state['sessionId']}:{state['connectionsOpened']}"
+
+ connection = ObservedConnection(endpoint.hostname, endpoint.port, timeout=30)
+ self._status_observation = None
+ self._status = (connection, state)
+ try:
+ yield state
+ finally:
+ connection.close()
+ self._status = None
+ self._status_observation = None
+
+ @contextmanager
+ def status_observation(self):
+ session = getattr(self, '_status', None)
+ if session is None:
+ yield None
+ return
+ connection, state = session
+ observation = None
+ owns_observation = False
+ try:
+ require(threading.get_ident() == state['owner'], 'status session thread changed')
+ require(not state['poisoned'], 'status session poisoned; no implicit retry')
+ observation = self._status_observation
+ if observation is None:
+ require(len(state['observations']) < state['observationBudget'], 'status observation budget exhausted')
+ observation = dict(sessionId=state['sessionId'], sequence=len(state['observations']) + 1,
+ clockDomain=CLOCK_DOMAIN, phases={}, requestWriteCompleted=False,
+ responseObserved=False, responsePersisted=False)
+ state['observations'].append(observation)
+ self._status_observation = observation
+ owns_observation = True
+ yield observation
+ except BaseException as exc:
+ if observation is not None:
+ observation.setdefault('failureType', type(exc).__name__)
+ state['poisoned'] = True
+ state.setdefault('failureType', type(exc).__name__)
+ connection.close()
+ raise
+ finally:
+ if owns_observation:
+ self._status_observation = None
+
+ def observation_for(self, sample):
+ session = getattr(self, '_status', None)
+ require(session is not None, 'status observation session required')
+ _, state = session
+ require(threading.get_ident() == state['owner'] and not state['poisoned'], 'status observer unavailable')
+ require(sample.get('clockDomain') == state['clockDomain'], 'status observation clock changed')
+ require(sample.get('query') == [('clientToken', state['token'])], 'status observation token changed')
+ reference = sample.get('statusObservation') or {}
+ require(type(reference.get('sequence')) is int
+ and reference == dict(sessionId=state['sessionId'], sequence=len(state['observations']))
+ and bool(state['observations']), 'status observation reference changed')
+ return state['observations'][-1]
+
+ def _open_admin(self, outgoing):
+ session = getattr(self, '_status', None)
+ if session is None:
+ return super()._open_admin(outgoing)
+ connection, state = session
+ target = urlsplit(outgoing.full_url)
+ endpoint = urlsplit(self.endpoint)
+ require(threading.get_ident() == state['owner'], 'status session thread changed')
+ require(not state['poisoned'], 'status session poisoned; no implicit retry')
+ require((target.scheme, target.netloc) == (endpoint.scheme, endpoint.netloc)
+ and target.path == state['path'] and not target.fragment
+ and parse_qsl(target.query, keep_blank_values=True) == [('clientToken', state['token'])]
+ and outgoing.get_method() == 'POST' and outgoing.data == b'',
+ 'keepalive permits only the original token status; no control mutation')
+ state['requests'] += 1
+ observation = self._status_observation
+ try:
+ opened_before = state['connectionsOpened']
+ with observation_phase(observation, 'requestWrite'):
+ connection.request('POST', target.path + '?' + target.query, body=b'',
+ headers=dict(outgoing.header_items()))
+ if observation is not None:
+ observation['requestWriteCompleted'] = True
+ sock = connection.sock
+ require(sock is not None and sock is connection.observed_socket,
+ 'status socket identity unavailable after request write')
+ observation.update(transport=dict(
+ requestSequence=state['requests'], connectionId=connection.observed_id,
+ newConnection=state['connectionsOpened'] > opened_before,
+ socketDescriptor=sock.fileno(), localAddress=list(sock.getsockname()),
+ peerAddress=list(sock.getpeername())))
+ with observation_phase(observation, 'responseHeaders'):
+ response = connection.getresponse()
+ if observation is not None:
+ observation['transport']['serverWillClose'] = response.will_close
+ # Probe owns the bounded full-body read and records the same timing,
+ # headers and response as its default urllib path. Closing a fully
+ # consumed HTTPResponse retains a server-supported keepalive socket.
+ response.code = response.status
+ if 300 <= response.status < 400:
+ response.close()
+ raise ValueError('Admin redirects are prohibited')
+ return response
+ except BaseException:
+ state['poisoned'] = True
+ connection.close()
+ raise
+
+ def admin(self, *args, **kwargs):
+ with self.status_observation() as observation:
+ provided = kwargs.pop('observation', None)
+ require(provided is None or provided is observation, 'status observation owner changed')
+ require(observation is None or 'callStartedMonotonicNs' not in observation,
+ 'status observation already invoked')
+ return super().admin(*args, **kwargs, observation=observation)
diff --git a/scripts/validation_heal/stop.py b/scripts/validation_heal/stop.py
new file mode 100644
index 00000000..05b001a0
--- /dev/null
+++ b/scripts/validation_heal/stop.py
@@ -0,0 +1,309 @@
+#!/usr/bin/env python3
+"""A06: cancellation, actual quiet writes and stale-token isolation on DX."""
+
+from __future__ import annotations
+
+import copy
+from datetime import datetime
+import json
+import sys
+import time
+from urllib.parse import quote
+import xml.etree.ElementTree as ET
+
+try:
+ from .heal_probe import Probe, canonical, require
+ from .healthy import BASE_OPTIONS, physical_content
+ from .cursor import OBJECTS, TASK_SECONDS, Window, wanted_pairs, terminal_proof, main as dx_main
+ from .shards import files, validate_repaired_physical
+ from .versions import public_version_id
+ from .status import validate_background
+except ImportError:
+ from heal_probe import Probe, canonical, require
+ from healthy import BASE_OPTIONS, physical_content
+ from cursor import OBJECTS, TASK_SECONDS, Window, wanted_pairs, terminal_proof, main as dx_main
+ from shards import files, validate_repaired_physical
+ from versions import public_version_id
+ from status import validate_background
+
+
+CASE_ID = "heal.admin-stop-and-stale-token"
+BRANCHES = ("stop-active", "repeat-stop", "old-status", "old-stop-new-task")
+QUIET_SECONDS = 5
+POLICY = {"referenceCandidate": "072b57b37559fbe626aeda8abf61617fdcbdd926",
+ "cancelActive": {"httpStatus": 200, "summary": "stopped"},
+ "repeatOrRetiredTokenStop": {"httpStatus": 200, "summary": "stopped", "detail": "",
+ "reason": "root_recovery::cancel_pending returns true for a retained terminal record"},
+ "unknownTokenStop": {"httpStatus": 500, "code": "InternalError", "messagePrefix": "Heal task not found: "},
+ "oldStatus": "retained aborted/cancelled outcome, partial coverage; same path/token",
+ "sources": ["crates/heal/src/heal/manager.rs:cancel_task", "crates/heal/src/heal/channel.rs:process_cancel_request",
+ "crates/heal/src/heal/manager/root_recovery.rs:cancel_pending retained-terminal branch",
+ "rustfs/src/admin/handlers/heal.rs:force_stop handler"],
+ "sourceHashes": {
+ "crates/heal/src/heal/manager.rs": "ba9289942fbbe8ce2632686ae86b9d1be0ae9923420e3b091d67148b4ee02094",
+ "crates/heal/src/heal/channel.rs": "7f669ca43b9559db9b97fba6b419a6c76a94d76acd8526db132d74b7c534d6b0",
+ "crates/heal/src/heal/manager/root_recovery.rs": "ba6beba9d9d14985e4c5448d85727036388d3d72f28879ac5c67fc5f760048ba",
+ "rustfs/src/admin/handlers/heal.rs": "7e4007c29a8329ced8e48b88111400e79cffd845bd6ddc6243b79f3cd48e8199"},
+ "unsupportedCandidates": "fail the frozen HTTP/body contract; do not accept arbitrary alternatives"}
+
+
+def status(probe, fleet, path, token, label):
+ observed = fleet.record(label, probe.admin(path, [("clientToken", token)], b""))
+ require(observed.get("httpStatus") == 200 and isinstance(observed.get("response"), dict), "token status failed")
+ return observed["response"]
+
+
+def start(probe, fleet, path, active, *, force=False):
+ observed = fleet.record("trigger", probe.admin(path, [("forceStart", "true")] if force else [],
+ json.dumps({**BASE_OPTIONS, "recursive": True}).encode()))
+ require(observed.get("httpStatus") == 200 and isinstance(observed.get("response"), dict), "DX task admission failed")
+ token = observed["response"].get("clientToken")
+ require(isinstance(token, str) and token, "DX task omitted its admitted token")
+ active.append((path, token))
+ return token
+
+
+def partial_window(status, targets, *, repair_required):
+ counters = (status.get("outcome") or {}).get("counters", {})
+ processed, healed = counters.get("processed"), counters.get("healed")
+ for name in ("processed", "healed", "unchanged", "skipped", "failed", "unknown", "attemptFailures"):
+ require(type(counters.get(name)) is int and counters[name] >= 0, "active window omitted typed canonical counters")
+ require(all(counters[name] == 0 for name in ("failed", "unknown", "attemptFailures"))
+ and counters.get("overflowed") is False, "active window contains a real failure or unknown responsibility")
+ return (status.get("summary") == "running" and status.get("outcome", {}).get("execution", {}).get("state") == "running"
+ and 0 < processed < OBJECTS and (not repair_required or 0 < healed < len(targets)))
+
+
+def wait_partial(probe, fleet, path, token, targets, *, repair_required):
+ deadline = time.monotonic() + TASK_SECONDS
+ while True:
+ current = status(probe, fleet, path, token, "running-window")
+ if partial_window(current, targets, repair_required=repair_required):
+ return current
+ require(current.get("summary") == "running", "NOT_OBSERVED: task finished before the required active window")
+ require(time.monotonic() < deadline, "NOT_OBSERVED: active window exceeded the frozen budget")
+ time.sleep(0.05)
+
+
+def validate_retired_stop(observed, token):
+ require(observed.get("httpStatus") == 200 and not observed.get("transportError"),
+ "retired token stop did not follow the frozen HTTP contract")
+ query = observed.get("query")
+ require(isinstance(query, list) and len(query) == 2
+ and all(isinstance(pair, (list, tuple)) and len(pair) == 2 for pair in query)
+ and [tuple(pair) for pair in query] == [("clientToken", token), ("forceStop", "true")]
+ and observed.get("method") == "POST" and observed.get("auth") == "signed"
+ and observed.get("requestBody") == "" and isinstance(observed.get("path"), str)
+ and observed["path"].startswith("/rustfs/admin/v3/heal/"), "retired stop did not target the exact original token")
+ body = observed.get("response")
+ defaults = {**BASE_OPTIONS, "scanMode": 1, "pool": None, "set": None}
+ require(isinstance(body, dict) and set(body) == {"summary", "detail", "startTime", "settings"}
+ and body["summary"] == "stopped" and body["detail"] == "" and body["settings"] == defaults,
+ "retired stop lacks the exact idempotent acknowledgement; ACK defaults are not task settings")
+ require(isinstance(body["startTime"], str) and datetime.fromisoformat(body["startTime"].replace("Z", "+00:00")).tzinfo is not None,
+ "retired stop acknowledgement lacks a valid response timestamp")
+ return {"httpStatus": 200, "summary": "stopped", "exactRetiredTaskId": token,
+ "responseEchoesTaskIdentity": False, "ackDefaultsAreOriginalTaskSettings": False,
+ "terminalResponsibilityMustRemainUnchanged": True}
+
+
+def validate_unknown_token(observed, token):
+ require(observed.get("httpStatus") == 500 and not observed.get("transportError"),
+ "unknown token did not follow the frozen HTTP contract")
+ body = observed.get("response")
+ fields = {element.tag.rsplit("}", 1)[-1]: element.text for element in ET.fromstring(body).iter()} \
+ if isinstance(body, str) else body if isinstance(body, dict) else {}
+ require((fields.get("Code") or fields.get("code")) == "InternalError"
+ and (fields.get("Message") or fields.get("message")) == "Heal task not found: " + token,
+ "unknown-token response has the wrong exact structured error/task identity")
+ return {"httpStatus": 500, "code": "InternalError", "exactUnknownTaskId": token}
+
+
+def validate_cancelled(current, bucket, expected, targets):
+ wanted = wanted_pairs(expected)
+ outcome = current.get("outcome") or {}
+ require(current.get("summary") == "stopped" and outcome.get("execution") == {"state": "aborted", "reason": "cancelled"}
+ and outcome.get("coverage") == "partial", "cancelled responsibility was incorrectly reported as complete/repaired")
+ counters = outcome.get("counters", {})
+ for name in ("processed", "healed", "unchanged", "skipped", "failed", "unknown", "attemptFailures"):
+ require(type(counters.get(name)) is int and counters[name] >= 0, "cancelled task omitted typed counters")
+ require(0 < counters["processed"] < OBJECTS and 0 < counters["healed"] < len(targets)
+ and counters["processed"] == sum(counters[name] for name in ("healed", "unchanged", "skipped", "failed"))
+ and all(counters[name] == 0 for name in ("failed", "unknown", "attemptFailures"))
+ and counters.get("overflowed") is False, "cancelled counters hide partial coverage, errors or unresolved responsibility")
+ objects, seen, incarnation, dispositions = outcome.get("objects"), set(), None, []
+ require(isinstance(objects, list) and 0 < len(objects) <= 128, "cancelled task omitted bounded diagnostic receipts")
+ repaired = {(entry["key"], entry["versionId"]) for entry in targets}
+ for receipt in objects:
+ identity = receipt.get("identity", {})
+ pair = identity.get("object"), public_version_id(identity.get("versionId"))
+ require(pair in wanted and pair not in seen and identity.get("bucket") == bucket
+ and type(identity.get("poolIndex")) is int and identity["poolIndex"] == 0
+ and type(identity.get("setIndex")) is int and identity["setIndex"] == 0
+ and isinstance(identity.get("bucketIncarnationId"), str) and identity["bucketIncarnationId"],
+ "cancelled diagnostic receipt lost its exact responsibility identity")
+ incarnation = incarnation or identity["bucketIncarnationId"]
+ require(incarnation == identity["bucketIncarnationId"], "cancelled diagnostics mixed bucket incarnations")
+ state = receipt.get("disposition", {}).get("state")
+ require(state in {"repaired", "verified_healthy", "cancelled"}
+ and (state != "repaired" or pair in repaired)
+ and (state != "verified_healthy" or pair not in repaired),
+ "cancelled/unknown/unrepaired object falsely counted as repaired or healthy")
+ seen.add(pair)
+ dispositions.append({"key": pair[0], "versionId": pair[1], "state": state})
+ return {"canonicalCounters": counters, "coverage": "partial", "bucketIncarnationId": incarnation,
+ "unstartedObjectsAreNotRepaired": True, "diagnosticObjectsAreCompleteManifest": False,
+ "diagnosticDispositions": dispositions}
+
+
+def cancelled_physical(original, quiet, targets, cancelled):
+ actual = files(quiet)
+ restored, remaining = [], []
+ for target in targets:
+ entry = actual.get((target["node"], target["disk"], target["relativePath"]))
+ if entry is None:
+ remaining.append(target)
+ else:
+ require(entry["sha256"] == target["sha256"] and entry["size"] == target["size"],
+ "cancelled task committed incorrect shard bytes")
+ restored.append(target)
+ require(restored and remaining and len(restored) == cancelled["canonicalCounters"]["healed"],
+ "cancelled physical repair count disagrees with its retained outcome")
+ restored_pairs = {(entry["key"], entry["versionId"]) for entry in restored}
+ require(isinstance(cancelled.get("diagnosticDispositions"), list) and cancelled["diagnosticDispositions"],
+ "cancelled physical proof omitted its witnessed diagnostic identities")
+ for receipt in cancelled["diagnosticDispositions"]:
+ pair = receipt["key"], receipt["versionId"]
+ require((receipt["state"] == "repaired") == (pair in restored_pairs),
+ "cancelled diagnostic disposition disagrees with the actual restored object")
+ expected = copy.deepcopy(original)
+ missing = {(entry["node"], entry["disk"], entry["relativePath"]) for entry in remaining}
+ for node, observation in expected.items():
+ observation["files"] = [entry for entry in observation["files"] if (node, entry["disk"], entry["path"]) not in missing]
+ proof = validate_repaired_physical(expected, quiet, restored[0], repair_targets=restored)
+ return remaining, {**proof, "actuallyRestoredParts": len(restored), "stillMissingParts": len(remaining),
+ "backupRescueUsed": False, "cancelledCountersMatchPhysical": True}
+
+
+def idle_fleet(fleet, binding):
+ deadline = time.monotonic() + 30
+ while True:
+ proofs = {}
+ for endpoint in binding["endpoints"]:
+ observed = fleet.record("task-exit-background", {"endpoint": endpoint, **Probe(binding, endpoint).admin(
+ "/rustfs/admin/v3/background-heal/status", [], b"")})
+ require(observed.get("httpStatus") == 200 and isinstance(observed.get("response"), dict),
+ "task exit has no complete background status")
+ value = observed["response"]
+ require(value.get("state") != "idle" or value.get("clusterStatusComplete") is True
+ and value.get("coverage", {}).get("unknown") == 0
+ and value.get("coverage", {}).get("responded") == len(binding["endpoints"]),
+ "task exit falsely reports a partial fleet as idle")
+ try:
+ validate_background(value, "idle", len(binding["endpoints"]))
+ proofs[endpoint] = {"state": "idle", "clusterStatusComplete": True,
+ "coverage": value["coverage"], "healOperations": value["healOperations"]}
+ except ValueError:
+ pass
+ if len(proofs) == len(binding["endpoints"]):
+ return proofs
+ require(time.monotonic() < deadline, "task did not drain queued/active/retrying responsibility on every peer")
+ time.sleep(0.1)
+
+
+def observe_task(probe, fleet, binding, bucket, expected, targets, active, *, original, after_cancelled=None, force_new=False):
+ require(binding["candidateCommit"] == POLICY["referenceCandidate"], "cancel policy is unreviewed for this candidate")
+ path = "/rustfs/admin/v3/heal/" + quote(bucket, safe="") + "/objects/"
+ fleet.record("frozen-cancel-policy", POLICY)
+ old = start(probe, fleet, path, active)
+ first = wait_partial(probe, fleet, path, old, targets, repair_required=True)
+ stop_ack = fleet.record("stop-active", probe.admin(path, [("clientToken", old), ("forceStop", "true")], b""))
+ require(stop_ack.get("httpStatus") == 200 and stop_ack.get("response", {}).get("summary") == "stopped",
+ "active cancellation did not follow the frozen acknowledgement contract")
+ cancelled = status(probe, fleet, path, old, "cancelled-outcome")
+ initial_cancellation = validate_cancelled(cancelled, bucket, expected, targets)
+ idle_fleet(fleet, binding)
+ cancelled = status(probe, fleet, path, old, "cancelled-after-drain")
+ cancellation = validate_cancelled(cancelled, bucket, expected, targets)
+ require(cancellation["bucketIncarnationId"] == initial_cancellation["bucketIncarnationId"]
+ and all(cancellation["canonicalCounters"][name] >= initial_cancellation["canonicalCounters"][name]
+ for name in ("processed", "healed", "unchanged", "skipped")),
+ "in-flight cancellation refinement changed identity or moved counters backwards")
+ fleet.record("cancelled-settlement", {"initial": initial_cancellation, "afterActualDrain": cancellation,
+ "earlierObservationsPreserved": True, "reason": "candidate refines cancelled reports while retiring in-flight workers"})
+ # Keep the actual service alive. An ACK or stopping its process would mask
+ # writes that escaped task cancellation.
+ quiet = []
+ exit_proofs = []
+ for index in range(3):
+ exit_proofs.append(idle_fleet(fleet, binding))
+ started = time.monotonic_ns()
+ scene = fleet.snapshot("cancelled-live-quiet")
+ quiet.append(scene)
+ fleet.record("quiet-sample-time", {"sample": index, "startedMonotonicNs": started,
+ "finishedMonotonicNs": time.monotonic_ns(), "serviceKeptRunning": True, "minimumBetweenSamplesSeconds": QUIET_SECONDS})
+ unchanged = status(probe, fleet, path, old, "quiet-old-status")
+ validate_cancelled(unchanged, bucket, expected, targets)
+ require(canonical(unchanged["outcome"]) == canonical(cancelled["outcome"]), "retired responsibility changed during write quiet window")
+ if index < 2:
+ time.sleep(QUIET_SECONDS)
+ require(all(physical_content(scene) == physical_content(quiet[0]) for scene in quiet[1:]),
+ "cancelled task continued physical writes after its terminal acknowledgement")
+ require(all(all((scene[node]["pid"], scene[node]["unit"]) == (quiet[0][node]["pid"], quiet[0][node]["unit"])
+ for node in fleet.nodes) for scene in quiet[1:]),
+ "quiet window replaced the original live service process")
+ remaining, physical = cancelled_physical(original, quiet[-1], targets, cancellation)
+ active.remove((path, old))
+ repeated = fleet.record("repeat-stop", probe.admin(path, [("clientToken", old), ("forceStop", "true")], b""))
+ repeated_proof = validate_retired_stop(repeated, old)
+ repeated_status = status(probe, fleet, path, old, "old-status-after-idempotent-stop")
+ require(canonical(repeated_status.get("outcome")) == canonical(cancelled["outcome"]),
+ "idempotent stop changed the original settled cancellation responsibility")
+ restart_proof = after_cancelled(path, old, cancelled, quiet[-1]) if after_cancelled is not None else None
+ new = start(probe, fleet, path, active, force=force_new)
+ require(new != old, "new task reused a cancelled token")
+ before = wait_partial(probe, fleet, path, new, remaining, repair_required=False)
+ old_status = status(probe, fleet, path, old, "old-status-during-new")
+ validate_cancelled(old_status, bucket, expected, targets)
+ require(canonical(old_status["outcome"]) == canonical(cancelled["outcome"]), "new task overwrote old partial responsibility")
+ stale = fleet.record("old-stop-during-new", probe.admin(path, [("clientToken", old), ("forceStop", "true")], b""))
+ stale_proof = validate_retired_stop(stale, old)
+ stale_status = status(probe, fleet, path, old, "old-status-after-stale-stop")
+ require(canonical(stale_status.get("outcome")) == canonical(cancelled["outcome"]),
+ "stale stop changed the old task's terminal responsibility")
+ after = status(probe, fleet, path, new, "new-after-stale-stop")
+ require(after.get("summary") != "stopped", "old token actually stopped the new task")
+ require(partial_window(after, remaining, repair_required=False),
+ "NOT_OBSERVED: stale-stop request was not bracketed by the same active new task")
+ after_stale_counters = dict(after["outcome"]["counters"])
+ deadline = time.monotonic() + TASK_SECONDS
+ while after.get("summary") == "running":
+ require(time.monotonic() < deadline, "new task exceeded the frozen completion budget")
+ time.sleep(0.05)
+ after = status(probe, fleet, path, new, "new-task-control")
+ active.remove((path, new))
+ response = fleet.record("new-terminal-old-cursor", probe.admin(path, [("clientToken", new), ("sinceSeq", "0")], b""))
+ require(response.get("httpStatus") == 200 and isinstance(response.get("response"), dict), "new task terminal cursor failed")
+ terminal = terminal_proof(response["response"], bucket, expected, {(entry["key"], entry["versionId"]) for entry in remaining},
+ Window(bucket, expected, new, "node1", fleet.run_id + "/" + new))
+ require(canonical(response["response"].get("outcome")) == canonical(after.get("outcome")),
+ "new task terminal responsibility changed")
+ old_final = status(probe, fleet, path, old, "old-final-status")
+ require(canonical(old_final.get("outcome")) == canonical(cancelled["outcome"]), "new completion rewrote the original cancellation")
+ final_exit = idle_fleet(fleet, binding)
+ return {"stop-active": {"verdict": "PASS", "activeWindow": first["outcome"]["counters"],
+ "cancelled": cancellation, "physicalQuietProof": physical, "liveQuietSamples": 3, "betweenSamplesSeconds": QUIET_SECONDS,
+ "sameLiveProcesses": True, "allPeerIdleProofs": exit_proofs, "restartProof": restart_proof},
+ "repeat-stop": {"verdict": "PASS", **repeated_proof},
+ "old-status": {"verdict": "PASS", "oldTaskId": old, "originalPartialOutcomePreserved": True},
+ "old-stop-new-task": {"verdict": "PASS", "oldTaskId": old, "newTaskId": new,
+ "newRunningBefore": before["outcome"]["counters"], "newRunningAfter": after_stale_counters,
+ "staleStop": stale_proof, "newCompleteResponsibility": terminal, "allPeerIdleProof": final_exit}}
+
+
+if __name__ == "__main__":
+ try:
+ dx_main(case_id=CASE_ID, observer=observe_task, branches=BRANCHES)
+ except Exception as exc:
+ print(f"Heal stop validation failed: {exc}", file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_heal/timeout_retention.py b/scripts/validation_heal/timeout_retention.py
new file mode 100644
index 00000000..9ae7705f
--- /dev/null
+++ b/scripts/validation_heal/timeout_retention.py
@@ -0,0 +1,982 @@
+"""Isolate archived I05 H1/H2 failures with bounded, understood request effects."""
+
+import hashlib
+import ipaddress
+import json
+import re
+import uuid
+
+try:
+ from .heal_probe import canonical, payload, require
+ from .heal_workload import verify_seed
+ from .healthy import physical_content, validate_topology
+ from .fleet_node import runtime_config
+ from .lock_observer import ADMISSION_SECONDS, CANDIDATE, DRAIN_SECONDS, TARGET, same_process
+ from .lock_timeout import (CALLER_RECEIVE_BUFFER_BYTES, CONTENDER_BYTES,
+ MIN_BACKPRESSURE_BYTES, timeout_proof, validate_timeout_ack)
+ from .race_retention import digest, validate
+except ImportError:
+ from heal_probe import canonical, payload, require
+ from heal_workload import verify_seed
+ from healthy import physical_content, validate_topology
+ from fleet_node import runtime_config
+ from lock_observer import ADMISSION_SECONDS, CANDIDATE, DRAIN_SECONDS, TARGET, same_process
+ from lock_timeout import (CALLER_RECEIVE_BUFFER_BYTES, CONTENDER_BYTES,
+ MIN_BACKPRESSURE_BYTES, timeout_proof, validate_timeout_ack)
+ from race_retention import digest, validate
+
+
+CASE = 'heal.lock-timeout-and-caller-abort'
+PRE_CONTENDER_HELPER = '704bd606071676dded130dca91d3d5278455bcaa'
+CONTENDER_HELPER = 'd100abeb8e85c4000a493aa994031e4a5d01518e'
+H2_CONTENDER_HELPER = '1b88cad3581e755d8e904697cb2ec471e04b5f8f'
+DRAIN_BUDGET_HELPER = '0442d4dd814ebfb490dcbb3cc266fb6fbebb0365'
+DESCRIPTOR_HELPER = 'dd7e54e6672cab76b5a6245ab3b4fbd051ddd39d'
+FILTER_STARTUP_HELPER = 'a9725c88acd4977545b278eebe3653c916f907f8'
+FILTER_STARTUP_ARCHIVE_SHA256 = 'c6760d0b4e657c306771a466e0a5aa4233f8e05db4aeae018ba037d289fca4aa'
+H2_FILTER_STARTUP_HELPER = '31b787c51de23587da1f9a2ac0f910400ddf66b3'
+H2_FILTER_STARTUP_ARCHIVE_SHA256 = '8a5399be0a35295703b079ed10cb55685ee4e347e9f9e276b29140afe6764e49'
+DRAIN_HELPERS = {DRAIN_BUDGET_HELPER, DESCRIPTOR_HELPER}
+FAILURES = {
+ PRE_CONTENDER_HELPER: {'type': 'ValueError',
+ 'message': 'caller lock diagnostic escaped its exact GET identity or runtime budget'},
+ CONTENDER_HELPER: {'type': 'ValueError',
+ 'message': 'I05 contender acquired a lock or lost its exact timeout lifecycle'},
+ H2_CONTENDER_HELPER: {'type': 'ValueError',
+ 'message': 'I05 contender acquired a lock or lost its exact timeout lifecycle'},
+ DRAIN_BUDGET_HELPER: {'type': 'ValueError',
+ 'message': 'NOT_OBSERVED: caller drain observation exceeded its budget'},
+ DESCRIPTOR_HELPER: {'type': 'ValueError',
+ 'message': 'NOT_OBSERVED: caller drain observation exceeded its budget'},
+}
+COMMON_LABELS = ('binding', 'inspector-build', 'preflight', 'integrity-fleet-attestation', 'reserved',
+ 'inspector-upload', 'started', 'deployed-topology', 'actual-topology', 'original-ACK-bound-S3',
+ 'original-ACK-proof', 'I05-timeout-original-physical', 'I05-timeout-original-S3',
+ 'I05-timeout-baseline-physical-after-public-read', 'I05-caller-baseline-resources',
+ 'I05-caller-checkpoint')
+LABELS = {
+ PRE_CONTENDER_HELPER: COMMON_LABELS + ('I05-caller-window', 'quiescent',
+ 'original-failure-scene', 'original-failure-logs', 'diagnostic-result'),
+ CONTENDER_HELPER: COMMON_LABELS + ('I05-caller-window', 'I05-caller-before-disconnect-socket',
+ 'I05-caller-window', 'I05-caller-admission-resources', 'I05-caller-window', 'quiescent',
+ 'original-failure-scene', 'original-failure-logs', 'diagnostic-result'),
+ H2_CONTENDER_HELPER: COMMON_LABELS + ('I05-caller-window', 'I05-caller-before-disconnect-socket',
+ 'I05-caller-window', 'I05-caller-admission-resources', 'I05-caller-window', 'quiescent',
+ 'original-failure-scene', 'original-failure-logs', 'diagnostic-result'),
+}
+DRAIN_PREFIX_LABELS = COMMON_LABELS + ('I05-caller-window', 'I05-caller-before-disconnect-socket',
+ 'I05-caller-window', 'I05-caller-admission-resources', 'I05-caller-window',
+ 'I05-timeout-caller-backpressure-socket')
+DRAIN_SUFFIX_LABELS = ('quiescent', 'original-failure-scene', 'original-failure-logs', 'diagnostic-result')
+
+
+def h2_labels(window_rounds=(1, 1, 1)):
+ """Exact journal shape when H1 cleans successfully and H2 fails in its contender window."""
+ return ('binding', 'inspector-build', 'preflight', 'integrity-fleet-attestation',
+ *('reserved' for _ in range(4)), *('inspector-upload' for _ in range(4)),
+ 'started', 'deployed-topology', 'actual-topology', 'original-ACK-bound-S3',
+ 'original-ACK-proof', 'I05-timeout-original-physical', 'I05-timeout-original-S3',
+ 'I05-timeout-baseline-physical-after-public-read',
+ *('I05-caller-baseline-resources' for _ in range(4)),
+ *('I05-caller-checkpoint' for _ in range(4)),
+ *('I05-caller-window' for _ in range(4 * window_rounds[0])), 'I05-caller-before-disconnect-socket',
+ *('I05-caller-window' for _ in range(4 * window_rounds[1])),
+ *('I05-caller-admission-resources' for _ in range(4)),
+ *('I05-caller-window' for _ in range(4 * window_rounds[2])), 'quiescent', 'original-failure-scene',
+ *('original-failure-logs' for _ in range(4)), 'diagnostic-result')
+
+
+def drain_failure_labels(folder):
+ """Accept only complete repeated H1 drain samples from the pinned failed helper."""
+ names = sorted(path.name for path in folder.glob('*.json'))
+ labels = [name[6:-5] for name in names]
+ prefix, suffix = list(DRAIN_PREFIX_LABELS), list(DRAIN_SUFFIX_LABELS)
+ require(labels[:len(prefix)] == prefix and labels[-len(suffix):] == suffix,
+ 'I05 drain failure has a different caller or retained-scene boundary')
+ samples = labels[len(prefix):-len(suffix)]
+ first_drain = samples.index('I05-caller-drain-socket') if 'I05-caller-drain-socket' in samples else -1
+ caller_windows = samples[:first_drain]
+ rounds = samples[first_drain:]
+ complete_rounds, terminal = divmod(len(rounds), 3)
+ expected_rounds = ['I05-caller-drain-socket', 'I05-caller-drain-resources', 'I05-caller-window'] * complete_rounds
+ require(first_drain > 0 and all(label == 'I05-caller-window' for label in caller_windows)
+ and complete_rounds >= 1 and rounds[:len(expected_rounds)] == expected_rounds
+ and (terminal == 0 or rounds[-terminal:] == ['I05-caller-drain-socket', 'I05-caller-drain-resources']),
+ 'I05 drain failure omits or reorders a complete socket/resource/journal sample')
+ return prefix + caller_windows + rounds + suffix
+
+
+def validate_census(value):
+ """Keep the reviewed helper's complete snapshots without inferring no leak."""
+ require(isinstance(value, dict)
+ and value.get('descriptorContentsRead') is False
+ and value.get('rawDescriptorPathsIncluded') is False
+ and value.get('snapshotOnly') is True and value.get('noLeakProven') is False
+ and type(value.get('startedMonotonicNs')) is int
+ and type(value.get('finishedMonotonicNs')) is int
+ and 0 < value['startedMonotonicNs'] <= value['finishedMonotonicNs']
+ and type(value.get('fileDescriptors')) is int
+ and 0 <= value['fileDescriptors'] <= 65536
+ and value.get('vanishedDescriptors') == 0
+ and isinstance(value.get('descriptors'), list)
+ and len(value['descriptors']) == value['fileDescriptors']
+ and isinstance(value.get('ownedTcpConnections'), list),
+ 'I05 descriptor census is incomplete or claims more than a snapshot')
+ descriptors, sockets = {}, {}
+ for row in value['descriptors']:
+ fd, kind = row.get('descriptor'), row.get('kind')
+ require(type(fd) is int and fd >= 0 and fd not in descriptors
+ and kind in {'socket', 'pipe', 'path', 'anonymous'}
+ and re.fullmatch(r'[0-9a-f]{64}', row.get('targetSha256', ''))
+ and set(row) == {'descriptor', 'kind', 'targetSha256'} | (
+ {'inode'} if kind in {'socket', 'pipe'} else set()),
+ 'I05 descriptor identity is missing, duplicated or contains an unreviewed field')
+ descriptors[fd] = row
+ if kind in {'socket', 'pipe'}:
+ inode = row.get('inode')
+ require(type(inode) is int and inode > 0
+ and row['targetSha256'] == hashlib.sha256(f'{kind}:[{inode}]'.encode()).hexdigest(),
+ 'I05 descriptor inode does not match its link fingerprint')
+ if kind == 'socket':
+ sockets.setdefault(inode, []).append(fd)
+ seen = set()
+ for row in value['ownedTcpConnections']:
+ inode = row.get('inode')
+ require(type(inode) is int and inode in sockets and inode not in seen
+ and row.get('descriptors') == sockets[inode]
+ and row.get('table') in {'tcp', 'tcp6'}
+ and re.fullmatch(r'[0-9A-F]{2}', row.get('state', ''))
+ and all(type(row.get(name)) is int and row[name] >= 0
+ for name in ('txQueueBytes', 'rxQueueBytes')),
+ 'I05 TCP census is not joined to the original owned descriptors')
+ seen.add(inode)
+ for name in ('localAddress', 'remoteAddress'):
+ endpoint = row.get(name)
+ require(isinstance(endpoint, list) and len(endpoint) == 2
+ and isinstance(endpoint[0], str) and type(endpoint[1]) is int
+ and 0 <= endpoint[1] <= 65535,
+ 'I05 TCP census has an invalid numeric endpoint')
+ ipaddress.ip_address(endpoint[0])
+
+
+def filter_startup_plan(result_path, failed_run, candidate):
+ """Preserve the exact pre-workload filter-check failure, without deleting it."""
+ require(failed_run == 39761856012, 'I05 filter startup isolation requires its reviewed failed run')
+ archive = result_path.parents[3]
+ files = {}
+ for path in archive.rglob('*'):
+ require(not path.is_symlink() and (path.is_dir() or path.is_file()),
+ 'I05 filter startup archive contains a link or special file')
+ if path.is_file():
+ require(path.stat().st_size <= 2 * 1024**2 and len(files) < 22,
+ 'I05 filter startup archive exceeds its reviewed bound')
+ files[path.relative_to(archive).as_posix()] = hashlib.sha256(path.read_bytes()).hexdigest()
+ require(len(files) == 22 and digest(files) == FILTER_STARTUP_ARCHIVE_SHA256,
+ 'I05 filter startup archive differs from its byte-exact original')
+ def read(path): return json.loads(path.read_text())
+ result = read(result_path)
+ provenance = result['provenance']
+ evidence = result_path.parents[1] / 'evidence' / CASE
+ folder = evidence / 'H1'
+ labels = ('binding', 'inspector-build', 'preflight', 'integrity-fleet-attestation',
+ 'reserved', 'inspector-upload', 'quiescent', 'original-failure-scene',
+ 'original-failure-logs', 'diagnostic-result')
+ require({path.name for path in folder.iterdir()} == {
+ f'{index:05d}-{label}.json' for index, label in enumerate(labels, 1)}
+ and not (evidence / 'H2').exists(),
+ 'I05 filter startup isolation cannot include a workload or another topology')
+ records = {label: read(folder / f'{index:05d}-{label}.json')
+ for index, label in enumerate(labels, 1)}
+ binding, reservation, scene = (records[label] for label in ('binding', 'reserved', 'original-failure-scene'))
+ run_id = 'gh-39761856012-1-h1'
+ owner = reservation['owner']
+ original = scene['vm000']
+ diagnostic = records['diagnostic-result']
+ require(result.get('verdict') == result.get('cleanup_status') == 'FAIL'
+ and result.get('case_id') == CASE
+ and candidate == read(archive / 'candidate.json')
+ and provenance['source_commit'] == binding['candidateCommit'] == CANDIDATE
+ and provenance['candidate_sha256'] == binding['candidatePackageSha256'] == candidate['package_sha256']
+ and provenance['testing_commit'] == binding['harnessCommit'] == FILTER_STARTUP_HELPER
+ and provenance['binary_sha256'] == binding['candidateBinarySha256'] == owner['binarySha256']
+ and binding['runId'] == run_id and reservation['node'] == 'vm000' and reservation['reserved'] is True
+ and original.get('quiescent') is True and original.get('pid') == 0 and original.get('files') == []
+ and original.get('unit') == owner['unit'] and original.get('hostname') == owner['hostname']
+ and diagnostic.get('diagnosticComplete') is False and diagnostic.get('cleanup') == 'FAIL'
+ and 'I05 kernel service BPF attachments are missing, foreign or incomplete' in
+ diagnostic.get('failure', {}).get('message', '')
+ and diagnostic.get('namespacesRetained') is True
+ and diagnostic.get('canonicalCasePassClaimed') is False,
+ 'I05 filter startup isolation lost its original candidate, owner or stopped empty scene')
+ validate_topology(scene, 'H1')
+ plan = {'kind': 'unresolved-i05-timeout', 'runId': run_id, 'nodes': ['vm000'],
+ 'bucket': binding['bucketPrefix'], 'owners': {'vm000': owner},
+ 'candidateCommit': CANDIDATE, 'harnessCommit': FILTER_STARTUP_HELPER,
+ 'binarySha256': binding['candidateBinarySha256'], 'ackFilesSha256': digest(files),
+ 'businessSceneSha256': {'vm000': digest(physical_content(scene)['vm000'])}}
+ validate(plan)
+ return [plan]
+
+
+def h2_filter_startup_plan(result_path, failed_run, candidate):
+ """Preserve only the reviewed four-peer startup failure after clean H1."""
+ require(failed_run == 39764496504, 'I05 H2 startup isolation requires its reviewed failed run')
+ archive = result_path.parents[3]
+ files = {}
+ for path in archive.rglob('*'):
+ require(not path.is_symlink() and (path.is_dir() or path.is_file()),
+ 'I05 H2 startup archive contains a link or special file')
+ if path.is_file():
+ require(path.stat().st_size <= 2 * 1024**2 and len(files) < 102,
+ 'I05 H2 startup archive exceeds its reviewed bound')
+ files[path.relative_to(archive).as_posix()] = hashlib.sha256(path.read_bytes()).hexdigest()
+ require(len(files) == 102 and digest(files) == H2_FILTER_STARTUP_ARCHIVE_SHA256,
+ 'I05 H2 startup archive differs from its byte-exact original')
+ def read(path): return json.loads(path.read_text())
+ result = read(result_path)
+ provenance = result['provenance']
+ evidence = result_path.parents[1] / 'evidence' / CASE
+ summary = read(evidence / 'healthy-summary.json')
+ h1 = {'topology': 'H1', 'diagnosticComplete': True, 'cleanup': 'PASS',
+ 'wholeCasePassClaimed': False, 'canonicalCasePassClaimed': False}
+ require(summary.get('caseId') == CASE and summary.get('cleanup') == 'FAIL'
+ and summary.get('requestedTopologies') == ['H1', 'H2']
+ and summary.get('complete') is False and summary.get('diagnosticComplete') is False
+ and summary.get('canonicalCasePassClaimed') is False
+ and len(summary.get('diagnostics', [])) == 2 and summary['diagnostics'][0] == h1
+ and read(evidence / 'H1/00042-diagnostic-result.json') == h1
+ and read(evidence / 'H1/00041-recovery.json') == {'cleanup': 'PASS', 'errors': []}
+ and read(evidence / 'H1/00040-cleanup.json') == {
+ 'node': 'vm000', 'cleanup': 'PASS', 'unitStopped': True, 'rootsRemoved': True,
+ 'hostname': 'rustfs000000'},
+ 'I05 H2 startup isolation requires completed H1 diagnostics and actual root cleanup')
+ folder = evidence / 'H2'
+ labels = ('binding', 'inspector-build', 'preflight', 'integrity-fleet-attestation',
+ *('reserved', 'inspector-upload') * 4, 'quiescent', 'original-failure-scene',
+ *('original-failure-logs' for _ in range(4)), 'diagnostic-result')
+ require({path.name for path in folder.iterdir()} == {
+ f'{index:05d}-{label}.json' for index, label in enumerate(labels, 1)},
+ 'I05 H2 startup isolation cannot include workload or unreviewed actions')
+ records = [(label, read(folder / f'{index:05d}-{label}.json'))
+ for index, label in enumerate(labels, 1)]
+ def many(label): return [value for name, value in records if name == label]
+ def one(label):
+ values = many(label)
+ require(len(values) == 1, 'I05 H2 startup lacks one exact ' + label)
+ return values[0]
+ binding, scene, diagnostic = (one(label) for label in ('binding', 'original-failure-scene', 'diagnostic-result'))
+ run_id = 'gh-39764496504-1-h2'
+ nodes = ['vm000', 'vm001', 'vm002', 'vm003']
+ owners = {row['node']: row['owner'] for row in many('reserved')}
+ require(result.get('case_id') == CASE and result.get('verdict') == result.get('cleanup_status') == 'FAIL'
+ and candidate == read(archive / 'candidate.json')
+ and provenance['source_commit'] == binding['candidateCommit'] == candidate['source_sha'] == CANDIDATE
+ and provenance['candidate_sha256'] == binding['candidatePackageSha256'] == candidate['package_sha256']
+ and provenance['testing_commit'] == binding['harnessCommit'] == H2_FILTER_STARTUP_HELPER
+ and provenance['binary_sha256'] == binding['candidateBinarySha256']
+ and binding['runId'] == run_id
+ and binding['bucketPrefix'] == 'rv-heal-' + hashlib.sha256(run_id.encode()).hexdigest()[:16]
+ and binding['endpoints'] == {f'node{i}': f'http://rustfs-node{i}:9000' for i in range(1, 5)}
+ and summary['diagnostics'][1] == diagnostic
+ and diagnostic.get('topology') == 'H2' and diagnostic.get('diagnosticComplete') is False
+ and diagnostic.get('cleanup') == 'FAIL' and diagnostic.get('namespacesRetained') is True
+ and diagnostic.get('wholeCasePassClaimed') is False and diagnostic.get('canonicalCasePassClaimed') is False
+ and diagnostic.get('failure', {}).get('type') == 'ValueError'
+ and 'bpftool not found for kernel 6.17.0-1022' in diagnostic.get('failure', {}).get('message', '')
+ and set(owners) == set(scene) == set(one('quiescent')) == set(nodes)
+ and all(row.get('reserved') is True for row in many('reserved'))
+ and one('integrity-fleet-attestation').get('nodes') == nodes
+ and one('integrity-fleet-attestation').get('allParticipatingNodesUseCandidate') is True
+ and one('inspector-build').get('built') is True
+ and one('inspector-build').get('candidateCommit') == CANDIDATE,
+ 'I05 H2 startup isolation lost the original failure, candidate or all four peers')
+ uploads = {row['node']: row for row in many('inspector-upload')}
+ logs = {row['node']: row for row in many('original-failure-logs')}
+ require(set(uploads) == set(logs) == set(one('preflight')) == set(nodes),
+ 'I05 H2 startup omits an original upload, preflight or service log')
+ for node in nodes:
+ owner, original = owners[node], scene[node]
+ require(original.get('quiescent') is True and original.get('pid') == 0 and original.get('files') == []
+ and original.get('unit') == logs[node].get('unit') == owner['unit']
+ and original.get('hostname') == one('preflight')[node].get('hostname') == owner['hostname']
+ and one('preflight')[node].get('mounts') == owner['mounts']
+ and original.get('binarySha256') == owner['binarySha256'] == binding['candidateBinarySha256']
+ and uploads[node].get('installed') is True
+ and uploads[node].get('inspectorSha256') == owner.get('inspectorSha256')
+ == one('inspector-build').get('binarySha256')
+ and one('quiescent')[node].get('quiescent') is True
+ and one('quiescent')[node].get('service', {}).get('MainPID') == '0',
+ 'I05 H2 startup scene is not the stopped, empty original on ' + node)
+ validate_topology(scene, 'H2')
+ plan = {'kind': 'unresolved-i05-timeout', 'runId': run_id, 'nodes': nodes,
+ 'bucket': binding['bucketPrefix'], 'owners': owners, 'candidateCommit': CANDIDATE,
+ 'harnessCommit': H2_FILTER_STARTUP_HELPER, 'binarySha256': binding['candidateBinarySha256'],
+ 'ackFilesSha256': digest(files), 'businessSceneSha256': {
+ node: digest(value) for node, value in physical_content(scene).items()}}
+ validate(plan)
+ return [plan]
+
+
+def successor_files(folder):
+ """Fingerprint the complete stopped diagnostic, including unknown receipts."""
+ require(folder.is_dir() and not folder.is_symlink(), 'I05 successor evidence root differs')
+ files, total = {}, 0
+ for path in sorted(folder.rglob('*')):
+ require(not path.is_symlink() and (path.is_dir() or path.is_file()),
+ 'I05 successor evidence contains a link or special file')
+ if not path.is_file() or path == folder / 'successor-retention.json':
+ continue
+ size = path.stat().st_size
+ total += size
+ require(size <= 32 * 1024**2 and total <= 256 * 1024**2 and len(files) < 4096,
+ 'I05 successor evidence exceeds its preservation budget')
+ files[path.relative_to(folder).as_posix()] = hashlib.sha256(path.read_bytes()).hexdigest()
+ require(files, 'I05 successor has no original evidence')
+ return files
+
+
+def successor_plan(folder, binding, scene):
+ """Preservation needs exact stopped bytes, never a successful PUT outcome."""
+ topology = 'H1' if binding['runId'].endswith('-h1') else 'H2'
+ nodes = ['vm000'] if topology == 'H1' else ['vm000', 'vm001', 'vm002', 'vm003']
+ reservations = [json.loads(path.read_text()) for path in folder.glob('*-reserved.json')]
+ owners = {row.get('node'): row.get('owner') for row in reservations}
+ require(len(reservations) == len(nodes) and set(owners) == set(scene) == set(nodes)
+ and all(row.get('reserved') is True for row in reservations)
+ and binding.get('candidateCommit') == CANDIDATE
+ and binding.get('endpoints') == {f'node{i}': f'http://rustfs-node{i}:9000'
+ for i in range(1, len(nodes) + 1)}
+ and binding.get('bucketPrefix') == 'rv-heal-' + hashlib.sha256(binding['runId'].encode()).hexdigest()[:16],
+ 'I05 successor preservation requires the complete original fleet binding')
+ for node, owner in owners.items():
+ original = scene[node]
+ require(owner.get('runtimeProfile') == 'lock-successor'
+ and original.get('quiescent') is True and type(original.get('pid')) is int and original['pid'] == 0
+ and original.get('hostname') == owner.get('hostname')
+ and original.get('unit') == owner.get('unit')
+ and original.get('binarySha256') == owner.get('binarySha256') == binding['candidateBinarySha256']
+ and len(original.get('formats', [])) == 4
+ and {part.get('disk') for part in original['formats']} == set(range(4)),
+ 'I05 successor requires the stopped original owner and all four disks')
+ plan = {'kind': 'unresolved-i05-successor', 'runId': binding['runId'], 'nodes': nodes,
+ 'bucket': binding['bucketPrefix'], 'owners': owners, 'candidateCommit': CANDIDATE,
+ 'harnessCommit': binding['harnessCommit'], 'binarySha256': binding['candidateBinarySha256'],
+ 'ackFilesSha256': digest(successor_files(folder)),
+ 'businessSceneSha256': {node: digest(value) for node, value in physical_content(scene).items()}}
+ validate(plan)
+ return plan
+
+
+def seal_successor_scene(folder, binding, scene):
+ files = successor_files(folder)
+ plan = successor_plan(folder, binding, scene)
+ require(digest(files) == plan['ackFilesSha256'], 'I05 successor evidence changed while sealing')
+ return {'schemaVersion': 1, 'plan': plan, 'originalFilesSha256': files,
+ 'candidatePackageSha256': binding['candidatePackageSha256'],
+ 'canonicalCasePassClaimed': False, 'unknownMutationMayBePresent': True,
+ 'namespaceDeletionPermitted': False, 'backupRestorationPermitted': False}
+
+
+def successor_archived_plan(result_path, failed_run, candidate):
+ result = json.loads(result_path.read_text())
+ provenance = result.get('provenance') or {}
+ evidence = result_path.parents[1] / 'evidence' / CASE
+ summary = json.loads((evidence / 'healthy-summary.json').read_text())
+ rows = summary.get('diagnostics', [])
+ require(result.get('case_id') == CASE and result.get('verdict') == result.get('cleanup_status') == 'FAIL'
+ and summary.get('caseId') == CASE and summary.get('diagnosticBranch') == 'queued-successor'
+ and summary.get('cleanup') == 'FAIL' and summary.get('complete') is False
+ and summary.get('diagnosticComplete') is False
+ and summary.get('canonicalCasePassClaimed') is False and summary.get('wholeCasePassClaimed') is False
+ and isinstance(rows, list) and 1 <= len(rows) <= 2
+ and [row.get('topology') for row in rows] in (['H1'], ['H2'], ['H1', 'H2'])
+ and [row['topology'] for row in rows] == summary.get('requestedTopologies', [])[:len(rows)]
+ and all(row.get('diagnosticComplete') is True and row.get('cleanup') == 'PASS' for row in rows[:-1])
+ and rows[-1].get('diagnosticComplete') is False and rows[-1].get('cleanup') == 'FAIL'
+ and rows[-1].get('namespacesRetained') is True
+ and 'failureCaptureError' not in rows[-1] and 'retentionCaptureError' not in rows[-1]
+ and all(row.get('branch') == 'queued-successor' and row.get('canonicalCasePassClaimed') is False
+ and row.get('wholeCasePassClaimed') is False for row in rows),
+ 'I05 successor recovery requires one retained diagnostic failure after clean predecessors')
+ require({p.name for p in evidence.iterdir() if p.is_dir()} == {row['topology'] for row in rows},
+ 'I05 successor recovery cannot omit another topology')
+ for row in rows:
+ found = list((evidence / row['topology']).glob('*-diagnostic-result.json'))
+ require(len(found) == 1 and json.loads(found[0].read_text()) == row,
+ 'I05 successor summary differs from its original diagnostic result')
+ folder = evidence / rows[-1]['topology']
+ def one(label):
+ paths = list(folder.glob('*-' + label + '.json'))
+ require(len(paths) == 1, 'I05 successor lacks one original ' + label)
+ return json.loads(paths[0].read_text())
+ manifest_path = folder / 'successor-retention.json'
+ require(manifest_path.is_file() and not manifest_path.is_symlink()
+ and manifest_path.stat().st_size <= 2 * 1024**2, 'I05 successor retention seal is missing or unsafe')
+ seal = json.loads(manifest_path.read_text())
+ require(seal.get('schemaVersion') == 1 and seal.get('canonicalCasePassClaimed') is False
+ and seal.get('unknownMutationMayBePresent') is True
+ and seal.get('namespaceDeletionPermitted') is False and seal.get('backupRestorationPermitted') is False
+ and seal.get('originalFilesSha256') == successor_files(folder),
+ 'I05 successor original evidence differs from its retention seal')
+ binding, scene = one('binding'), one('original-failure-scene')
+ require(re.fullmatch(rf'gh-{failed_run}-[1-9][0-9]*-h[12]', binding.get('runId', ''))
+ and candidate.get('source_sha') == binding.get('candidateCommit') == provenance.get('source_commit') == CANDIDATE
+ and candidate.get('package_sha256') == binding.get('candidatePackageSha256')
+ == provenance.get('candidate_sha256') == seal.get('candidatePackageSha256')
+ and binding.get('harnessCommit') == provenance.get('testing_commit')
+ and binding.get('candidateBinarySha256') == provenance.get('binary_sha256'),
+ 'I05 successor archive changed run, candidate, helper or binary')
+ plan = successor_plan(folder, binding, scene)
+ require(seal.get('plan') == plan, 'I05 successor preservation plan differs from original evidence')
+ return [plan]
+
+
+def archived_plan(artifact, failed_run, candidate):
+ """Refuse uncertain effects, later actions, mixed cases and physical drift.
+
+ This is preservation, with no S3 call, backup restoration or verdict change.
+ A contender is accepted only for the reviewed helper's exact refused PUT.
+ """
+ require(type(failed_run) is int and failed_run > 0, 'I05 requires an exact failed run')
+ paths = list(artifact.glob('*/run/*/cases/*.json'))
+ require(len(paths) == 1 and paths[0].stem == CASE, 'I05 cannot omit another failed case')
+ def read(path):
+ require(path.is_file() and not path.is_symlink() and path.stat().st_size <= 2 * 1024**2,
+ 'I05 archive member is missing, unsafe or oversized')
+ return json.loads(path.read_text())
+ result = read(paths[0])
+ provenance = result.get('provenance') or {}
+ summary_path = paths[0].parents[1] / 'evidence' / CASE / 'healthy-summary.json'
+ if read(summary_path).get('diagnosticBranch') == 'queued-successor':
+ return successor_archived_plan(paths[0], failed_run, candidate)
+ if provenance.get('testing_commit') == FILTER_STARTUP_HELPER:
+ return filter_startup_plan(paths[0], failed_run, candidate)
+ if provenance.get('testing_commit') == H2_FILTER_STARTUP_HELPER:
+ return h2_filter_startup_plan(paths[0], failed_run, candidate)
+ require(result.get('case_id') == CASE and result.get('verdict') == result.get('cleanup_status') == 'FAIL'
+ and provenance.get('source_commit') == candidate.get('source_sha') == CANDIDATE
+ and provenance.get('candidate_sha256') == candidate.get('package_sha256')
+ and provenance.get('testing_commit') in FAILURES,
+ 'I05 requires its original failed candidate and reviewed helper boundary')
+ helper = provenance['testing_commit']
+ failure = FAILURES[helper]
+ evidence = paths[0].parents[1] / 'evidence' / CASE
+ summary = read(evidence / 'healthy-summary.json')
+ row = {'topology': 'H1', 'diagnosticComplete': False, 'cleanup': 'FAIL',
+ 'wholeCasePassClaimed': False, 'canonicalCasePassClaimed': False,
+ 'failure': failure, 'namespacesRetained': True}
+ diagnostics = summary.get('diagnostics') or []
+ if len(diagnostics) == 2 and diagnostics[0].get('topology') == 'H1':
+ topology = 'H2'
+ row = {**row, 'topology': 'H2'}
+ h1 = diagnostics[0]
+ h1_dir = evidence / 'H1'
+ h1_results = list(h1_dir.glob('*-diagnostic-result.json'))
+ require(helper in (CONTENDER_HELPER, H2_CONTENDER_HELPER)
+ and h1.get('diagnosticComplete') is True and h1.get('cleanup') == 'PASS'
+ and h1.get('wholeCasePassClaimed') is False and h1.get('canonicalCasePassClaimed') is False
+ and len(h1_results) == 1,
+ 'I05 H2 isolation requires a completed and cleaned H1 diagnostic')
+ h1_result = read(h1_results[0])
+ require(h1_result.get('topology') == 'H1' and h1_result.get('diagnosticComplete') is True
+ and h1_result.get('cleanup') == 'PASS'
+ and h1_result.get('wholeCasePassClaimed') is False
+ and h1_result.get('canonicalCasePassClaimed') is False,
+ 'I05 H1 diagnostic result does not prove completed cleanup')
+ require(diagnostics[1] == row, 'I05 H2 failure differs from its exact known boundary')
+ elif helper in DRAIN_HELPERS:
+ topology = 'H1'
+ require(diagnostics == [row], 'I05 drain failure differs from its exact H1 boundary')
+ else:
+ topology = 'H1'
+ require(diagnostics == [row], 'I05 H1 failure differs from its exact known boundary')
+ require(summary.get('caseId') == CASE and summary.get('cleanup') == 'FAIL'
+ and summary.get('diagnostics') == diagnostics and summary.get('complete') is False
+ and summary.get('diagnosticComplete') is False
+ and summary.get('requestedTopologies') == ['H1', 'H2']
+ and summary.get('canonicalCasePassClaimed') is False
+ and (topology == 'H2' or not (evidence / 'H2').exists()),
+ 'I05 requires one retained failure and no canonical acceptance claim')
+ folder = evidence / topology
+ if topology == 'H2':
+ archived_names = sorted(path.name for path in folder.iterdir())
+ actual_labels = [re.sub(r'^\d{5}-', '', name)[:-5] for name in archived_names if name.endswith('.json')]
+ checkpoint_end = actual_labels.index('I05-caller-checkpoint') + 4
+ first_socket = actual_labels.index('I05-caller-before-disconnect-socket')
+ first_admission = actual_labels.index('I05-caller-admission-resources')
+ last_admission = first_admission + 4
+ first_quiescent = actual_labels.index('quiescent')
+ window_rounds = ((first_socket - checkpoint_end) // 4,
+ (first_admission - first_socket - 1) // 4,
+ (first_quiescent - last_admission) // 4)
+ require(all(rounds >= 1 for rounds in window_rounds)
+ and first_socket - checkpoint_end == 4 * window_rounds[0]
+ and first_admission - first_socket - 1 == 4 * window_rounds[1]
+ and first_quiescent - last_admission == 4 * window_rounds[2],
+ 'I05 H2 lock windows do not contain complete four-peer sampling rounds')
+ labels = h2_labels(window_rounds)
+ elif helper in DRAIN_HELPERS:
+ labels = drain_failure_labels(folder)
+ else:
+ labels = LABELS[helper]
+ numbered = [(label, f'{index:05d}-{label}.json') for index, label in enumerate(labels, 1)]
+ require({path.name for path in folder.iterdir()} == {name for _, name in numbered} | {'fixture', 'timeout'},
+ 'I05 archive contains another action or lacks its original boundary')
+ def one(label):
+ matches = [name for current, name in numbered if current == label]
+ require(len(matches) == 1, 'I05 archive lacks one exact ' + label)
+ return read(folder / matches[0])
+ def many(label):
+ return [read(folder / name) for current, name in numbered if current == label]
+ binding, scene = one('binding'), one('original-failure-scene')
+ run_id, bucket = binding.get('runId', ''), binding.get('bucketPrefix', '')
+ suffix = topology.lower()
+ nodes = ['vm000'] if topology == 'H1' else ['vm000', 'vm001', 'vm002', 'vm003']
+ endpoints = {f'node{index}': f'http://rustfs-node{index}:9000'
+ for index in range(1, len(nodes) + 1)}
+ require(re.fullmatch(rf'gh-{failed_run}-[1-9][0-9]*-{suffix}', run_id)
+ and bucket == 'rv-heal-' + hashlib.sha256(run_id.encode()).hexdigest()[:16]
+ and binding.get('candidateCommit') == CANDIDATE
+ and binding.get('candidatePackageSha256') == candidate['package_sha256']
+ and binding.get('candidateBinarySha256') == provenance.get('binary_sha256')
+ and binding.get('harnessCommit') == helper
+ and binding.get('endpoints') == endpoints
+ and binding.get('defaultEndpoint') == 'node1' and one('diagnostic-result') == row,
+ 'I05 original run, package, helper, binary or failure boundary differs')
+ reservations = many('reserved')
+ owners = {item.get('node'): item.get('owner') for item in reservations}
+ require(len(reservations) == len(nodes) and set(owners) == set(nodes)
+ and all(item.get('reserved') is True for item in reservations)
+ and all(owner.get('runtimeProfile') == 'lock-timeout' for owner in owners.values())
+ and set(scene) == set(nodes),
+ 'I05 requires every original reserved node and retained scene')
+ owner = owners['vm000']
+ windows = many('I05-caller-window')
+ sample_counts = {node: sum(window.get('node') == node for window in windows) for node in nodes}
+ baselines = {item.get('node'): item for item in many('I05-caller-baseline-resources')}
+ if topology == 'H2':
+ attestation = one('integrity-fleet-attestation')
+ actual = one('actual-topology')
+ uploads = {item.get('node'): item for item in many('inspector-upload')}
+ require(attestation.get('nodes') == nodes and attestation.get('allParticipatingNodesUseCandidate') is True
+ and actual.get('topology') == 'H2' and actual.get('nodes') == 4
+ and actual.get('independentDrives') == 16
+ and set(uploads) == set(nodes)
+ and all(uploads[node].get('installed') is True
+ and uploads[node].get('inspectorSha256') == owners[node].get('inspectorSha256')
+ for node in nodes)
+ and set(many('preflight')[0]) == set(nodes)
+ and all(many('preflight')[0][node].get('hostname') == owners[node].get('hostname')
+ and many('preflight')[0][node].get('mounts') == owners[node].get('mounts')
+ for node in nodes)
+ and set(many('deployed-topology')[0]) == set(nodes)
+ and set(many('started')[0]) == set(nodes)
+ and {item.get('node') for item in many('original-failure-logs')} == set(nodes)
+ and {item.get('node') for item in baselines.values()} == set(nodes)
+ and {item.get('node') for item in many('I05-caller-checkpoint')} == set(nodes)
+ and {item.get('node') for item in many('I05-caller-admission-resources')} == set(nodes)
+ and len(set(sample_counts.values())) == 1 and min(sample_counts.values()) >= 3,
+ 'I05 H2 evidence omits a peer, inspector upload, lock checkpoint or service log')
+ admission = {item.get('node'): item for item in many('I05-caller-admission-resources')}
+ logs = {item.get('node'): item for item in many('original-failure-logs')}
+ require(set(baselines) == set(admission) == set(logs) == set(nodes)
+ and all(type(baselines[node].get('fileDescriptors')) is int
+ and type(admission[node].get('fileDescriptors')) is int
+ and baselines[node].get('identity', {}).get('binarySha256') == binding['candidateBinarySha256']
+ and admission[node].get('identity', {}).get('binarySha256') == binding['candidateBinarySha256']
+ and baselines[node].get('identity', {}).get('settings') == runtime_config('lock-timeout')
+ and admission[node].get('identity', {}).get('settings') == runtime_config('lock-timeout')
+ and logs[node].get('unit') == owners[node].get('unit')
+ and isinstance(logs[node].get('journal'), str) and bool(logs[node]['journal'])
+ for node in nodes),
+ 'I05 H2 resource identity, runtime settings or original service logs differ')
+ for node in nodes:
+ same_process(admission[node].get('identity'), baselines[node].get('identity'))
+ inspector = one('inspector-build')
+ require(inspector.get('candidateCommit') == CANDIDATE
+ and all(inspector.get('binarySha256') == value.get('inspectorSha256') for value in owners.values())
+ and inspector.get('built') is True, 'I05 original inspector identity differs')
+ for node in nodes:
+ original = scene[node]
+ node_owner = owners[node]
+ require(original.get('quiescent') is True and type(original.get('pid')) is int and original['pid'] == 0
+ and original.get('hostname') == node_owner.get('hostname')
+ and original.get('unit') == node_owner.get('unit')
+ and original.get('binarySha256') == node_owner.get('binarySha256') == binding['candidateBinarySha256']
+ and len(original.get('formats', [])) == 4
+ and {part.get('disk') for part in original['formats']} == set(range(4)),
+ 'I05 stopped four-drive original physical scene changed on ' + node)
+ require(physical_content(scene) == physical_content(one('I05-timeout-original-physical'))
+ == physical_content(one('I05-timeout-baseline-physical-after-public-read')),
+ 'I05 stopped four-drive original physical scene changed')
+
+ # Replay only the bounded seed ledger locally. No result is inferred from
+ # current LISTs, and a missing, unknown or failed mutation refuses recovery.
+ fixture = folder / 'fixture'
+ names = {f'{i:06d}-{kind}.json' for i in range(1, 10) for kind in ('request', 'ack')}
+ require({path.name for path in fixture.iterdir()} == names, 'I05 requires all nine seed request/ACK pairs')
+ expected, journal, public_properties, seen, domain = [], {}, {}, set(), None
+ for sequence in range(1, 10):
+ request_path = fixture / f'{sequence:06d}-request.json'
+ ack_path = fixture / f'{sequence:06d}-ack.json'
+ request, ack = read(request_path), read(ack_path)
+ operation = 'create_bucket' if sequence == 1 else 'put_bucket_versioning' if sequence == 2 else (
+ 'delete_object' if sequence == 6 else 'put_object')
+ params = {'Bucket': bucket}
+ if sequence == 2:
+ params['VersioningConfiguration'] = {'Status': 'Enabled'}
+ elif sequence >= 3:
+ index, generation = (0, sequence - 3) if sequence <= 6 else (1, sequence - 7)
+ key = f'objects/{index:04d}.bin'
+ params['Key'] = key
+ if operation == 'put_object':
+ params.update(ContentType='application/octet-stream', Metadata={'heal-fixture': 'DV',
+ 'nonce': f'{index}-{generation}'}, Tagging='suite=heal&fixture=DV')
+ domain = domain or request.get('clockDomain')
+ require(isinstance(domain, str) and bool(domain)
+ and request.get('schemaVersion') == ack.get('schemaVersion') == 2
+ and request.get('runId') == ack.get('runId') == run_id
+ and request.get('sequence') == ack.get('sequence') == sequence
+ and request.get('operation') == operation and request.get('params') == params
+ and request.get('expectHttp') is None and request.get('identity') is None
+ and request.get('clockDomain') == ack.get('clockDomain') == domain
+ and ack.get('requestFile') == request_path.name
+ and ack.get('requestSha256') == hashlib.sha256(request_path.read_bytes()).hexdigest()
+ and ack.get('operationCheck') == 'PASS', 'I05 seed request/ACK binding differs')
+ observation = ack.get('observation') or {}
+ response = observation.get('response') or {}
+ require(observation.get('operation') == operation and observation.get('params') == params
+ and observation.get('endpoint') == 'node1' and observation.get('clockDomain') == domain
+ and observation.get('clientCallInvoked') is True and observation.get('acknowledged') is True
+ and observation.get('resultUnknown') is False and not observation.get('clientCleanupError')
+ and response.get('ResponseMetadata', {}).get('HTTPStatusCode') == (204 if sequence == 6 else 200)
+ and response.get('ResponseMetadata', {}).get('RetryAttempts') == 0,
+ 'I05 seed contains an uncertain or rejected mutation')
+ if sequence >= 3:
+ version = response.get('VersionId', '')
+ require(str(uuid.UUID(version)) == version and uuid.UUID(version).int and version not in seen,
+ 'I05 seed has an invalid or repeated version')
+ seen.add(version)
+ entry = {'key': key, 'versionId': version, 'deleteMarker': sequence == 6,
+ 'isLatest': sequence in (6, 9)}
+ if sequence == 6:
+ require(response.get('DeleteMarker') is True, 'I05 marker DELETE lacks its acknowledged marker')
+ else:
+ body = payload(f'{bucket}/{key}/{generation}', 32 * 1024**2 if index == 0 else 1024)
+ expected_payload = {'size': len(body), 'sha256': hashlib.sha256(body).hexdigest()}
+ require(request.get('payload') == observation.get('payload') == expected_payload,
+ 'I05 seed payload differs from the deterministic request')
+ require(isinstance(response.get('ETag'), str) and bool(response['ETag']),
+ 'I05 seed PUT lacks its acknowledged ETag')
+ public_properties[(key, version)] = {'ETag': response['ETag'], 'ContentLength': len(body),
+ 'ContentType': params['ContentType'], 'Metadata': params['Metadata']}
+ entry['payload'] = expected_payload
+ expected.append(entry)
+ for path in (request_path, ack_path):
+ journal[path.relative_to(folder).as_posix()] = hashlib.sha256(path.read_bytes()).hexdigest()
+ baseline = one('original-ACK-bound-S3')
+ require(baseline.get('bucket') == bucket and baseline == one('I05-timeout-original-S3'),
+ 'I05 original public baseline differs')
+ verify_seed(expected, baseline)
+ for entry in baseline['entries']:
+ if entry['deleteMarker']:
+ continue
+ require(all(entry.get('properties', {}).get(field) == value
+ for field, value in public_properties[(entry['key'], entry['versionId'])].items())
+ and entry.get('tags') == [{'Key': 'fixture', 'Value': 'DV'}, {'Key': 'suite', 'Value': 'heal'}],
+ 'I05 original public metadata, ETag or tags differ from seed request/ACK')
+
+ timeout = folder / 'timeout'
+ expected_timeout_items = {'scope.json', 'failure.json', 'caller'}
+ expected_caller_items = {'scope.json', 'failure.json', 'transport'}
+ has_contender = helper in {CONTENDER_HELPER, H2_CONTENDER_HELPER} | DRAIN_HELPERS
+ if has_contender:
+ expected_timeout_items.add('contender')
+ expected_caller_items.add('admission.json')
+ if helper in DRAIN_HELPERS:
+ expected_timeout_items.add('timeout.json')
+ require({p.name for p in timeout.iterdir()} == expected_timeout_items
+ and {p.name for p in (timeout / 'caller').iterdir()} == expected_caller_items,
+ 'I05 archive contains an unreviewed action or lacks its exact failure boundary')
+ timeout_failure = read(timeout / 'failure.json')
+ caller_failure = read(timeout / 'caller/failure.json')
+ expected_failure_state = 'NOT_OBSERVED' if helper in DRAIN_HELPERS else 'INCOMPLETE'
+ require(timeout_failure.get('diagnosticComplete') is False
+ and timeout_failure.get('errorType') == failure['type']
+ and timeout_failure.get('state') == expected_failure_state
+ and timeout_failure.get('canonicalCasePassClaimed') is False
+ and timeout_failure.get('originalRequestsAndResponsesRetained') is True
+ and caller_failure.get('observationComplete') is False
+ and caller_failure.get('errorType') == failure['type']
+ and caller_failure.get('state') == expected_failure_state
+ and caller_failure.get('canonicalCasePassClaimed') is False
+ and caller_failure.get('originalTransportEvidenceRetained') is True,
+ 'I05 nested failure records differ from the exact retained diagnostic failure')
+ scope = read(timeout / 'scope.json')
+ entry = scope.get('entry')
+ require(entry in expected and entry.get('key') == 'objects/0000.bin' and not entry['isLatest']
+ and entry.get('deleteMarker') is False and scope.get('runId') == run_id
+ and scope.get('candidateCommit') == CANDIDATE
+ and scope.get('bindingSha256') == hashlib.sha256(canonical(binding).encode()).hexdigest(),
+ 'I05 caller target is not its acknowledged historical seed')
+ transport = timeout / 'caller/transport'
+ require({p.name for p in transport.iterdir()} == {'request.json', 'request-written.json', 'disconnected.json'},
+ 'I05 caller transport contains another request or result')
+ request, sent, closed = (read(transport / name)
+ for name in ('request.json', 'request-written.json', 'disconnected.json'))
+ caller_transport_request = request
+ target = f'/{bucket}/{entry["key"]}?versionId={entry["versionId"]}'
+ require(request.get('operation') == 'get_object' and request.get('params') == {
+ 'Bucket': bucket, 'Key': entry['key'], 'VersionId': entry['versionId']}
+ and request.get('endpoint') == 'node1' and request.get('endpointUrl') == binding['endpoints']['node1']
+ and all(request.get(field) == binding[field] for field in (
+ 'candidateCommit', 'harnessCommit', 'candidatePackageSha256', 'candidateBinarySha256'))
+ and request.get('bindingSha256') == scope['bindingSha256']
+ and closed.get('clientSocketClosed') is True and closed.get('connectionAttempts') == 1
+ and type(closed.get('clientClosedMonotonicNs')) is int
+ and closed['clientClosedMonotonicNs'] >= sent.get('requestWrittenMonotonicNs', 0),
+ 'I05 transport is not the sole closed read-only historical GET')
+ for value in (request, sent, closed):
+ require(value.get('runId') == run_id and value.get('requestTarget') == target
+ and value.get('callerMarker') == request.get('callerMarker')
+ and re.fullmatch(r'auto-testing-i05/[0-9a-f-]{36}', value.get('callerMarker', ''))
+ and value.get('clockDomain') == domain and value.get('retryAllowed') is False
+ and value.get('canonicalCasePassClaimed') is False,
+ 'I05 closed caller lost its exact request identity')
+ for value in (sent, closed):
+ require(value.get('requestSha256') == hashlib.sha256((transport / 'request.json').read_bytes()).hexdigest()
+ and value.get('requestWriteCompleted') is True
+ and value.get('resultUnknown') is True and value.get('acknowledged') is False
+ and value.get('serverOwnershipProven') is False,
+ 'I05 closed caller was retried or promoted to an acknowledged result')
+ if helper in DRAIN_HELPERS:
+ caller_scope = read(timeout / 'caller/scope.json')
+ require(caller_scope.get('runId') == run_id and caller_scope.get('candidateCommit') == CANDIDATE
+ and caller_scope.get('bindingSha256') == scope['bindingSha256']
+ and caller_scope.get('entry') == entry
+ and caller_scope.get('admissionSeconds') == ADMISSION_SECONDS
+ and caller_scope.get('drainSeconds') == DRAIN_SECONDS
+ and caller_scope.get('canonicalCasePassClaimed') is False
+ and caller_scope.get('mutatesServiceConfiguration') is False
+ and scope.get('minimumCallerPayloadBytes') == MIN_BACKPRESSURE_BYTES
+ and scope.get('receiveBufferBytesRequested') == CALLER_RECEIVE_BUFFER_BYTES
+ and request.get('receiveBufferBytesRequested') == CALLER_RECEIVE_BUFFER_BYTES
+ and sent.get('requestInvocationStarted') is True
+ and closed.get('responseObserved') is False and closed.get('connectionAttempts') == 1,
+ 'I05 drain archive changed its bounded read-only caller or time budget')
+ if has_contender:
+ contender = timeout / 'contender'
+ require({p.name for p in contender.iterdir()} == {'000001-request.json', '000001-ack.json'},
+ 'I05 contender archive is not the sole one-shot request and ACK')
+ request_path, ack_path = contender / '000001-request.json', contender / '000001-ack.json'
+ request_bytes = request_path.read_bytes()
+ request, ack = read(request_path), read(ack_path)
+ body = payload(f'{run_id}/I05-timeout-contender', CONTENDER_BYTES)
+ response = validate_timeout_ack(ack, request, request_bytes, run_id, 'node1', bucket,
+ entry['key'], body, clock_domain=domain)
+ require(ack.get('operationCheck') == 'PASS'
+ and ack.get('observation', {}).get('acknowledged') is False
+ and ack.get('observation', {}).get('resultUnknown') is False,
+ 'I05 contender result is not a durable, known refusal')
+ caller_admission = read(timeout / 'caller/admission.json')
+ holder = (caller_admission.get('attempt') or {})
+ require(holder.get('admitted') is True and holder.get('observedAcquiredWithoutDrop') is True,
+ 'I05 contender lacks its original admitted GET lock holder')
+ require(set(baselines) == set(nodes) and len(windows) == sample_counts['vm000'] * len(nodes),
+ 'I05 contender archive lacks each peer journal window')
+ rows = {node: [] for node in nodes}
+ previous_cursors = {}
+ window_counts = {node: 0 for node in nodes}
+ for window in windows:
+ node = window.get('node')
+ require(node in rows and window.get('complete') is True
+ and window.get('unit') == owners[node]['unit'] and not window.get('sourceMessageLoss')
+ and isinstance(window.get('beforeCursor'), str)
+ and isinstance(window.get('afterCursor'), str)
+ and (node not in previous_cursors or window['beforeCursor'] == previous_cursors[node])
+ and isinstance(window.get('journal'), str),
+ 'I05 contender lock journal is incomplete or belongs to another service')
+ previous_cursors[node] = window['afterCursor']
+ window_counts[node] += 1
+ for line in window['journal'].splitlines():
+ try:
+ event = json.loads(line)
+ except json.JSONDecodeError as error:
+ raise ValueError('I05 contender lock journal contains malformed JSON') from error
+ require(isinstance(event, dict), 'I05 contender lock journal contains a non-object event')
+ rows[node].append(event)
+ require(len(set(window_counts.values())) == 1 and min(window_counts.values()) >= 3,
+ 'I05 contender lock windows are incomplete or imbalanced across H2 peers')
+ require(all(not any(event.get('request_id') == response.get('ResponseMetadata', {}).get('RequestId')
+ for event in events) for node, events in rows.items() if node != 'vm000'),
+ 'I05 contender request ID appeared on a foreign endpoint')
+ proof = timeout_proof(rows['vm000'], response, holder, bucket, entry['key'])
+ require(proof is not None and proof.get('typedTimeoutBeyondBudgetObserved') is True
+ and proof.get('writeLockAcquired') is False,
+ 'I05 contender ACK lacks its exact failed target write-lock attempt')
+ if helper in DRAIN_HELPERS:
+ caller_request_id = holder.get('request_id')
+ caller_lock_id = holder.get('lock_attempt_id')
+ caller_events = holder.get('events') or []
+ caller_socket = caller_admission.get('socket') or {}
+ require(caller_admission.get('node') == 'vm000'
+ and holder.get('states') == ['acquiring', 'acquired']
+ and holder.get('diagnosticGuardDropped') is False
+ and holder.get('distributedReleaseProven') is False
+ and holder.get('heldAtDisconnectProven') is False
+ and isinstance(caller_request_id, str) and str(uuid.UUID(caller_request_id)) == caller_request_id
+ and uuid.UUID(caller_request_id).int
+ and isinstance(caller_lock_id, str) and str(uuid.UUID(caller_lock_id)) == caller_lock_id
+ and uuid.UUID(caller_lock_id).int and len(caller_events) == 2
+ and [event.get('state') for event in caller_events] == ['acquiring', 'acquired'],
+ 'I05 caller lacks the exact admitted GET owner at its intentional disconnect')
+ caller_uri = f'/{bucket}/{entry["key"]}?versionId={entry["versionId"]}'
+ for event in caller_events:
+ require(event.get('target') == TARGET and event.get('request_id') == caller_request_id
+ and event.get('lock_attempt_id') == caller_lock_id
+ and event.get('event') == 'object_namespace_lock' and event.get('component') == 'ecstore'
+ and event.get('subsystem') == 'object_lock' and event.get('op') == 'get_object'
+ and event.get('mode') == 'read' and event.get('bucket') == bucket
+ and event.get('object') == entry['key']
+ and event.get('requested_version_id') == entry['versionId']
+ and event.get('owner') == holder.get('owner')
+ and event.get('namespace') == holder.get('namespace')
+ and len([span for span in event.get('spans', [])
+ if span.get('name') == 'http-request'
+ and span.get('request_id') == caller_request_id
+ and span.get('method') == 'GET' and span.get('uri') == caller_uri
+ and span.get('user_agent') == caller_transport_request.get('callerMarker')]) == 1,
+ 'I05 admitted caller lock owner is not bound to its exact historical GET')
+ caller_lock_rows = [event for event in rows['vm000']
+ if event.get('target') == TARGET and event.get('request_id') == caller_request_id]
+ require([event.get('state') for event in caller_lock_rows] == ['acquiring', 'acquired', 'guard_dropped']
+ and all(event.get('lock_attempt_id') == caller_lock_id
+ and event.get('op') == 'get_object' and event.get('mode') == 'read'
+ and event.get('bucket') == bucket and event.get('object') == entry['key']
+ and event.get('requested_version_id') == entry['versionId']
+ and event.get('owner') == holder.get('owner')
+ and event.get('namespace') == holder.get('namespace')
+ for event in caller_lock_rows),
+ 'I05 caller journal lost or changed its original GET lock lifecycle')
+ dropped = [event for event in rows['vm000']
+ if event.get('request_id') == caller_request_id
+ and event.get('event') == 'get_object_stream_body']
+ require(len(dropped) == 1 and dropped[0].get('state') == 'dropped_incomplete'
+ and dropped[0].get('bucket') == bucket and dropped[0].get('object') == entry['key'],
+ 'I05 caller disconnect lacks its exact incomplete response-body event')
+ mutations = []
+ for event in rows['vm000']:
+ spans = [event.get('span'), *(event.get('spans') or [])]
+ for span in spans:
+ if isinstance(span, dict) and span.get('method') in {'PUT', 'POST', 'PATCH', 'DELETE'}:
+ mutations.append((event.get('request_id'), span.get('method'), span.get('uri')))
+ require(mutations and all(request_id == response['ResponseMetadata']['RequestId']
+ and method == 'PUT' and uri == f'/{bucket}/{entry["key"]}'
+ for request_id, method, uri in mutations),
+ 'I05 caller window contains another or unknown S3 mutation')
+ backpressure = one('I05-timeout-caller-backpressure-socket')
+ expected_process = caller_socket.get('process')
+ samples = backpressure.get('samples') or []
+ require(backpressure.get('node') == 'vm000' and backpressure.get('readOnly') is True
+ and backpressure.get('process') == expected_process
+ and backpressure.get('clockDomain') == caller_socket.get('clockDomain')
+ and backpressure.get('networkNamespace') == caller_socket.get('networkNamespace')
+ and backpressure.get('clientAddress') == sent.get('localAddress')
+ and backpressure.get('serverAddress') == sent.get('peerAddress')
+ and len(samples) == 2
+ and all(sample.get('vanishedDescriptors') == 0
+ and len(sample.get('ownedMatches') or []) == 1
+ and sample['ownedMatches'][0].get('state') == '01'
+ and type(sample['ownedMatches'][0].get('txQueueBytes')) is int
+ and sample['ownedMatches'][0]['txQueueBytes'] > 0
+ for sample in samples)
+ and len({sample['ownedMatches'][0].get('inode') for sample in samples}) == 1,
+ 'I05 contender is not bound to the original backpressured caller socket')
+ timeout_proof_record = read(timeout / 'timeout.json')
+ require(timeout_proof_record.get('requestId') == response['ResponseMetadata']['RequestId']
+ and timeout_proof_record.get('lockAttemptId') == proof['lockAttemptId']
+ and timeout_proof_record.get('owner') == proof['owner']
+ and timeout_proof_record.get('namespace') == proof['namespace']
+ and timeout_proof_record.get('events') == proof['events']
+ and timeout_proof_record.get('typedTimeoutBeyondBudgetObserved') is True
+ and timeout_proof_record.get('writeLockAcquired') is False
+ and timeout_proof_record.get('backpressureSocket') == backpressure
+ and timeout_proof_record.get('readResponseBackpressureObservedAfterTimeout') is True
+ and timeout_proof_record.get('canonicalCasePassClaimed') is False,
+ 'I05 contender timeout proof differs from the exact archived lock and socket evidence')
+ baseline = baselines.get('vm000')
+ drain_sockets = many('I05-caller-drain-socket')
+ drain_resources = many('I05-caller-drain-resources')
+ first_drain = next(index for index, (label, _) in enumerate(numbered)
+ if label == 'I05-caller-drain-socket')
+ drain_windows = [read(folder / name) for label, name in numbered[first_drain:]
+ if label == 'I05-caller-window']
+ require(baseline is not None and len(drain_sockets) == len(drain_resources) >= 1
+ and len(drain_windows) in {len(drain_sockets), len(drain_sockets) - 1},
+ 'I05 drain archive lacks a complete service and socket sample pair')
+ def same_socket_process(value):
+ process = value.get('process') or {}
+ expected = baseline['identity']
+ require(all(process.get(name) == expected.get(name) for name in (
+ 'pid', 'startTicks', 'bootId', 'unit', 'executable', 'binarySha256', 'mountNamespace',
+ 'commandSha256', 'environmentFileSha256', 'settings')),
+ 'I05 drain socket sample crossed its original service process')
+ for socket_state, resource in zip(drain_sockets, drain_resources, strict=True):
+ same_socket_process(socket_state)
+ require(socket_state.get('node') == resource.get('node') == 'vm000'
+ and socket_state.get('clockDomain') == caller_socket.get('clockDomain')
+ and socket_state.get('networkNamespace') == caller_socket.get('networkNamespace')
+ and socket_state.get('clientAddress') == sent.get('localAddress')
+ and socket_state.get('serverAddress') == sent.get('peerAddress')
+ and socket_state.get('readOnly') is True
+ and socket_state.get('continuousAbsenceProven') is False
+ and len(socket_state.get('samples') or []) == 2
+ and all(sample.get('vanishedDescriptors') == 0 and not sample.get('ownedMatches')
+ for sample in socket_state['samples'])
+ and type(resource.get('fileDescriptors')) is int and resource['fileDescriptors'] >= 0
+ and resource.get('readOnly') is True
+ and resource.get('identity', {}).get('settings') == baseline['identity'].get('settings'),
+ 'I05 drain observation contains a changed process, socket or resource sample')
+ same_process(resource.get('identity'), baseline['identity'])
+ if helper == DESCRIPTOR_HELPER:
+ resource_states = [baseline, one('I05-caller-admission-resources'), *drain_resources]
+ for resource in resource_states:
+ same_process(resource.get('identity'), baseline['identity'])
+ census = resource.get('descriptorCensus')
+ validate_census(census)
+ require(resource.get('node') == 'vm000' and resource.get('readOnly') is True
+ and resource.get('clockDomain') == baseline['identity']['bootId']
+ and resource.get('networkNamespace') == caller_socket.get('networkNamespace')
+ and resource.get('fileDescriptors') == census['fileDescriptors']
+ and resource['identity'].get('settings') == {
+ **runtime_config('lock-timeout'), 'RUSTFS_HEAL_TASK_TIMEOUT_SECS': '300'},
+ 'I05 resource census crossed its original process or network namespace')
+ for socket_state in [one('I05-caller-before-disconnect-socket'), backpressure, *drain_sockets]:
+ same_socket_process(socket_state)
+ require(socket_state.get('networkNamespace') == baseline.get('networkNamespace')
+ and socket_state.get('clockDomain') == baseline['identity']['bootId']
+ and socket_state.get('clientAddress') == sent.get('localAddress')
+ and socket_state.get('serverAddress') == sent.get('peerAddress'),
+ 'I05 socket census crossed its original caller or network namespace')
+ for sample in socket_state.get('samples') or []:
+ validate_census(sample)
+ matches = [{k: v for k, v in row.items()
+ if k not in {'localAddress', 'remoteAddress'}}
+ for row in sample['ownedTcpConnections']
+ if row['localAddress'] == socket_state['serverAddress']
+ and row['remoteAddress'] == socket_state['clientAddress']]
+ require(sample.get('ownedMatches') == matches,
+ 'I05 caller socket census lost its exact owned TCP join')
+ write_rows = [event for event in rows['vm000'] if event.get('mode') == 'write']
+ require(write_rows == proof['events']
+ and [event.get('state') for event in write_rows] == ['acquiring', 'failed'],
+ 'I05 drain journal contains a successful or unbound namespace write')
+ for path in timeout.rglob('*.json'):
+ read(path)
+ journal[path.relative_to(folder).as_posix()] = hashlib.sha256(path.read_bytes()).hexdigest()
+ plan = {'kind': 'unresolved-i05-timeout', 'runId': run_id, 'nodes': nodes, 'bucket': bucket,
+ 'owners': owners, 'candidateCommit': CANDIDATE, 'harnessCommit': helper,
+ 'binarySha256': binding['candidateBinarySha256'], 'ackFilesSha256': digest(journal),
+ 'businessSceneSha256': {node: digest(physical_content(scene)[node]) for node in nodes}}
+ validate(plan)
+ return [plan]
diff --git a/scripts/validation_heal/tools/heal_meta_inspect.rs b/scripts/validation_heal/tools/heal_meta_inspect.rs
new file mode 100644
index 00000000..fe03b16e
--- /dev/null
+++ b/scripts/validation_heal/tools/heal_meta_inspect.rs
@@ -0,0 +1,286 @@
+// Read-only inspector linked to the exact candidate's rustfs-filemeta crate.
+// Do not print inline bytes, user metadata, encryption envelopes, or IAM data.
+use rustfs_filemeta::FileMeta;
+use rustfs_utils::HashAlgorithm;
+use serde_json::json;
+
+fn bounded_file(path: &str) -> Result, Box> {
+ let metadata = std::fs::symlink_metadata(path)?;
+ if !metadata.is_file() || metadata.len() > 64 * 1024 * 1024 {
+ return Err("expected bounded, non-symlink evidence file".into());
+ }
+ Ok(std::fs::read(path)?)
+}
+
+fn sha256(bytes: &[u8]) -> String {
+ HashAlgorithm::SHA256
+ .hash_encode(bytes)
+ .as_ref()
+ .iter()
+ .map(|byte| format!("{byte:02x}"))
+ .collect()
+}
+
+fn verify_framing(
+ bytes: &[u8],
+ logical_size: usize,
+ block_size: usize,
+ data_blocks: usize,
+ legacy: bool,
+ algorithm: &HashAlgorithm,
+) -> Result> {
+ if logical_size == 0 || block_size == 0 || data_blocks == 0 {
+ return Err("positive bitrot verification requires supported nonempty geometry".into());
+ }
+ if !matches!(
+ algorithm,
+ HashAlgorithm::HighwayHash256S | HashAlgorithm::HighwayHash256SLegacy
+ ) {
+ return Err("unsupported bitrot framing; no integrity claim".into());
+ }
+ let mut remaining = logical_size;
+ let mut offset: usize = 0;
+ let mut blocks: usize = 0;
+ while remaining > 0 {
+ let logical = remaining.min(block_size);
+ let length = logical.div_ceil(data_blocks);
+ let length = if legacy { (length + 1) & !1 } else { length };
+ let end = offset
+ .checked_add(32 + length)
+ .ok_or("part size overflow")?;
+ let block = bytes.get(offset..end).ok_or("short shard read")?;
+ if algorithm.hash_encode(&block[32..]).as_ref() != &block[..32] {
+ return Err(format!("bitrot hash mismatch in block {blocks}").into());
+ }
+ offset = end;
+ remaining -= logical;
+ blocks += 1;
+ }
+ if offset != bytes.len() {
+ return Err("unexpected trailing shard bytes".into());
+ }
+ Ok(blocks)
+}
+
+fn verify_inline(args: &[String]) -> Result<(), Box> {
+ if args.len() != 6 {
+ return Err("usage: --verify-inline XL_META BUCKET KEY VERSION_ID".into());
+ }
+ let bytes = bounded_file(&args[2])?;
+ let meta = FileMeta::load(&bytes)?;
+ meta.data.validate()?;
+ let decoded = meta.into_file_info_versions(&args[3], &args[4], true)?;
+ let matches: Vec<_> = decoded
+ .versions
+ .iter()
+ .filter(|info| {
+ info.version_id
+ .filter(|id| !id.is_nil())
+ .map(|id| id.to_string())
+ .unwrap_or_else(|| "null".to_owned())
+ == args[5]
+ })
+ .collect();
+ if matches.len() != 1 {
+ return Err("inline verification requires one exact metadata version".into());
+ }
+ let info = matches[0];
+ if info.deleted
+ || !info.inline_data()
+ || info.parts.len() != 1
+ || info.parts[0].size == 0
+ || usize::try_from(info.parts[0].actual_size).ok() != Some(info.parts[0].size)
+ || usize::try_from(info.size).ok() != Some(info.parts[0].size)
+ {
+ return Err("unsupported inline version or part identity".into());
+ }
+ let data = meta.data.find(&args[5])?.ok_or("inline value is missing")?;
+ let algorithm = if info.uses_legacy_checksum {
+ HashAlgorithm::HighwayHash256SLegacy
+ } else {
+ info.erasure
+ .get_checksum_info(info.parts[0].number)
+ .algorithm
+ };
+ let blocks = verify_framing(
+ &data,
+ usize::try_from(info.parts[0].size)?,
+ info.erasure.block_size,
+ info.erasure.data_blocks,
+ info.uses_legacy_checksum,
+ &algorithm,
+ )?;
+ println!(
+ "{}",
+ json!({"schemaVersion": 1, "bucket": args[3], "key": args[4],
+ "versionId": args[5], "physicalBitrotVerified": true, "ecReconstructionVerified": false,
+ "algorithm": algorithm, "blocksVerified": blocks, "inlineLength": data.len(), "inlineSha256": sha256(&data)})
+ );
+ Ok(())
+}
+
+// This path checks stored framing without invoking the service or repairing
+// anything. EC reconstruction is checked separately against the frozen shard.
+fn verify_part(args: &[String]) -> Result<(), Box> {
+ if args.len() != 8 {
+ return Err(
+ "usage: --verify-part XL_META BUCKET KEY PART_FILE VERSION_ID PART_NUMBER".into(),
+ );
+ }
+ let meta = FileMeta::load(&bounded_file(&args[2])?)?;
+ let versions = meta.into_file_info_versions(&args[3], &args[4], true)?;
+ let matches: Vec<_> = versions
+ .versions
+ .iter()
+ .filter(|info| {
+ info.version_id
+ .filter(|id| !id.is_nil())
+ .map(|id| id.to_string())
+ .unwrap_or_else(|| "null".to_owned())
+ == args[6]
+ })
+ .collect();
+ if matches.len() != 1 {
+ return Err("part verification requires one exact metadata version".into());
+ }
+ let info = matches[0];
+ let number: usize = args[7].parse()?;
+ let part = info
+ .parts
+ .iter()
+ .find(|part| part.number == number)
+ .ok_or("part is absent from metadata")?;
+ let path = std::path::Path::new(&args[5]);
+ let data_dir = info
+ .data_dir
+ .ok_or("external part has no data directory")?
+ .to_string();
+ if info.deleted
+ || info.inline_data()
+ || info.erasure.data_blocks == 0
+ || info.erasure.block_size == 0
+ || part.size <= 0
+ || path.file_name().and_then(|name| name.to_str())
+ != Some(format!("part.{number}").as_str())
+ || path
+ .parent()
+ .and_then(|parent| parent.file_name())
+ .and_then(|name| name.to_str())
+ != Some(data_dir.as_str())
+ {
+ return Err("unsupported external part identity or geometry".into());
+ }
+ let algorithm = if info.uses_legacy_checksum {
+ HashAlgorithm::HighwayHash256SLegacy
+ } else {
+ info.erasure.get_checksum_info(number).algorithm
+ };
+ let bytes = bounded_file(&args[5])?;
+ let blocks = verify_framing(
+ &bytes,
+ usize::try_from(part.size)?,
+ info.erasure.block_size,
+ info.erasure.data_blocks,
+ info.uses_legacy_checksum,
+ &algorithm,
+ )?;
+ println!(
+ "{}",
+ json!({"schemaVersion": 1, "bucket": args[3], "key": args[4],
+ "versionId": args[6], "partNumber": number, "dataDir": data_dir,
+ "algorithm": algorithm, "blocksVerified": blocks, "physicalSize": bytes.len(),
+ "physicalBitrotVerified": true, "ecReconstructionVerified": false})
+ );
+ Ok(())
+}
+
+fn main() -> Result<(), Box> {
+ let args: Vec = std::env::args().collect();
+ if args.get(1).is_some_and(|value| value == "--verify-part") {
+ return verify_part(&args);
+ }
+ if args.get(1).is_some_and(|value| value == "--verify-inline") {
+ return verify_inline(&args);
+ }
+ if args.len() != 4 {
+ return Err("usage: heal-meta-inspect XL_META_FILE BUCKET KEY".into());
+ }
+ let metadata = std::fs::symlink_metadata(&args[1])?;
+ if !metadata.is_file() || metadata.len() > 64 * 1024 * 1024 {
+ return Err("expected bounded, non-symlink xl.meta evidence copy".into());
+ }
+ let bytes = std::fs::read(&args[1])?;
+ let meta = FileMeta::load(&bytes)?;
+ if meta.versions.is_empty() {
+ return Err("empty metadata is not an authoritative version manifest".into());
+ }
+ let decoded = meta.into_file_info_versions(&args[2], &args[3], true)?;
+ meta.data.validate()?;
+ let versions: Result, Box> = decoded
+ .versions
+ .iter()
+ .map(|info| -> Result<_, Box> {
+ let role = if info.erasure.index == 0 {
+ "not-an-erasure-shard"
+ } else if info.erasure.index <= info.erasure.data_blocks {
+ "data"
+ } else {
+ "parity"
+ };
+ // Locate only the exact decoded inline value. Exposing the bytes
+ // would leak user content; offset/length plus an occurrence count
+ // lets a bounded injector target the value instead of guessing at
+ // an xl.meta position.
+ let data_key = info
+ .version_id
+ .filter(|version_id| !version_id.is_nil())
+ .map(|version_id| version_id.to_string())
+ .unwrap_or_else(|| "null".to_string());
+ let decoded_inline = if info.inline_data() {
+ meta.data.find(&data_key)?
+ } else {
+ None
+ };
+ let inline = decoded_inline
+ .as_ref()
+ .filter(|data| !data.is_empty())
+ .map(|data| {
+ let offsets: Vec<_> = bytes
+ .windows(data.len())
+ .enumerate()
+ .filter_map(|(offset, candidate)| {
+ (candidate == data.as_slice()).then_some(offset)
+ })
+ .collect();
+ json!({
+ "length": data.len(),
+ "sha256": sha256(data),
+ "rawOccurrenceCount": offsets.len(),
+ "rawOffset": (offsets.len() == 1).then_some(offsets[0])
+ })
+ });
+ Ok(json!({
+ "versionId": info.version_id.map(|id| id.to_string()),
+ "isLatest": info.is_latest, "deleted": info.deleted,
+ "dataDir": info.data_dir.map(|id| id.to_string()), "size": info.size,
+ "usesLegacyChecksum": info.uses_legacy_checksum,
+ "inline": inline,
+ "erasure": info.erasure, "role": role,
+ "parts": info.parts.iter().map(|part| json!({
+ "number": part.number, "size": part.size, "actualSize": part.actual_size,
+ "etag": part.etag, "integrityDescriptorPresent": part.integrity.is_some()
+ })).collect::>()
+ }))
+ })
+ .collect();
+ let versions = versions?;
+ println!(
+ "{}",
+ serde_json::to_string_pretty(&json!({
+ "schemaVersion": 1, "bucket": args[2], "key": args[3],
+ "metadataVersion": meta.meta_ver, "versions": versions,
+ "physicalBitrotVerified": false, "authorityQuorumProved": false
+ }))?
+ );
+ Ok(())
+}
diff --git a/scripts/validation_heal/tools/heal_shared_version_fixture.rs b/scripts/validation_heal/tools/heal_shared_version_fixture.rs
new file mode 100644
index 00000000..7c24a17f
--- /dev/null
+++ b/scripts/validation_heal/tools/heal_shared_version_fixture.rs
@@ -0,0 +1,292 @@
+// Encode a controlled C10 shared-reference fixture with the exact candidate
+// codec. The node verifies both original physical generations before calling
+// this stdin/stdout encoder; this program has no filesystem mutation API.
+use rustfs_filemeta::FileMeta;
+use std::io::{Read, Write};
+
+fn main() -> Result<(), Box> {
+ let args: Vec = std::env::args().collect();
+ if args.len() != 5 {
+ return Err(
+ "usage: shared-fixture BUCKET KEY SOURCE_VERSION DEST_VERSION < XL_META".into(),
+ );
+ }
+ let mut original = Vec::new();
+ std::io::stdin()
+ .take(64 * 1024 * 1024 + 1)
+ .read_to_end(&mut original)?;
+ let (encoded, receipt) = encode(&original, &args[1], &args[2], &args[3], &args[4])?;
+ eprintln!("{}", receipt);
+ std::io::stdout().write_all(&encoded)?;
+ Ok(())
+}
+
+fn encode(
+ original: &[u8],
+ bucket: &str,
+ key: &str,
+ source_version: &str,
+ destination_version: &str,
+) -> Result<(Vec, serde_json::Value), Box> {
+ if original.is_empty() || original.len() > 64 * 1024 * 1024 {
+ return Err("requires bounded original metadata".into());
+ }
+ let mut meta = FileMeta::load(original)?;
+ meta.data.validate()?;
+ let decoded = meta.into_file_info_versions(bucket, key, true)?;
+ let lookup = |id: &str| {
+ decoded
+ .versions
+ .iter()
+ .filter(|version| {
+ version
+ .version_id
+ .is_some_and(|value| value.to_string() == id)
+ })
+ .collect::>()
+ };
+ let sources = lookup(source_version);
+ let destinations = lookup(destination_version);
+ if sources.len() != 1 || destinations.len() != 1 {
+ return Err("requires exactly one existing version for each ACK identity".into());
+ }
+ let src = sources[0];
+ let dst = destinations[0];
+ if src.version_id == dst.version_id
+ || src.deleted
+ || dst.deleted
+ || src.is_remote()
+ || dst.is_remote()
+ || src.inline_data()
+ || dst.inline_data()
+ || src.size <= 0
+ || src.size != dst.size
+ || src.parts.is_empty()
+ || src.parts.len() != dst.parts.len()
+ || src.erasure.algorithm != dst.erasure.algorithm
+ || src.erasure.data_blocks != dst.erasure.data_blocks
+ || src.erasure.parity_blocks != dst.erasure.parity_blocks
+ || src.erasure.block_size != dst.erasure.block_size
+ || src.erasure.index != dst.erasure.index
+ || src.erasure.distribution != dst.erasure.distribution
+ || src.erasure.checksums != dst.erasure.checksums
+ || src.parts.iter().zip(&dst.parts).any(|(left, right)| {
+ left.etag != right.etag
+ || left.number != right.number
+ || left.size != right.size
+ || left.actual_size != right.actual_size
+ || left.index != right.index
+ || left.checksums != right.checksums
+ || left.error != right.error
+ })
+ {
+ return Err("requires compatible distinct external plaintext versions".into());
+ }
+ let src_dir = src.data_dir.ok_or("source data directory absent")?;
+ let dst_dir = dst.data_dir.ok_or("destination data directory absent")?;
+ if src_dir.is_nil() || dst_dir.is_nil() || src_dir == dst_dir {
+ return Err("requires two distinct original physical generations".into());
+ }
+ let before = meta
+ .versions
+ .iter()
+ .map(|version| version.parse_version_meta())
+ .collect::, _>>()?;
+ let mut changed = before
+ .iter()
+ .find(|version| version.get_version_id() == dst.version_id)
+ .ok_or("shallow destination absent")?
+ .clone();
+ let source_object = before
+ .iter()
+ .find(|version| version.get_version_id() == src.version_id)
+ .and_then(|version| version.object.as_ref())
+ .ok_or("source is not an object")?;
+ let changed_object = changed
+ .object
+ .as_mut()
+ .ok_or("destination is not an object")?;
+ changed_object.data_dir = Some(src_dir);
+ // A shared directory must also share the source commitment descriptor.
+ // Preserve all other destination metadata, including its ACK identity.
+ let is_integrity = |key: &str| {
+ key == "x-rustfs-internal-shard-integrity-v1"
+ || key == "x-minio-internal-shard-integrity-v1"
+ };
+ changed_object.meta_sys.retain(|key, _| !is_integrity(key));
+ for (key, value) in &source_object.meta_sys {
+ if is_integrity(key) {
+ changed_object.meta_sys.insert(key.clone(), value.clone());
+ }
+ }
+ meta.add_version_filemata(changed.clone())?;
+ let encoded = meta.marshal_msg()?;
+ if encoded.len() > 64 * 1024 * 1024 {
+ return Err("encoded fixture exceeds metadata budget".into());
+ }
+ let reloaded = FileMeta::load(&encoded)?;
+ if reloaded.versions.len() != before.len() {
+ return Err("fixture changed the original version set".into());
+ }
+ for version in &reloaded.versions {
+ let value = version.parse_version_meta()?;
+ let old = before
+ .iter()
+ .find(|item| item.get_version_id() == value.get_version_id())
+ .ok_or("fixture invented a version")?;
+ let expected = if value.get_version_id() == dst.version_id {
+ &changed
+ } else {
+ old
+ };
+ if &value != expected {
+ return Err("fixture changed unrelated version metadata".into());
+ }
+ }
+ let receipt = serde_json::json!({"sourceVersion":source_version,"destinationVersion":destination_version,
+ "sharedDataDir":src_dir.to_string(),"retiredDataDir":dst_dir.to_string(),
+ "versionCount":before.len(),"filesystemMutationPerformed":false,"physicalByteEqualityClaimed":false});
+ Ok((encoded, receipt))
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use rustfs_filemeta::{ChecksumInfo, ErasureInfo, FileInfo, ObjectPartInfo};
+ use rustfs_utils::HashAlgorithm;
+
+ const SOURCE: &str = "00000000-0000-0000-0000-000000000001";
+ const DESTINATION: &str = "00000000-0000-0000-0000-000000000002";
+ const SOURCE_DIR: &str = "00000000-0000-0000-0000-000000000003";
+ const DESTINATION_DIR: &str = "00000000-0000-0000-0000-000000000004";
+
+ fn fixture() -> FileMeta {
+ let mut meta = FileMeta::new();
+ for (index, (version, directory)) in [(SOURCE, SOURCE_DIR), (DESTINATION, DESTINATION_DIR)]
+ .into_iter()
+ .enumerate()
+ {
+ let info = FileInfo {
+ volume: "bucket".to_owned(),
+ name: "shared.bin".to_owned(),
+ version_id: Some(version.parse().unwrap()),
+ data_dir: Some(directory.parse().unwrap()),
+ mod_time: Some(
+ time::OffsetDateTime::from_unix_timestamp(1_700_000_000 + index as i64)
+ .unwrap(),
+ ),
+ size: 1024,
+ parts: vec![ObjectPartInfo {
+ number: 1,
+ size: 1024,
+ actual_size: 1024,
+ etag: "identical-ack-etag".to_owned(),
+ ..Default::default()
+ }],
+ erasure: ErasureInfo {
+ algorithm: "ReedSolomon".to_owned(),
+ data_blocks: 2,
+ parity_blocks: 2,
+ block_size: 1024 * 1024,
+ index: 1,
+ distribution: vec![1, 2, 3, 4],
+ checksums: vec![ChecksumInfo {
+ part_number: 1,
+ algorithm: HashAlgorithm::HighwayHash256S,
+ hash: Default::default(),
+ }],
+ },
+ ..Default::default()
+ };
+ meta.add_version(info).unwrap();
+ }
+ meta
+ }
+
+ #[test]
+ fn exact_existing_versions_share_one_directory_without_changing_other_metadata() {
+ let original = fixture().marshal_msg().unwrap();
+ let source_before = FileMeta::load(&original)
+ .unwrap()
+ .into_file_info_versions("bucket", "shared.bin", true)
+ .unwrap()
+ .versions
+ .into_iter()
+ .find(|version| version.version_id == Some(SOURCE.parse().unwrap()))
+ .unwrap();
+ let (encoded, receipt) =
+ encode(&original, "bucket", "shared.bin", SOURCE, DESTINATION).unwrap();
+ let decoded = FileMeta::load(&encoded)
+ .unwrap()
+ .into_file_info_versions("bucket", "shared.bin", true)
+ .unwrap();
+ assert_eq!(decoded.versions.len(), 2);
+ assert!(
+ decoded
+ .versions
+ .iter()
+ .all(|version| version.data_dir == Some(SOURCE_DIR.parse().unwrap()))
+ );
+ assert_eq!(receipt["retiredDataDir"], DESTINATION_DIR);
+ assert_eq!(receipt["filesystemMutationPerformed"], false);
+ assert_eq!(receipt["physicalByteEqualityClaimed"], false);
+ assert_eq!(
+ decoded
+ .versions
+ .into_iter()
+ .find(|version| version.version_id == Some(SOURCE.parse().unwrap()))
+ .unwrap(),
+ source_before
+ );
+ }
+
+ #[test]
+ fn malformed_absent_same_and_already_shared_versions_are_refused() {
+ assert!(encode(b"invalid", "bucket", "shared.bin", SOURCE, DESTINATION).is_err());
+ let original = fixture().marshal_msg().unwrap();
+ assert!(encode(&original, "bucket", "shared.bin", SOURCE, SOURCE).is_err());
+ assert!(encode(&original, "bucket", "shared.bin", SOURCE, SOURCE_DIR).is_err());
+ let (shared, _) = encode(&original, "bucket", "shared.bin", SOURCE, DESTINATION).unwrap();
+ assert!(encode(&shared, "bucket", "shared.bin", SOURCE, DESTINATION).is_err());
+ }
+
+ #[test]
+ fn incompatible_part_and_deleted_destination_are_refused() {
+ for deleted in [false, true] {
+ let mut meta = fixture();
+ let mut info = meta
+ .into_file_info_versions("bucket", "shared.bin", true)
+ .unwrap()
+ .versions
+ .into_iter()
+ .find(|version| version.version_id == Some(DESTINATION.parse().unwrap()))
+ .unwrap();
+ if deleted {
+ // A deleted payload with erasure geometry is purge-pending,
+ // not a canonical delete marker in the candidate codec.
+ info = FileInfo {
+ version_id: info.version_id,
+ mod_time: info.mod_time,
+ deleted: true,
+ ..Default::default()
+ };
+ } else {
+ // Single-part ETags are not stored in PartETags. Change a
+ // durable part field so the rejection input survives load.
+ info.parts[0].size -= 1;
+ }
+ meta.add_version(info).unwrap();
+ assert!(
+ encode(
+ &meta.marshal_msg().unwrap(),
+ "bucket",
+ "shared.bin",
+ SOURCE,
+ DESTINATION
+ )
+ .is_err(),
+ "incompatible destination was accepted (delete_marker={deleted})"
+ );
+ }
+ }
+}
diff --git a/scripts/validation_heal/unformatted.py b/scripts/validation_heal/unformatted.py
new file mode 100644
index 00000000..6c42da40
--- /dev/null
+++ b/scripts/validation_heal/unformatted.py
@@ -0,0 +1,302 @@
+#!/usr/bin/env python3
+"""B04: candidate-created blank-member initialization or exact safe refusal.
+
+The foreign format is exported unchanged from a second released-process H1
+deployment. Restoring the original archive is recovery for a negative branch,
+and never earns a product-rebuild claim.
+"""
+
+from __future__ import annotations
+
+import argparse
+from datetime import datetime, timedelta, timezone
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import secrets
+import signal
+import sys
+import time
+
+try:
+ from .heal_probe import Probe, compare_inventories, require, save
+ from .heal_workload import Recorder, seed, verify_seed
+ from .healthy import Fleet, ReplacementAdmissionBlocked, physical_content, validate_topology
+ from .inspector import verify as verify_inspector
+ from .matrix import select
+ from .disk_loss import info_disks, member_state
+ from .replacement import format_identity, physical_rebuild, automatic_completion
+ from .shards import files, map_target
+except ImportError:
+ from heal_probe import Probe, compare_inventories, require, save
+ from heal_workload import Recorder, seed, verify_seed
+ from healthy import Fleet, ReplacementAdmissionBlocked, physical_content, validate_topology
+ from inspector import verify as verify_inspector
+ from matrix import select
+ from disk_loss import info_disks, member_state
+ from replacement import format_identity, physical_rebuild, automatic_completion
+ from shards import files, map_target
+
+
+CASE_ID = "heal.disk-replacement-unformatted"
+BRANCHES = ("blank-unformatted", "foreign-format")
+OBSERVATION_SECONDS = 90
+SIZE = 1024**2 + 73
+REJECTED_STATES = {"disk not found", "disk access denied", "file access denied", "volume access denied",
+ "offline", "unformatted disk", "corrupted format", "inconsistent disk", "corrupted backend"}
+
+
+def binding(args, candidate, nodes, run_id, bucket):
+ return {"schemaVersion": 1, "runId": run_id, "jumpHostOnly": True, "exclusiveHealService": True,
+ "leaseExpiresAt": (datetime.now(timezone.utc) + timedelta(minutes=30)).isoformat(), "bucketPrefix": bucket,
+ "endpoints": {f"node{index}": f"http://rustfs-node{index}:9000" for index in range(1, len(nodes) + 1)},
+ "defaultEndpoint": "node1", "candidateCommit": candidate["source_sha"], "harnessCommit": args.testing_commit,
+ "candidatePackageSha256": candidate["package_sha256"], "candidateBinarySha256": args.binary_sha,
+ "rcBinarySha256": hashlib.sha256(args.rc_bin.read_bytes()).hexdigest(),
+ "maxInventoryObjects": 512, "maxInventoryBytes": 512 * 1024**2}
+
+
+def foreign_donor(args, node):
+ directory = args.evidence / "foreign-donor-H1"
+ directory.mkdir()
+ run_id = f"gh-{os.environ['GITHUB_RUN_ID']}-{os.environ.get('GITHUB_RUN_ATTEMPT', '1')}-h1"
+ fleet = Fleet([node], directory, run_id, args.tool_directory)
+ candidate = json.loads(args.candidate.read_text())
+ fleet.record("inspector-build", verify_inspector(args.tool_directory, candidate))
+ bucket = "rv-heal-" + hashlib.sha256(run_id.encode()).hexdigest()[:16]
+ exported, error = None, None
+ try:
+ fleet.deploy(args.binary_sha, bucket, secrets.token_hex(16), secrets.token_hex(24))
+ fleet.record("actual-foreign-donor-topology", validate_topology(fleet.snapshot("foreign-donor-formats"), "H1"))
+ fleet.stop()
+ exported = fleet.record("released-process-foreign-format", fleet.remote(node, "format-export", disk=0))
+ require(exported.get("readOnly") is True and exported.get("binarySha256") == args.binary_sha,
+ "foreign format was not exported from the exact stopped released candidate")
+ except (Exception, KeyboardInterrupt) as exc:
+ error = exc
+ finally:
+ clean = fleet.cleanup()
+ fleet.record("foreign-donor-finalization", {"cleanup": "PASS" if clean else "FAIL", "productRepairClaimed": False})
+ require(clean, "foreign donor cleanup failed; primary cluster must not start")
+ if error is not None: raise error
+ return exported
+
+
+def refusal(observed, target, original_endpoint, foreign):
+ total = len(target["cohort"])
+ indexed = info_disks(observed, total)
+ selected = indexed[(0, 0, target["setSlot"])]
+ require(selected["endpoint"] == original_endpoint and selected.get("state") in REJECTED_STATES,
+ "unready/foreign member lacks a structured exact-slot refusal or was marked healthy")
+ permitted_ids = {"", target["diskUUID"]} | ({foreign["value"]["xl"]["this"]} if foreign else set())
+ require(selected.get("uuid") in permitted_ids and selected.get("runtimeState") not in {"online", "ready"},
+ "unready/foreign member became writable or acquired an invented disk identity")
+ expected = {shard["setSlot"]: shard["diskUUID"] for shard in target["cohort"]}
+ for slot, disk in indexed.items():
+ if slot[2] == target["setSlot"]: continue
+ require(disk.get("state") == "ok" and disk.get("uuid") == expected[slot[2]]
+ and disk.get("runtimeState") in {None, "online"}, "replacement refusal changed another original member")
+ return {"exactSlot": target["setSlot"], "endpoint": selected["endpoint"], "state": selected["state"],
+ "reportedUUID": selected.get("uuid"), "unreadyMemberHealthyClaimed": False}
+
+
+def negative_scene(before, scene, member, branch, foreign):
+ left, right = files(before), files(scene)
+ require({key: value for key, value in right.items() if key[:2] != member}
+ == {key: value for key, value in left.items() if key[:2] != member}, "refusal changed a surviving user file")
+ require(not any(key[:2] == member for key in right), "unready/foreign member received user data")
+ for node, value in before.items():
+ for old in value["formats"]:
+ current = next(item for item in scene[node]["formats"] if item["disk"] == old["disk"])
+ if (node, old["disk"]) != member:
+ require(current == old, "refusal changed an unrelated original disk format")
+ elif branch == "foreign-format":
+ require(current.get("sha256") == foreign["sha256"] and current.get("value") == foreign["value"],
+ "candidate overwrote the exact foreign cluster/member format")
+ else:
+ require(current.get("present") is False and current.get("unformattedReplacement") is True,
+ "blank refusal created an unexplained format")
+ return {"allSurvivingUserFilesAndFormatsUnchanged": True, "targetUserFilesAbsent": True,
+ "foreignIdentityUnchanged": branch == "foreign-format", "productRepairClaimed": False}
+
+
+def run_branch(args, topology, nodes, branch, foreign):
+ directory = args.evidence / f"{topology}-{branch}"
+ directory.mkdir()
+ run_id = f"gh-{os.environ['GITHUB_RUN_ID']}-{os.environ.get('GITHUB_RUN_ATTEMPT', '1')}-{topology.lower()}"
+ fleet = Fleet(nodes, directory, run_id, args.tool_directory, runtime_profile="replacement",
+ require_replacement_admission=True)
+ candidate = json.loads(args.candidate.read_text())
+ fleet.record("inspector-build", verify_inspector(args.tool_directory, candidate))
+ bucket = "rv-heal-" + hashlib.sha256(run_id.encode()).hexdigest()[:16]
+ access, secret = secrets.token_hex(16), secrets.token_hex(24)
+ os.environ.update(HEAL_ACCESS_KEY=access, HEAL_SECRET_KEY=secret)
+ lab = binding(args, candidate, nodes, run_id, bucket)
+ fleet.record("binding", lab)
+ probe = Probe(lab)
+ result = {"topology": topology, "branch": branch, "verdict": "RUNNING", "cleanup": "PENDING"}
+ try:
+ require(candidate["source_sha"] == "072b57b37559fbe626aeda8abf61617fdcbdd926", "unsupported candidate initialization/refusal contract")
+ fleet.deploy(args.binary_sha, bucket, access, secret)
+ recorder = Recorder(probe, directory / "fixture")
+ expected = seed(recorder, bucket, "DB", 128, [SIZE])
+ baseline = fleet.record("S3-baseline", probe.inventory(bucket, unversioned_current_tags=True))
+ fleet.record("fixture-ACK-manifest", {"expected": expected, **verify_seed(expected, baseline)})
+ fleet.stop()
+ before = fleet.snapshot("original-full-physical", quiescent=True)
+ fleet.record("actual-original-topology", validate_topology(before, topology))
+ target = map_target(before, bucket, expected[0]["key"], "data", 1)
+ node = nodes[-1]
+ selected = next(shard for shard in target["cohort"] if shard["node"] == node)
+ target = {**target, **selected}
+ member, disk = (node, target["disk"]), target["disk"]
+ physical_format = next(item for item in before[node]["formats"] if item["disk"] == disk)
+ fleet.start()
+ original = member_state(fleet.record("original-member-Admin", probe.admin("/rustfs/admin/v3/info", [], b"", method="GET")),
+ target, len(target["cohort"]))
+ fleet.stop()
+ replaced = fleet.record("whole-original-root-quarantined", fleet.remote(node, "replace-drive", disk=disk,
+ expectedFormatSha256=physical_format["sha256"]))
+ require(replaced.get("newDataRootEmpty") is True and replaced.get("oldRootMovedIntact") is True,
+ "B04 replacement lacks its actual empty root and exact original archive")
+ if branch == "foreign-format":
+ require(foreign["binarySha256"] == args.binary_sha, "foreign donor used another released candidate")
+ fleet.record("foreign-source-provenance", foreign)
+ fleet.record("exact-foreign-format-injection", fleet.remote(node, "foreign-format", rawBase64=foreign["rawBase64"], sha256=foreign["sha256"]))
+ earliest = int(time.time())
+ fleet.record("registered-initialization-refusal-window", {"seconds": OBSERVATION_SECONDS,
+ "branch": branch, "directoryTestRootFallback": True, "actualDeviceReplacementClaimed": False,
+ "manualHealMutations": 0, "refusalRequiresExactAdminSlotAndUnchangedScene": True})
+ fleet.start()
+ fleet.record("actual-old-archive-refusal", fleet.remote(node, "replacement-probe"))
+ deadline, samples, initialized = time.monotonic() + OBSERVATION_SECONDS, [], False
+ while True:
+ state = fleet.record("initialization-physical-format-window", fleet.remote(node, "replacement-state"))
+ require(state.get("originalArchiveUnchanged") is True, "candidate accessed or changed the original replacement archive")
+ current = next(item for item in state["formats"] if item["disk"] == disk)
+ if branch == "blank-unformatted" and current["present"]:
+ initialized = True
+ break
+ if branch == "foreign-format":
+ require(current.get("sha256") == foreign["sha256"] and current.get("value") == foreign["value"],
+ "candidate overwrote or reidentified a foreign formatted member")
+ samples.append(refusal(fleet.record("exact-unready-member-Admin", probe.admin("/rustfs/admin/v3/info", [], b"", method="GET")),
+ target, original["endpoint"], foreign if branch == "foreign-format" else None))
+ if time.monotonic() >= deadline: break
+ time.sleep(5)
+ if initialized:
+ current = fleet.snapshot("candidate-generated-initialized-format")
+ fleet.record("actual-new-disk-identity", format_identity(before, current, member))
+ endpoint = replaced["dataPath"] if topology == "H1" else f"http://rustfs-node{nodes.index(node) + 1}:9000{replaced['dataPath']}"
+ responsibility = automatic_completion(probe, fleet, endpoint, earliest)
+ fleet.stop()
+ rebuilt = fleet.snapshot("automatic-blank-rebuild", quiescent=True)
+ physical = fleet.record("complete-original-role-rebuild", physical_rebuild(before, rebuilt, member))
+ parts = []
+ for entry in expected:
+ cohort = map_target(rebuilt, bucket, entry["key"], "data", 1)
+ shard = next(shard for shard in cohort["cohort"] if (shard["node"], shard["disk"]) == member)
+ parts.append({"disk": disk, "relativePath": shard["relativePath"], "versionId": "null", "partNumber": 1,
+ "sha256": shard["sha256"], "physicalSize": shard["size"]})
+ streamed = fleet.record("all-new-member-streaming-checks", fleet.remote(node, "verify-parts", parts=[
+ {name: part[name] for name in ("disk", "relativePath", "versionId", "partNumber")} for part in parts]))
+ require(streamed.get("complete") is True and len(streamed.get("parts", [])) == len(parts), "blank rebuild omitted a streaming part")
+ for wanted, proof in zip(parts, streamed["parts"], strict=True):
+ require(proof.get("physicalBitrotVerified") is True and all(proof.get(name) == value for name, value in wanted.items()),
+ "blank rebuild streaming proof differs from the original same-role ACK shard")
+ result.update(initialization="candidate-generated-and-completely-rebuilt", responsibility=responsibility,
+ physical=physical, backupRestorationUsed=False)
+ else:
+ require(len(samples) >= 3, "unformatted/foreign refusal did not span its registered observation window")
+ fleet.stop()
+ scene = fleet.record("refused-complete-physical", {peer: fleet.remote(peer, "snapshot", quiescent=True,
+ allowUnformattedReplacement=peer == node) for peer in nodes})
+ physical = negative_scene(before, scene, member, branch, foreign)
+ fleet.record("safe-refusal-negative-proof", {"samples": samples, "physical": physical})
+ recovery = fleet.record("negative-original-member-recovery", fleet.remote(node, "restore-replacement"))
+ require(recovery.get("originalRestoredExactly") is True and recovery.get("productRepairClaimed") is False,
+ "negative recovery was not exact or falsely claimed product reconstruction")
+ rebuilt = fleet.snapshot("negative-original-restored", quiescent=True)
+ require(physical_content(before) == physical_content(rebuilt), "refusal recovery changed the original physical scene")
+ result.update(initialization="structured-safe-refusal", physical=physical, refusalSamples=len(samples),
+ backupRestorationUsed=True, productRepairClaimed=False)
+ fleet.start()
+ for endpoint in lab["endpoints"]:
+ inventory = fleet.record("all-peer-final-S3", {"endpoint": endpoint,
+ **Probe(lab, endpoint).inventory(bucket, unversioned_current_tags=True)})
+ fleet.record("all-peer-exact-ACK-manifest", {**verify_seed(expected, inventory), **compare_inventories(baseline, inventory)})
+ fleet.stop()
+ require(physical_content(rebuilt) == physical_content(fleet.snapshot("after-S3-physical", quiescent=True)), "S3 verification changed the proven physical result")
+ result.update(verdict="PASS", all128ACKsPreserved=True, allPeerS3ManifestEqual=True, syntheticFormatOrUUIDUsed=False)
+ except ReplacementAdmissionBlocked as exc:
+ result.update(verdict="BLOCKED", replacementAdmissionBlocked=True,
+ admissionEvidence=exc.evidence, deploymentSkipped=True,
+ fixtureSeedSkipped=True,
+ failure={"type": type(exc).__name__, "message": str(exc)})
+ except (Exception, KeyboardInterrupt) as exc:
+ result.update(verdict="FAIL", failure={"type": type(exc).__name__, "message": str(exc)})
+ if fleet.reserved:
+ try:
+ fleet.stop()
+ for peer in nodes:
+ fleet.record("B04-failure-format-and-original", {"node": peer, **fleet.remote(peer, "replacement-state")})
+ except Exception as evidence_error:
+ result["failureEvidenceError"] = str(evidence_error)
+ finally:
+ result["cleanup"] = "PASS" if fleet.cleanup() else "FAIL"
+ fleet.record("branch-result", result)
+ return result
+
+
+def main():
+ def interrupted(signum, frame): raise KeyboardInterrupt(f"Heal workflow interrupted by signal {signum}")
+ signal.signal(signal.SIGTERM, interrupted)
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--candidate", type=Path, required=True)
+ parser.add_argument("--case-id", choices=(CASE_ID,), required=True)
+ parser.add_argument("--testing-commit", required=True)
+ parser.add_argument("--binary-sha", required=True)
+ parser.add_argument("--rc-bin", type=Path, required=True)
+ parser.add_argument("--evidence", type=Path, required=True)
+ parser.add_argument("--tool-directory", type=Path, required=True)
+ args = parser.parse_args()
+ nodes = os.environ.get("RUSTFS_NODES", "vm000 vm001 vm002 vm003").split()
+ require(len(nodes) >= 4 and len(set(nodes)) == len(nodes) and all(re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", node) for node in nodes), "four current Heal nodes required")
+ require(bool(re.fullmatch(r"[0-9a-f]{64}", args.binary_sha)) and bool(re.fullmatch(r"[1-9][0-9]*", os.environ.get("GITHUB_RUN_ID", ""))), "verified workflow candidate required")
+ require(select(CASE_ID)[0]["variants"] == list(BRANCHES), "B04 executor omitted a source branch")
+ results, donor_error = [], None
+ try:
+ donor = foreign_donor(args, nodes[0])
+ for topology, selected in (("H1", nodes[:1]), ("H2", nodes[:4])):
+ for branch in BRANCHES:
+ results.append(run_branch(args, topology, selected, branch, donor))
+ if results[-1].get("replacementAdmissionBlocked") is True:
+ break
+ if results[-1]["cleanup"] != "PASS": break
+ if (results[-1].get("replacementAdmissionBlocked") is True
+ or results[-1]["cleanup"] != "PASS"): break
+ except (Exception, KeyboardInterrupt) as exc:
+ donor_error = {"type": type(exc).__name__, "message": str(exc)}
+ finally:
+ complete = len(results) == 4 and all(item["verdict"] == "PASS" and item["cleanup"] == "PASS" for item in results)
+ replacement_admission_blocked = (bool(results)
+ and any(item.get("replacementAdmissionBlocked") is True for item in results)
+ and all(item.get("cleanup") == "PASS" for item in results)
+ and donor_error is None)
+ save(args.evidence / "healthy-summary.json", {"caseId": CASE_ID, "results": results, "donorFailure": donor_error,
+ "cleanup": "PASS" if results and all(item["cleanup"] == "PASS" for item in results) and donor_error is None else "FAIL",
+ "complete": complete, "replacementAdmissionBlocked": replacement_admission_blocked,
+ "canonicalPassClaimed": False})
+ if replacement_admission_blocked:
+ print("heal.disk-replacement-unformatted: BLOCKED before reservation or fixture seed by mount admission preflight")
+ return
+ require(complete, "B04 requires both source branches on H1/H2, released foreign donor and complete cleanup")
+
+
+if __name__ == "__main__":
+ try: main()
+ except Exception as exc:
+ print(f"Heal unformatted replacement validation failed: {exc}", file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_heal/version_race_adapter.py b/scripts/validation_heal/version_race_adapter.py
new file mode 100644
index 00000000..b8a0e73e
--- /dev/null
+++ b/scripts/validation_heal/version_race_adapter.py
@@ -0,0 +1,105 @@
+"""Adapt owned H1/H2 fleet snapshots to the frozen version physical oracle."""
+import copy
+
+try:
+ from . import version_race_fixture as model, version_race_physical as physical
+ from .heal_probe import require
+ from .healthy import validate_topology
+ from .shards import files
+except ImportError:
+ import version_race_fixture as model, version_race_physical as physical
+ from heal_probe import require
+ from healthy import validate_topology
+ from shards import files
+
+
+SELECTED=('objects-0000.bin','objects-0001.bin','objects-0002.bin',model.TARGET)
+
+
+def view(snapshot,bucket,keys,inspector_sha):
+ """Use format set slots, never node/disk sorting, as decoder drive indices."""
+ topology='H1' if len(snapshot)==1 else 'H2'
+ proof=validate_topology(snapshot,topology)
+ require(len(keys)==len(set(keys)) and set(keys)<={f'objects-{i:04d}.bin' for i in range(257)},
+ 'version snapshot keys differ from the frozen flat namespace')
+ formats={};rows=[];metadata=[];identities={};indexed=files(snapshot)
+ for node,value in snapshot.items():
+ require(len(value['formats'])==4 and {v['disk'] for v in value['formats']}==set(range(4)),
+ 'version snapshot lacks four distinct local drives')
+ for drive in value['formats']:
+ slot=proof['diskUUIDs'].index(drive['value']['xl']['this'])
+ identities[node,drive['disk']]=slot;formats[slot]=copy.deepcopy(drive)
+ for key in keys:
+ identity=node,drive['disk'],f'{bucket}/{key}/xl.meta'
+ row=indexed.get(identity)
+ if row is None:metadata.append({'disk':slot,'key':key,'absent':True})
+ else:
+ require(isinstance(row.get('metadata'),dict) and not row.get('metadataError'),
+ 'metadata absence cannot substitute for a decode error')
+ metadata.append({'disk':slot,'key':key,'metadataSha256':row['sha256'],
+ 'decoded':copy.deepcopy(row['metadata'])})
+ for row in value['files']:
+ require(row['path'].startswith(bucket+'/'),'version snapshot contains another bucket')
+ key=row['path'][len(bucket)+1:].split('/')[0]
+ require(key in keys,'version snapshot contains an unregistered key')
+ rows.append({k:copy.deepcopy(v) for k,v in {**row,'disk':identities[node,row['disk']]}.items()
+ if k!='metadata'})
+ require(len(set(identities.values()))==len(identities)==len(proof['diskUUIDs']),
+ 'version snapshot duplicates a global set slot')
+ normalized={'formats':formats,'files':rows}
+ physical.file_index(normalized,disk_count=len(identities))
+ return normalized,{'inspectorSha256':inspector_sha,'rows':metadata}
+
+
+def slice_entries(entries,key):return [copy.deepcopy(v) for v in entries if v['key']==key]
+
+
+def metadata_targets(snapshot,bucket,ledger,branch,inspector_sha):
+ require(branch in model.BRANCHES[4:] and ledger.bucket==bucket and ledger.bound and not ledger.poisoned
+ and ledger.keys=={f'objects-{i:04d}.bin' for i in range(257)},
+ 'G04 requires the complete bound original 257-key ledger')
+ entries=ledger.entries();keys=sorted(ledger.keys)
+ normalized,decoded=view(snapshot,bucket,keys,inspector_sha)
+ for key in SELECTED:
+ physical.key_state(normalized,decoded,bucket,key,slice_entries(entries,key),
+ inspector_sha=inspector_sha,disk_count=len(snapshot)*4)
+ indexed=files(snapshot);node=next(iter(snapshot));targets=[]
+ for key in keys:
+ data=[v for v in entries if v['key']==key and not v['deleteMarker']]
+ members={identity:row for identity,row in indexed.items() if identity[2].startswith(f'{bucket}/{key}/')}
+ require(len(members)==len(snapshot)*4*(1+len(data))
+ and sum(path.endswith('/part.1') for _,_,path in members)==len(snapshot)*4*len(data),
+ 'G04 baseline has unregistered files or incomplete original version shards')
+ relative=f'{bucket}/{key}/xl.meta';row=indexed[node,3,relative]
+ targets.append({'node':node,'disk':3,'relativePath':relative,'key':key,
+ 'sha256':row['sha256'],'size':row['size']})
+ require(targets[-1]['key']==model.TARGET,'G04 selected target is not the final flat key')
+ return targets
+
+
+def reconstruction(before,after,bucket,target):
+ """Only the selected metadata may change; every data byte stays original."""
+ identity=target['node'],target['disk'],target['relativePath']
+ prefix=f"{bucket}/{target['key']}/"
+ left={i:v for i,v in files(before).items() if i[2].startswith(prefix)}
+ right={i:v for i,v in files(after).items() if i[2].startswith(prefix)}
+ require(set(left)==set(right) and identity in left
+ and all(before[node]['formats']==after[node]['formats'] for node in before),
+ 'G04 metadata repair changed file population or formats')
+ require(all(value==right[i] for i,value in left.items() if i!=identity),
+ 'G04 metadata repair changed another metadata member or data shard')
+ require(0=0 and
+ re.fullmatch('[0-9a-f]{64}',row['sha256']) is not None,'invalid file size or digest')
+ identity=(row['disk'],row['path'])
+ require(identity not in indexed,'duplicate physical member');indexed[identity]=row
+ return indexed
+
+
+def key_state(snapshot,metadata,bucket,key,expected,*,inspector_sha,disk_count=4,allowed_orphans=(),allowed_backups=None,allow_multipart=False):
+ """Bind exact expected versions to every set member and coding index.
+
+ `expected` is an immutable ACK-ledger slice, not a version list discovered
+ from this same snapshot. `allowed_orphans` are exact old dataDir UUIDs from
+ the verified baseline, only for versions with successful deletion ACKs.
+ """
+ require(isinstance(inspector_sha,str) and re.fullmatch('[0-9a-f]{64}',inspector_sha) is not None
+ and metadata.get('inspectorSha256')==inspector_sha,'candidate inspector digest mismatch')
+ require(isinstance(key,str) and re.fullmatch(r'objects-[0-9]{4}\.bin',key) is not None,
+ 'registered flat fixture key required')
+ require(isinstance(bucket,str) and re.fullmatch('[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]',bucket) is not None,
+ 'invalid fixture bucket')
+ require(all(v.get('key')==key for v in expected),'foreign key in expected version slice')
+ wanted={v['versionId']:v for v in expected}
+ require(len(wanted)==len(expected) and len(wanted)<=8,'duplicate or excessive expected versions')
+ require(not wanted or sum(v.get('isLatest') is True for v in wanted.values())==1,
+ 'expected ACK ledger must have exactly one latest entry')
+ rows=[v for v in metadata['rows'] if v['key']==key]
+ require(len(rows)==disk_count and all(type(v['disk']) is int for v in rows) and
+ {v['disk'] for v in rows}==set(range(disk_count)),'complete distinct set metadata observations required')
+ files=file_index(snapshot,disk_count=disk_count);prefix=f'{bucket}/{key}/'
+ members={i:v for i,v in files.items() if i[1].startswith(prefix)}
+ used=set();versions={};directories=set();multipart_sidecars=[]
+ for row in rows:
+ disk=row['disk'];meta_id=(disk,prefix+'xl.meta')
+ if row.get('absent') is True:
+ require(not wanted and meta_id not in files and 'decoded' not in row,
+ 'metadata absence conflicts with live ACK version or snapshot')
+ continue
+ require(row.get('absent') in (None,False) and meta_id in files,'metadata file/observation missing')
+ require(files[meta_id]['sha256']==row['metadataSha256'],'metadata decode not bound to snapshot SHA')
+ used.add(meta_id);decoded=row['decoded']
+ require(decoded['bucket']==bucket and decoded['key']==key,'decoded object identity differs')
+ decoded_versions=decoded['versions']
+ by_id={canonical_version(v['versionId']):v for v in decoded_versions}
+ require(len(by_id)==len(decoded_versions) and set(by_id)==set(wanted),
+ 'physical version/marker population differs from ACK ledger')
+ require(bool(wanted),'empty xl.meta is not authoritative deletion proof')
+ for vid,v in by_id.items():
+ entry=wanted[vid]
+ require(type(entry.get('deleteMarker')) is bool and type(entry.get('isLatest')) is bool,
+ 'ACK marker/latest must be booleans')
+ require(v['deleted'] is entry['deleteMarker'] and v['isLatest'] is entry['isLatest'],
+ 'physical latest/marker differs from serial ACK order')
+ item=versions.setdefault(vid,dict(deleteMarker=entry['deleteMarker'],isLatest=entry['isLatest'],
+ coding={},layouts=[],dataDirs=[],descriptors=[]))
+ if v['deleted']:
+ require(v.get('dataDir') in (None,NIL) and not v.get('parts') and v.get('inline') is None,
+ 'delete marker unexpectedly references data')
+ continue
+ directory=v['dataDir']
+ require(isinstance(directory,str) and str(uuid.UUID(directory))==directory and directory!=NIL,
+ 'invalid external data directory')
+ require(v.get('inline') is None and v['size']==entry['payload']['size'],
+ 'physical external object size differs from ACK bytes')
+ parts=v['parts'];wanted_parts=entry.get('multipartParts') if allow_multipart else None
+ if wanted_parts is not None:
+ require(len(wanted_parts)==3 and [p['number'] for p in wanted_parts]==[1,2,3]
+ and sum(p['size'] for p in wanted_parts)==entry['payload']['size'],
+ 'three ordered ACK multipart descriptors required')
+ require([{k:p.get(k) for k in ('number','size')} for p in parts]==wanted_parts,
+ 'multipart metadata differs from ACK part sizes/order')
+ else:
+ require(len(parts)==1 and type(parts[0]['number']) is int and parts[0]['number']==1
+ and parts[0]['size']==entry['payload']['size'],'single-part external fixture required')
+ ec=v['erasure'];index=ec['index']
+ require(type(index) is int and index in range(1,disk_count+1) and index not in item['coding'],
+ 'invalid or duplicate erasure index')
+ require(type(ec['data_blocks']) is int and ec['data_blocks']==(2 if disk_count==4 else 12) and
+ type(ec['parity_blocks']) is int and ec['parity_blocks']==(2 if disk_count==4 else 4) and
+ type(ec['block_size']) is int and ec['block_size']>0 and
+ len(ec['distribution'])==disk_count and all(type(i) is int for i in ec['distribution']) and
+ set(ec['distribution'])==set(range(1,disk_count+1)),'invalid registered H1/H2 EC geometry')
+ require(ec['distribution'][disk]==index,'erasure index differs from disk distribution')
+ coding={}
+ for descriptor in parts:
+ part_id=(disk,prefix+directory+'/part.'+str(descriptor['number']))
+ require(part_id in files,'live version lacks a physical shard');used.add(part_id)
+ part=files[part_id];require(part['size']>0,'external shard cannot be empty')
+ coding[descriptor['number']]={'sha256':part['sha256'],'size':part['size']}
+ if wanted_parts is not None:
+ side_id=(disk,part_id[1]+'.meta')
+ # Committed authority is xl.meta + all part.N shards.
+ # part.N.meta belongs to the upload protocol and may be
+ # reclaimed after commit or omitted by Heal reconstruction.
+ # Keep any remaining auxiliary bytes separate from coding.
+ if side_id in files:
+ require(files[side_id]['size']>0,'empty multipart sidecar')
+ used.add(side_id);multipart_sidecars.append(copy.deepcopy(files[side_id]))
+ item['coding'][index]=coding if wanted_parts is not None else coding[1]
+ item['layouts'].append({k:copy.deepcopy(ec[k]) for k in ('data_blocks','parity_blocks','block_size','distribution')})
+ item['dataDirs'].append(directory)
+ item['descriptors'].append({k:copy.deepcopy(v[k]) for k in ('size','parts')})
+ for item in versions.values():
+ if item['deleteMarker']:continue
+ require(set(item['coding'])==set(range(1,disk_count+1)) and len(set(item['dataDirs']))==1 and
+ all(v==item['layouts'][0] for v in item['layouts']) and
+ all(v==item['descriptors'][0] for v in item['descriptors']),
+ 'live version generation/layout/parts differ across drives')
+ item['dataDir']=item.pop('dataDirs')[0];item['layout']=item.pop('layouts')[0]
+ item['descriptor']=item.pop('descriptors')[0]
+ require(item['dataDir'] not in directories,'distinct live versions share a fixture data directory')
+ directories.add(item['dataDir'])
+ allowed=set(allowed_orphans)
+ require(not directories.intersection(allowed),'deleted generation became a live version')
+ orphans=[];backups=[]
+ for identity,row in members.items():
+ if identity in used:continue
+ if identity in (allowed_backups or {}):
+ original=allowed_backups[identity]
+ require(all(row[k]==original[k] for k in ('size','sha256')),'metadata backup differs from exact pre-fault bytes')
+ backups.append(copy.deepcopy(row));continue
+ relative=identity[1][len(prefix):].split('/')
+ require(len(relative)==2 and relative[0] in allowed and relative[1]=='part.1',
+ 'unregistered/orphan physical member not bound to a deleted generation')
+ orphans.append(copy.deepcopy(row))
+ return dict(versions=versions,unreferencedDeletedGenerationFiles=orphans,verifiedMetadataBackups=backups,allLiveVersionsFourShards=disk_count==4,allLiveVersionsFullSet=True,shardsPerVersion=disk_count,
+ observedMultipartSidecars=multipart_sidecars,
+ casePassClaimed=False,deletedGenerationFilesAbsent=not orphans,garbageCollectorResponsibilityProved=False)
+
+
+def additive_backups(after,before_metadata,after_metadata,bucket,key,before_entries,after_entries,order,allowed,observations,*,inspector_sha,disk_count=4):
+ """Validate exposed decoder semantics, not arbitrary bytes or live authority.
+
+ Only serial additive PUTs are admitted. A rollback view may represent any
+ ACK prefix; every old descriptor comes from the pre-race decode, every new
+ one from the final decode which transition also binds to independent data.
+ """
+ old={v['versionId']:v for v in before_entries};new={v['versionId']:v for v in after_entries}
+ require(isinstance(order,list) and 1<=len(order)<=5 and len(order)==len(set(order)) and
+ set(old)0 and complete and len(pending)>=32:
+ target=targets[-1]
+ if model.TARGET not in pending or (target['node'],3,target['relativePath']) in race.files(snapshot):
+ raise race.WindowNotObserved('G04 selected metadata repaired before the frozen partial window')
+ return snapshot
+ if not pending:raise race.WindowNotObserved('G04 all metadata repaired before the partial window')
+ require(value['summary']=='running','G04 original root stopped before physical partial repair')
+ time.sleep(.25)
+ raise race.WindowNotObserved('G04 no nonzero physical partial window within 120 seconds')
+
+
+def foreground(recorder,ledger,plan,probe,fleet,path,token,reference,branch,target_node):
+ require(len(plan)==1,'G04 requires one selected source mutation')
+ samples=[]
+ try:
+ with ThreadPoolExecutor(max_workers=1) as pool:
+ worker=pool.submit(model.submit,recorder,ledger,plan[0]);deadline=time.monotonic()+90
+ try:
+ while not worker.done() and time.monotonic()0 and not authority:
+ try:fleet.stop();fleet.snapshot('retained-failure-scene',quiescent=True)
+ except Exception as error:fleet.record('failure-scene-error',{'type':type(error).__name__,'message':str(error)})
+ fleet.record('last-ACK-rescue-required',{'state':'DIRTY','namespacesRetained':True,
+ 'lastACKPhysicalAuthorityVerified':False,'originalGenerationBlindRestorationProhibited':True})
+ result['cleanup']='FAIL'
+ else:result['cleanup']='PASS' if fleet.cleanup() else 'FAIL'
+ fleet.record('branch-result',result)
+ return result
diff --git a/scripts/validation_heal/versions.py b/scripts/validation_heal/versions.py
new file mode 100644
index 00000000..d7d37ec5
--- /dev/null
+++ b/scripts/validation_heal/versions.py
@@ -0,0 +1,1275 @@
+#!/usr/bin/env python3
+"""D03/J04: frozen version cohorts, recursive repair and repeat responsibility."""
+
+from __future__ import annotations
+
+import argparse
+import copy
+import gzip
+from datetime import datetime, timedelta, timezone
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import secrets
+import signal
+import sys
+import time
+from urllib.parse import quote
+
+try:
+ from .heal_probe import Probe, compare_inventories, payload, require, save
+ from .fleet_node import PROCESS_FIELDS
+ from .heal_workload import Recorder, seed, verify_seed
+ from .healthy import Fleet, ReplacementAdmissionBlocked, physical_content, validate_topology
+ from .inspector import verify as verify_inspector
+ from .matrix import select
+ from .heal_fault import bounded_fault_id
+ from .boundaries import (BRANCHES as SIZE_BRANCHES, verify_size_contract, boundary_sizes,
+ seed_boundaries, verify_boundary_ranges)
+ from .object_lock import BRANCHES as LOCK_BRANCHES, KEYS as LOCK_KEYS, seed_locked, prove_locked, cleanup_locked_fleet
+ from .read_repair import BRANCHES as READ_BRANCHES, tracked_read_task
+ from .drive_gate import validate_unwritable
+ from .scanner_repair import BRANCHES as SCANNER_BRANCHES, SOURCE_TARGETS, JournalWindow, observe_task as scanner_task
+ from .markers import (BRANCHES as MARKER_BRANCHES, INSTANCES as MARKER_INSTANCES, seed_markers,
+ marker_plan, prepare_stale, verify_markers, reveal_current)
+ from .stale_rejoin import BRANCHES as STALE_BRANCHES, run_branch as stale_branch
+ from .disk_loss import BRANCHES as DISK_LOSS_BRANCHES, run_branch as disk_loss_branch, info_disks
+ from .replacement import BRANCHES as REPLACEMENT_BRANCHES, AUTOMATIC_BRANCHES as AUTO_REPLACEMENT_BRANCHES, GRACEFUL_BRANCHES, CRASH_BRANCHES, run_branch as replacement_branch
+ from .shards import (SIZES, files, map_target, run_task, validate_fault,
+ validate_coordination_rejection, validate_coordination_rejection_logs,
+ validate_repaired_physical, verify_cohort, verify_ranges)
+except ImportError:
+ from heal_probe import Probe, compare_inventories, payload, require, save
+ from fleet_node import PROCESS_FIELDS
+ from heal_workload import Recorder, seed, verify_seed
+ from healthy import Fleet, ReplacementAdmissionBlocked, physical_content, validate_topology
+ from inspector import verify as verify_inspector
+ from matrix import select
+ from heal_fault import bounded_fault_id
+ from boundaries import (BRANCHES as SIZE_BRANCHES, verify_size_contract, boundary_sizes,
+ seed_boundaries, verify_boundary_ranges)
+ from object_lock import BRANCHES as LOCK_BRANCHES, KEYS as LOCK_KEYS, seed_locked, prove_locked, cleanup_locked_fleet
+ from read_repair import BRANCHES as READ_BRANCHES, tracked_read_task
+ from drive_gate import validate_unwritable
+ from scanner_repair import BRANCHES as SCANNER_BRANCHES, SOURCE_TARGETS, JournalWindow, observe_task as scanner_task
+ from markers import (BRANCHES as MARKER_BRANCHES, INSTANCES as MARKER_INSTANCES, seed_markers,
+ marker_plan, prepare_stale, verify_markers, reveal_current)
+ from stale_rejoin import BRANCHES as STALE_BRANCHES, run_branch as stale_branch
+ from disk_loss import BRANCHES as DISK_LOSS_BRANCHES, run_branch as disk_loss_branch, info_disks
+ from replacement import BRANCHES as REPLACEMENT_BRANCHES, AUTOMATIC_BRANCHES as AUTO_REPLACEMENT_BRANCHES, GRACEFUL_BRANCHES, CRASH_BRANCHES, run_branch as replacement_branch
+ from shards import (SIZES, files, map_target, run_task, validate_fault,
+ validate_coordination_rejection, validate_coordination_rejection_logs,
+ validate_repaired_physical, verify_cohort, verify_ranges)
+
+
+BRANCHES = {
+ "heal.historical-versions": ("old-version-part", "current-version-part",
+ "recursive-all-versions", "multiple-version-targets"),
+ "heal.repeat-idempotency": ("normal-repeat", "deep-repeat", "repeat-after-restart", "versioned-repeat"),
+ "heal.versioning-suspended-null": ("enabled-to-suspended", "null-overwrite", "null-delete-marker"),
+ "heal.below-read-quorum": ("r-minus-1-unversioned", "r-minus-1-historical"),
+ "heal.missing-or-corrupt-xlmeta": ("missing-xlmeta", "truncated-xlmeta", "corrupt-xlmeta", "inline-payload", "versioned-xlmeta"),
+ "heal.object-metadata-and-tags": ("user-metadata", "content-headers", "versioned-tags"),
+ "heal.multiple-shard-loss": ("loss-1-data", "loss-1-parity", "loss-2-through-m-data-parity"),
+ "heal.special-keys-and-pagination": ("unicode-space-percent", "encoded-slash", "adjacent-prefixes", "multi-page-list"),
+ "heal.object-size-boundaries": SIZE_BRANCHES,
+ "heal.object-lock-retention": LOCK_BRANCHES,
+ "heal.metadata-payload-mismatch": ("length-mismatch", "payload-mismatch", "insufficient-authority"),
+ "heal.read-repair-attribution": READ_BRANCHES,
+ "heal.disk-readonly-and-permission": ("read-only", "permission-denied"),
+ "heal.scanner-cold-object-repair": SCANNER_BRANCHES,
+ "heal.delete-marker-semantics": MARKER_BRANCHES,
+ "heal.stale-version-rejoin": STALE_BRANCHES,
+ "heal.disk-loss-and-repair": DISK_LOSS_BRANCHES,
+ "heal.disk-replacement-formatted": REPLACEMENT_BRANCHES,
+ "heal.replacement-auto-rebuild": AUTO_REPLACEMENT_BRANCHES,
+ "heal.target-graceful-restart": GRACEFUL_BRANCHES,
+ "heal.target-process-crash": CRASH_BRANCHES,
+}
+VERSION_SIZE = 5 * 1024**2 + 123
+SEMANTIC_KEYS = ("中文/文件/metadata.bin", "space key/headers.bin", "literal%2Fslash/versioned.bin")
+SPECIAL_KEYS = ("中文/空 格%25.bin", "space key/object.bin", "literal%2Fslash/object.bin",
+ "literal/slash/object.bin", "a/object.bin", "ab/object.bin", "b/object.bin", "nested/a/b/object.bin")
+
+
+def seed_mismatch(recorder, bucket):
+ recorder.call("create_bucket", {"Bucket": bucket})
+ recorder.call("put_bucket_versioning", {"Bucket": bucket, "VersioningConfiguration": {"Status": "Enabled"}})
+ expected = []
+ for branch in BRANCHES["heal.metadata-payload-mismatch"]:
+ key = "mismatch/" + branch + ".bin"
+ for nonce, size in enumerate((VERSION_SIZE, VERSION_SIZE, 2 * 1024**2 + 147)):
+ body = payload(f"{bucket}/{key}/{nonce}", size)
+ response = recorder.call("put_object", {"Bucket": bucket, "Key": key,
+ "Metadata": {"heal-fixture": "metadata-payload-mismatch", "generation": str(nonce)},
+ "Tagging": f"suite=heal&generation=v{nonce}"}, body)
+ version = response.get("VersionId")
+ require(version not in {None, "", "null"}, "mismatch fixture omitted a real version ACK")
+ expected.append({"key": key, "versionId": version, "isLatest": nonce == 2, "deleteMarker": False,
+ "payload": {"size": size, "sha256": hashlib.sha256(body).hexdigest()}})
+ return expected
+
+
+def mismatch_plan(branch, snapshot, bucket, expected):
+ key = "mismatch/" + branch + ".bin"
+ selected = [entry for entry in expected if entry["key"] == key]
+ require(len(selected) == 3 and len({entry["versionId"] for entry in selected}) == 3
+ and all(not entry["deleteMarker"] for entry in selected), "mismatch fixture lacks three distinct data versions")
+ target = choose_target(snapshot, bucket, selected[0], "data")
+ donor = choose_target(snapshot, bucket, selected[2 if branch == "length-mismatch" else 1], "data")
+ wanted_size = selected[0]["payload"]["size"]
+ require((selected[2]["payload"]["size"] != wanted_size) and selected[1]["payload"]["size"] == wanted_size,
+ "mismatch donor sizes do not distinguish length and payload faults")
+ erasure = target["logical"]["erasure"]
+ require(erasure == donor["logical"]["erasure"], "mismatch donor has a different EC layout")
+ read = erasure["data_blocks"]
+ ordered = sorted(target["cohort"], key=lambda shard: shard["index"])
+ negative = branch == "insufficient-authority"
+ replaced = ordered[read - 1:] if negative else [target]
+ faults = []
+ for shard in replaced:
+ source = next(item for item in donor["cohort"] if (item["node"], item["disk"]) == (shard["node"], shard["disk"]))
+ require(source["index"] == shard["index"] and source["role"] == shard["role"]
+ and source["dataDir"] != shard["dataDir"] and source["sha256"] != shard["sha256"]
+ and (source["size"] != shard["size"] if branch == "length-mismatch" else source["size"] == shard["size"]),
+ "mismatch donor is not the frozen same-role distinct-generation part")
+ faults.append({**target, **shard, "donorRelativePath": source["relativePath"], "donorSha256": source["sha256"],
+ "donorSize": source["size"]})
+ return {"scope": {"bucket": bucket, "key": key}, "recursive": True, "expected": selected,
+ "faults": faults, "proofs": [target], "donorProof": donor, "kind": "replace-part",
+ "requiredRepairs": set() if negative else {version_identity(target)}, "metadataRepairs": set(),
+ "negative": negative, "readQuorum": read, "retainedShards": ordered[:read - 1] if negative else []}
+
+
+def seed_special_keys(recorder, bucket):
+ recorder.call("create_bucket", {"Bucket": bucket})
+ expected = []
+ keys = [*SPECIAL_KEYS, *(f"pages/{index:04d}.bin" for index in range(257))]
+ for key in keys:
+ body = payload(f"{bucket}/{key}/0", VERSION_SIZE if key in SPECIAL_KEYS else 128)
+ response = recorder.call("put_object", {"Bucket": bucket, "Key": key,
+ "ContentType": "application/octet-stream", "Metadata": {"heal-fixture": "DS",
+ "logical-key-sha256": hashlib.sha256(key.encode()).hexdigest()}}, body)
+ require(response.get("VersionId") in {None, "null"}, "special-key fixture unexpectedly became versioned")
+ expected.append({"key": key, "versionId": "null", "deleteMarker": False, "isLatest": True,
+ "payload": {"size": len(body), "sha256": hashlib.sha256(body).hexdigest()}})
+ require(len(keys) == len(set(keys)) > 257, "special keys collided with the pagination fixture")
+ return expected
+
+
+def verify_pagination(inventory):
+ pagination = inventory.get("pagination") or {}
+ entries = inventory["entries"]
+ require(pagination.get("requestedPageSize") == 17 and len(entries) > 257
+ and {entry["key"] for entry in entries}.issuperset(SPECIAL_KEYS),
+ "special-key inventory omitted its explicit pagination or distinct logical keys")
+ for name in ("versionPageSizes", "currentPageSizes"):
+ pages = pagination.get(name, [])
+ require(len(pages) > 1 and all(type(count) is int and 0 <= count <= 17 for count in pages)
+ and sum(pages) == len(entries), "multi-page LIST omitted or duplicated acknowledged objects")
+ return {"maxKeys": 17, "versionPages": len(pagination["versionPageSizes"]),
+ "currentPages": len(pagination["currentPageSizes"]), "acknowledgedObjects": len(entries)}
+
+
+def semantic_body(bucket, key, nonce):
+ return gzip.compress(payload(f"{bucket}/{key}/{nonce}", VERSION_SIZE), mtime=0)
+
+
+def seed_semantics(recorder, bucket):
+ recorder.call("create_bucket", {"Bucket": bucket})
+ recorder.call("put_bucket_versioning", {"Bucket": bucket, "VersioningConfiguration": {"Status": "Enabled"}})
+ expected, public = [], []
+ for key in SEMANTIC_KEYS:
+ for nonce in range(3):
+ body = semantic_body(bucket, key, nonce)
+ metadata = {"UserKey": quote(f"中文值-{nonce}", safe=""), "generation": str(nonce)}
+ headers = {"ContentType": "application/octet-stream", "ContentEncoding": "gzip",
+ "ContentDisposition": "attachment; filename*=UTF-8''" + quote("中文文件.bin", safe=""),
+ "CacheControl": "no-store"}
+ color = "blue" if nonce % 2 == 0 else "red"
+ response = recorder.call("put_object", {"Bucket": bucket, "Key": key, "Metadata": metadata,
+ **headers, "Tagging": f"color={color}&generation=v{nonce}"}, body)
+ require(response.get("VersionId") not in {None, "null"}, "semantic fixture omitted its precise historical version")
+ expected.append({"key": key, "versionId": response["VersionId"], "deleteMarker": False, "isLatest": nonce == 2,
+ "payload": {"size": len(body), "sha256": hashlib.sha256(body).hexdigest()}})
+ public.append({"key": key, "versionId": response["VersionId"],
+ "properties": {**headers, "Metadata": {name.lower(): value for name, value in metadata.items()}},
+ "tags": [{"Key": "color", "Value": color}, {"Key": "generation", "Value": f"v{nonce}"}],
+ "payloadEncoding": "actual deterministic gzip bytes; no server-side compression claim"})
+ return expected, public
+
+
+def verify_semantics(public, baseline):
+ for spec in public:
+ entry = next((entry for entry in baseline["entries"] if version_identity(entry) == version_identity(spec)), None)
+ require(entry is not None and entry["tags"] == spec["tags"]
+ and all(entry["properties"].get(name) == value for name, value in spec["properties"].items()),
+ "semantic baseline lost normalized user metadata, actual encoding headers or exact versioned tags")
+ return {"versionedTagSetsAndPublicHeadersVerified": True, "expected": public}
+
+
+def version_identity(entry):
+ return entry["key"], public_version_id(entry.get("versionId"))
+
+
+def public_version_id(value):
+ # The candidate's typed Admin identity serializes Uuid::nil(), whereas
+ # the S3 identity and ACK ledger expose that same null version as "null".
+ # Normalize only those representations; every non-null UUID stays exact.
+ return "null" if value in {None, "", "null", "00000000-0000-0000-0000-000000000000"} else value
+
+
+def validate_version_receipts(status, bucket, expected, mode, *, required_repairs=(), metadata_repairs=()):
+ """Prove the complete responsibility set; counters alone never certify repair."""
+ outcome = status.get("outcome") or {}
+ require(status.get("summary") == "finished" and status.get("truncated", False) is False
+ and outcome.get("execution", {}).get("state") == "completed"
+ and outcome.get("coverage") == "complete" and outcome.get("objectsTruncated") is False,
+ "version task lacks complete terminal coverage")
+ wanted = {version_identity(entry): entry for entry in expected}
+ require(wanted and len(wanted) == len(expected), "fixture has duplicate version responsibility")
+ objects = outcome.get("objects", [])
+ require(len(objects) == len(wanted), "version task omitted or duplicated a version/marker receipt")
+ actual, incarnation = {}, None
+ required_repairs, metadata_repairs = set(required_repairs), set(metadata_repairs)
+ require(required_repairs <= set(wanted) and metadata_repairs <= {key for key, _ in wanted},
+ "repair plan escapes the selected responsibility set")
+ for receipt in objects:
+ identity = receipt.get("identity", {})
+ pair = identity.get("object"), public_version_id(identity.get("versionId"))
+ require(pair in wanted and pair not in actual and identity.get("bucket") == bucket
+ and type(identity.get("poolIndex")) is int and identity["poolIndex"] == 0
+ and type(identity.get("setIndex")) is int and identity["setIndex"] == 0
+ and isinstance(identity.get("bucketIncarnationId"), str) and identity["bucketIncarnationId"],
+ "version responsibility has a wrong, duplicate or incomplete identity")
+ incarnation = incarnation or identity["bucketIncarnationId"]
+ require(identity["bucketIncarnationId"] == incarnation, "version receipts mix bucket incarnations")
+ state = receipt.get("disposition", {}).get("state")
+ healthy = "metadata_healthy" if wanted[pair]["deleteMarker"] or mode == 1 else "verified_healthy"
+ if pair in required_repairs:
+ require(state == "repaired", "faulted version lacks a repaired responsibility")
+ elif pair[0] in metadata_repairs:
+ require(state in {"repaired", healthy}, "metadata recovery returned unknown or a false healthy disposition")
+ else:
+ require(state == healthy, "repeat or control version claimed repair, unknown, or insufficient integrity")
+ actual[pair] = state
+ require(set(actual) == set(wanted), "version responsibility set differs from acknowledged fixture")
+ for key in metadata_repairs:
+ require(any(pair[0] == key and state == "repaired" for pair, state in actual.items()),
+ "missing metadata was never attributed as repaired")
+ healed = sum(state == "repaired" for state in actual.values())
+ counters = outcome.get("counters", {})
+ wanted_counters = {"processed": len(wanted), "healed": healed, "unchanged": len(wanted) - healed,
+ "skipped": 0, "failed": 0, "unknown": 0, "attemptFailures": 0, "overflowed": False}
+ require(all(type(counters.get(name)) is type(value) and counters[name] == value
+ for name, value in wanted_counters.items()), "version counters differ from complete receipt dispositions")
+ return {"completeVersionAndMarkerResponsibility": True, "receipts": len(actual), "repaired": healed,
+ "mode": mode, "bucketIncarnationId": incarnation}
+
+
+def verify_quorum_contract(manifest, candidate):
+ contract = manifest.get("quorumContract") or {}
+ extractor = Path(__file__).with_name("prepare_heal_meta_inspector.py")
+ require(contract.get("recognized") is True and contract.get("candidateCommit") == candidate["source_sha"]
+ and contract.get("readQuorumRule") == "decoded-data-blocks"
+ and contract.get("extractorSourceSha256") == hashlib.sha256(extractor.read_bytes()).hexdigest()
+ and set(contract.get("sourceFiles", {})) == {"rustfs/src/error.rs", "crates/ecstore/src/set_disk/metadata.rs"}
+ and all(re.fullmatch(r"[0-9a-f]{64}", value) for value in contract["sourceFiles"].values())
+ and contract.get("unrecoverableGet", {}).get("httpStatus") == 503
+ and isinstance(contract.get("unrecoverableGet", {}).get("code"), str),
+ "negative quorum case requires a fresh recognized exact-candidate contract artifact")
+ return contract
+
+
+def validate_unrecoverable_get(observation, contract):
+ expected = contract["unrecoverableGet"]
+ response = observation.get("response") or {}
+ require(observation.get("acknowledged") is False and observation.get("resultUnknown") is False
+ and "payload" not in observation
+ and response.get("ResponseMetadata", {}).get("HTTPStatusCode") == expected["httpStatus"]
+ and response.get("Error", {}).get("Code") == expected["code"],
+ "below-quorum GET returned data, an unknown transport outcome or a different frozen S3 error")
+
+
+def validate_failed_version(status, bucket, expected, target):
+ outcome = status.get("outcome") or {}
+ require(status.get("summary") == "stopped" and status.get("truncated", False) is False
+ and outcome.get("execution", {}).get("state") == "completed_with_errors"
+ and outcome.get("coverage") == "complete" and outcome.get("objectsTruncated") is False,
+ "negative Heal task hid failed execution or incomplete responsibility")
+ objects = outcome.get("objects", [])
+ wanted = {version_identity(entry): entry for entry in expected}
+ require(version_identity(target) in wanted and len(objects) == len(wanted), "negative Heal omitted a version or marker")
+ actual = set()
+ for receipt in objects:
+ identity = receipt.get("identity", {})
+ pair = identity.get("object"), public_version_id(identity.get("versionId"))
+ require(pair in wanted and pair not in actual and identity.get("bucket") == bucket
+ and type(identity.get("poolIndex")) is int and identity["poolIndex"] == 0
+ and type(identity.get("setIndex")) is int and identity["setIndex"] == 0,
+ "negative Heal attributed the failure to another version or set")
+ disposition = receipt.get("disposition", {})
+ if pair == version_identity(target):
+ require(disposition.get("state") == "failed"
+ and disposition.get("details") in {"recoverable", "retry_exhausted", "permanent"},
+ "unrecoverable version was reported repaired, healthy, absent, skipped or unknown")
+ else:
+ require(disposition.get("state") == ("metadata_healthy" if wanted[pair]["deleteMarker"] else "verified_healthy"),
+ "negative Heal lost another historical version's positive integrity")
+ actual.add(pair)
+ require(actual == set(wanted), "negative Heal changed the responsibility set")
+ wanted_counters = {"processed": len(wanted), "healed": 0, "unchanged": len(wanted) - 1,
+ "failed": 1, "skipped": 0, "unknown": 0, "overflowed": False}
+ counters = outcome.get("counters", {})
+ require(all(type(counters.get(name)) is type(value) and counters[name] == value
+ for name, value in wanted_counters.items())
+ and type(counters.get("attemptFailures")) is int and counters["attemptFailures"] >= 1,
+ "negative Heal failure counters hide attempts or claim repair")
+ return {"failedVersion": version_identity(target), "completeResponsibility": True,
+ "attemptFailures": counters["attemptFailures"], "repairPassClaimed": False}
+
+
+def choose_target(snapshot, bucket, entry, role, *, excluded_drives=()):
+ target = map_target(snapshot, bucket, entry["key"], role, 1, entry["versionId"], latest_required=False)
+ selected = next((shard for shard in target["cohort"] if shard["role"] == role
+ and (shard["node"], shard["disk"]) not in excluded_drives), None)
+ require(selected is not None, "distinct physical version fault position is unavailable")
+ return {**target, **selected}
+
+
+def metadata_target(snapshot, part_target):
+ relative = f"{part_target['bucket']}/{part_target['key']}/xl.meta"
+ entry = files(snapshot)[part_target["node"], part_target["disk"], relative]
+ return {**part_target, "relativePath": relative, "sha256": entry["sha256"], "size": entry["size"]}
+
+
+def validate_all_members_online(observed, snapshot):
+ topology = validate_topology(snapshot, "H1" if len(snapshot) == 1 else "H2")
+ indexed = info_disks(observed, topology["independentDrives"])
+ require(len({disk["endpoint"] for disk in indexed.values()}) == len(indexed),
+ "multi-loss Admin members reuse a physical endpoint")
+ members = []
+ for slot, expected_uuid in enumerate(topology["diskUUIDs"]):
+ disk = indexed[0, 0, slot]
+ require(disk.get("uuid") in {None, "", expected_uuid},
+ "multi-loss Admin slot differs from the frozen format UUID")
+ members.append({"slot": [0, 0, slot], "uuid": disk.get("uuid"), "endpoint": disk["endpoint"],
+ "state": disk.get("state"), "runtimeState": disk.get("runtimeState")})
+ online = all(member["uuid"] == topology["diskUUIDs"][member["slot"][2]]
+ and member["state"] == "ok" and member["runtimeState"] in {None, "online"}
+ for member in members)
+ return {"allFormattedMembersOnline": online, "members": members}
+
+
+def wait_all_members_online(probe, fleet, snapshot, label):
+ # Read readiness only proves quorum. Poll complete member views before the
+ # single admitted repair; no task or fixture operation is retried here.
+ deadline = time.monotonic() + 90
+ while True:
+ views = {}
+ for endpoint in probe.binding["endpoints"]:
+ require(time.monotonic() < deadline,
+ "NOT_OBSERVED: multi-loss full-member startup exceeded 90 seconds")
+ reader = Probe(probe.binding, endpoint)
+ observed = fleet.record("multi-loss-members-" + label + "-" + endpoint,
+ reader.admin("/rustfs/admin/v3/info", [], b"", method="GET"))
+ views[endpoint] = validate_all_members_online(observed, snapshot)
+ proof = fleet.record("multi-loss-members-proof-" + label,
+ {"views": views, "allEndpointViewsOnline": all(
+ view["allFormattedMembersOnline"] for view in views.values()),
+ "observationOnly": True, "healTaskRetried": False})
+ if proof["allEndpointViewsOnline"]:
+ require(time.monotonic() < deadline,
+ "NOT_OBSERVED: multi-loss full-member startup exceeded 90 seconds")
+ return proof
+ remaining = deadline - time.monotonic()
+ require(remaining > 0, "NOT_OBSERVED: multi-loss full-member startup exceeded 90 seconds")
+ time.sleep(min(2, remaining))
+
+
+def multi_loss_instances(snapshot, bucket, expected, fixture):
+ sample = next(entry for entry in expected if not entry["deleteMarker"])
+ target = choose_target(snapshot, bucket, sample, "data")
+ geometry = target["logical"]["erasure"]
+ data, parity = geometry["data_blocks"], geometry["parity_blocks"]
+ require(data > 0 and parity > 0 and data + parity == len(target["cohort"]),
+ "multi-loss matrix needs a complete same-version EC authority")
+ instances = []
+ for loss in range(1, parity + 1):
+ for data_loss in range(max(0, loss - parity), min(loss, data) + 1):
+ source_branch = ("loss-1-data" if data_loss else "loss-1-parity") if loss == 1 else "loss-2-through-m-data-parity"
+ instances.append({"name": f"{source_branch}-n{loss}-d{data_loss}-p{loss - data_loss}",
+ "sourceBranch": source_branch, "loss": loss, "dataLoss": data_loss, "parityLoss": loss - data_loss,
+ "key": f"objects/{len(instances):04d}.bin", "fixture": fixture,
+ "readQuorum": data, "remainingSameVersionShards": data + parity - loss,
+ "writeCommitCondition": "all physical members online; only selected part files isolated"})
+ require({entry["sourceBranch"] for entry in instances} == set(BRANCHES["heal.multiple-shard-loss"]),
+ "actual EC geometry cannot cover every required multi-loss branch")
+ require(len({entry["key"] for entry in instances}) == len(instances)
+ and all(entry["remainingSameVersionShards"] >= data for entry in instances),
+ "multi-loss matrix reused a fixture or crossed below its read quorum")
+ return instances
+
+
+def multi_loss_plan(instance, snapshot, bucket, expected, fixture):
+ selected = [entry for entry in expected if entry["key"] == instance["key"]]
+ data = [entry for entry in selected if not entry["deleteMarker"]]
+ require(len(data) == (3 if fixture == "DV" else 1), "multi-loss instance lacks its fresh ACK cohort")
+ target = choose_target(snapshot, bucket, data[0], "data")
+ cohort = sorted(target["cohort"], key=lambda shard: shard["index"])
+ missing = [shard for shard in cohort if shard["role"] == "data"][:instance["dataLoss"]]
+ missing += [shard for shard in cohort if shard["role"] == "parity"][:instance["parityLoss"]]
+ require(len(missing) == instance["loss"] and len(cohort) - len(missing) >= instance["readQuorum"],
+ "actual multi-loss target roles or quorum differ from the frozen parameter matrix")
+ faults = [{**target, **shard} for shard in missing]
+ return {"scope": {"bucket": bucket, "key": instance["key"]}, "recursive": fixture == "DV",
+ "expected": selected, "faults": faults, "proofs": faults,
+ "requiredRepairs": {version_identity(data[0])}, "metadataRepairs": set(), "parameters": instance}
+
+
+def map_inline_target(snapshot, bucket, entry, *, role="data", allow_empty=False):
+ validate_topology(snapshot, "H1" if len(snapshot) == 1 else "H2")
+ indexed, cohort, authority = files(snapshot), [], None
+ path = f"{bucket}/{entry['key']}/xl.meta"
+ for node, observation in snapshot.items():
+ for physical in observation["formats"]:
+ file = indexed.get((node, physical["disk"], path), {})
+ metadata = file.get("metadata", {})
+ versions = [version for version in metadata.get("versions", [])
+ if (version.get("versionId") or "null") == entry["versionId"]]
+ require(metadata.get("bucket") == bucket and metadata.get("key") == entry["key"] and len(versions) == 1,
+ "inline baseline lacks an exact full-set metadata authority")
+ version = versions[0]
+ inline, erasure = version.get("inline") or {}, version["erasure"]
+ layout = physical["value"]["xl"]
+ slot = layout["sets"][0].index(layout["this"])
+ empty = allow_empty and entry["payload"]["size"] == 0
+ require(version.get("deleted") is False and version.get("size") == entry["payload"]["size"]
+ and erasure["index"] == erasure["distribution"][slot], "inline identity or physical role is unsupported")
+ if empty:
+ require(not inline and version["parts"] and all(part["size"] == part["actualSize"] == 0 for part in version["parts"])
+ and not any(identity[0] == node and identity[1] == physical["disk"]
+ and identity[2].startswith(f"{bucket}/{entry['key']}/")
+ and identity[2].rsplit("/", 1)[-1].startswith("part.") for identity in indexed),
+ "zero object unexpectedly has inline bytes or external payload parts")
+ else:
+ require(entry["payload"]["size"] > 0 and inline.get("rawOccurrenceCount") == 1
+ and type(inline.get("rawOffset")) is int and type(inline.get("length")) is int and inline["length"] > 32
+ and 0 <= inline["rawOffset"] < inline["rawOffset"] + inline["length"] <= file["size"]
+ and re.fullmatch(r"[0-9a-f]{64}", inline.get("sha256", "")),
+ "inline location or digest is unsupported")
+ actual_role = "data" if erasure["index"] <= erasure["data_blocks"] else "parity"
+ require(version.get("role") == actual_role, "inline erasure index and role differ")
+ logical = {name: version.get(name) for name in ("versionId", "isLatest", "deleted", "dataDir", "size", "parts", "usesLegacyChecksum")}
+ logical["erasure"] = {name: erasure[name] for name in ("data_blocks", "parity_blocks", "block_size", "distribution", "algorithm")}
+ require(authority is None or logical == authority, "inline version or part geometry lacks full-set authority")
+ authority = logical
+ cohort.append({"node": node, "disk": physical["disk"], "relativePath": path,
+ "versionId": entry["versionId"], "partNumber": 1, "dataDir": version.get("dataDir"),
+ "diskUUID": layout["this"], "setSlot": slot, "role": actual_role, "index": erasure["index"],
+ "sha256": file["sha256"], "size": file["size"], "inlineValue": inline})
+ require(len(cohort) == authority["erasure"]["data_blocks"] + authority["erasure"]["parity_blocks"]
+ and len({item["index"] for item in cohort}) == len(cohort), "inline set is incomplete or duplicated")
+ target = next(item for item in cohort if item["role"] == role)
+ return {**target, "bucket": bucket, "key": entry["key"], "poolIndex": 0, "setIndex": 0,
+ "inlineProof": not empty, "zeroProof": empty, "cohort": cohort, "logical": authority}
+
+
+def verify_proof(fleet, target, label):
+ if target.get("zeroProof"):
+ return fleet.record(label, {"zeroLengthMetadataAuthorityVerified": True, "payloadBytes": 0,
+ "bitrotBlocksApplicable": False, "physicalPartInvented": False,
+ "proof": "decoded complete same-version metadata; physical no-part baseline; independent post-repair oracle"})
+ if not target.get("inlineProof"):
+ return verify_cohort(fleet, target, label)
+ proofs = []
+ for entry in target["cohort"]:
+ proof = fleet.record(label, {"node": entry["node"], **fleet.remote(entry["node"], "verify-inline",
+ disk=entry["disk"], relativePath=entry["relativePath"], versionId=entry["versionId"])})
+ require(proof.get("physicalBitrotVerified") is True and proof.get("inlineLength") == entry["inlineValue"]["length"]
+ and proof.get("inlineSha256") == entry["inlineValue"]["sha256"],
+ "recovered inline shard differs from frozen same-role bytes")
+ proofs.append(proof)
+ return {"allInlineShardsBitrotVerified": True, "originalInlineSha256Restored": True, "shards": proofs}
+
+
+def release_permission_gate_live(fleet, node, before_release, metadata_faults):
+ """Restore a B02 permission gate without restarting its candidate process."""
+ released = fleet.record("drive-gate-released-live",
+ fleet.remote(node, "drive-gate-release-live"))
+ require(released.get("sameOriginalProcess") is True and released.get("serviceRestarted") is False
+ and isinstance(released.get("processBefore"), dict)
+ and isinstance(released.get("processAfter"), dict)
+ and all(name in released["processBefore"] and name in released["processAfter"]
+ and released["processBefore"][name] == released["processAfter"][name]
+ for name in PROCESS_FIELDS)
+ and released["processAfter"].get("pid", 0) > 1,
+ "B02 permission gate release did not preserve the original candidate process")
+ fleet.drive_gates.remove(node)
+
+ expected_process = released["processAfter"]
+ process_before_snapshot = fleet.record("process-after-live-gate-release-before-snapshot",
+ fleet.remote(node, "process"))
+ require(all(name in process_before_snapshot and process_before_snapshot[name] == expected_process[name]
+ for name in PROCESS_FIELDS),
+ "B02 candidate process changed after live permission gate release")
+ after_release = fleet.snapshot("physical-after-live-gate-release", metadata_faults=metadata_faults)
+ require(after_release[node].get("pid") == expected_process["pid"]
+ and after_release[node].get("binarySha256") == expected_process["binarySha256"],
+ "B02 live physical snapshot is not bound to the released candidate process")
+ process_after_snapshot = fleet.record("process-after-live-gate-release-after-snapshot",
+ fleet.remote(node, "process"))
+ require(all(name in process_after_snapshot and process_after_snapshot[name] == expected_process[name]
+ for name in PROCESS_FIELDS),
+ "B02 candidate process changed while taking the post-release physical snapshot")
+ require(physical_content(before_release) == physical_content(after_release),
+ "ATTRIBUTION_NOT_OBSERVED: target changed after same-process gate release before the fresh manual token")
+ return {"release": released, "physicalUnchangedUntilFreshToken": True,
+ "processBoundSnapshot": True, "processIdentity": expected_process}
+
+
+def boundary_plan(instance, snapshot, bucket, expected):
+ selected = [entry for entry in expected if entry["key"] == instance["key"]]
+ require(len(selected) == 1 and selected[0]["payload"]["size"] == instance["size"], "boundary lost its ACK size")
+ shape, kind = instance["expectedShape"], instance["kind"]
+ target = choose_target(snapshot, bucket, selected[0], instance["role"]) if shape == "external" else map_inline_target(
+ snapshot, bucket, selected[0], role=instance["role"], allow_empty=shape == "zero-metadata")
+ offset = None
+ if shape == "inline" and kind == "bitflip":
+ offset = target["inlineValue"]["rawOffset"] + 32
+ if shape == "zero-metadata" and kind == "bitflip":
+ kind = "header"
+ metadata = shape != "external" and not (shape == "inline" and instance["kind"] == "bitflip")
+ return {"scope": {"bucket": bucket, "key": instance["key"]}, "recursive": False,
+ "expected": selected, "faults": [target], "proofs": [target], "kind": kind, "offset": offset,
+ "requiredRepairs": set() if metadata else {version_identity(selected[0])},
+ "metadataRepairs": {instance["key"]} if metadata else set(), "parameters": instance}
+
+
+def suspend_fixture(recorder, bucket, historical):
+ expected = copy.deepcopy(historical)
+ recorder.call("put_bucket_versioning", {"Bucket": bucket, "VersioningConfiguration": {"Status": "Suspended"}})
+ require(recorder.call("get_bucket_versioning", {"Bucket": bucket}).get("Status") == "Suspended",
+ "versioning suspension was not persisted")
+ for key in sorted({entry["key"] for entry in historical}):
+ body = payload(f"{bucket}/{key}/3", VERSION_SIZE)
+ params = {"Bucket": bucket, "Key": key, "ContentType": "application/octet-stream",
+ "Metadata": {"heal-fixture": "suspended-null"}, "Tagging": "suite=heal&fixture=DV"}
+ for content in (payload(f"{bucket}/{key}/superseded-null", VERSION_SIZE), body):
+ response = recorder.call("put_object", params, content)
+ require(response.get("VersionId", "null") == "null", "suspended PUT created a non-null version")
+ # Create/delete one null marker on every key, then rebuild the live null
+ # value. This witnesses actual suspended overwrite and deletion ACKs;
+ # it does not invent history by editing xl.meta.
+ marker = recorder.call("delete_object", {"Bucket": bucket, "Key": key})
+ require(marker.get("DeleteMarker") is True and marker.get("VersionId", "null") == "null",
+ "suspended delete did not acknowledge a null marker")
+ deleted = recorder.call("delete_object", {"Bucket": bucket, "Key": key, "VersionId": "null"})
+ require(deleted.get("DeleteMarker") is True and deleted.get("VersionId", "null") == "null",
+ "precise null-marker removal was not acknowledged")
+ response = recorder.call("put_object", params, body)
+ require(response.get("VersionId", "null") == "null", "suspended overwrite changed null identity")
+ for entry in expected:
+ if entry["key"] == key:
+ entry["isLatest"] = False
+ null_entry = {"key": key, "versionId": "null", "deleteMarker": False, "isLatest": True,
+ "payload": {"sha256": hashlib.sha256(body).hexdigest(), "size": len(body)}}
+ if key == "objects/0002.bin":
+ marker = recorder.call("delete_object", {"Bucket": bucket, "Key": key})
+ require(marker.get("DeleteMarker") is True and marker.get("VersionId", "null") == "null",
+ "final suspended delete lacks the exact null marker")
+ null_entry = {"key": key, "versionId": "null", "deleteMarker": True, "isLatest": True}
+ expected.append(null_entry)
+ return expected
+
+
+def reenable_and_verify(recorder, fleet, bucket, baseline):
+ recorder.call("put_bucket_versioning", {"Bucket": bucket, "VersioningConfiguration": {"Status": "Enabled"}})
+ require(recorder.call("get_bucket_versioning", {"Bucket": bucket}).get("Status") == "Enabled",
+ "versioning was not re-enabled after null repair")
+ key = "objects/re-enabled.bin"
+ body = payload(f"{bucket}/{key}/0", VERSION_SIZE)
+ put = recorder.call("put_object", {"Bucket": bucket, "Key": key}, body)
+ require(put.get("VersionId") not in {None, "null"}, "re-enabled versioning did not create a real new version")
+ current = fleet.record("re-enabled-s3", recorder.probe.inventory(bucket))
+ new_entries = [entry for entry in current["entries"] if entry["key"] == key]
+ require(len(new_entries) == 1 and new_entries[0]["versionId"] == put["VersionId"]
+ and new_entries[0]["payload"] == {"size": len(body), "sha256": hashlib.sha256(body).hexdigest()}
+ and new_entries[0]["isLatest"] is True and not new_entries[0]["deleteMarker"],
+ "re-enabled version identity or independent payload differs")
+ # The inventory's current listing also contains the new key. Compare
+ # original entries directly, and require one exact additional entry.
+ require(len(current["entries"]) == len(baseline["entries"]) + 1
+ and [entry for entry in current["entries"] if entry["key"] != key] == baseline["entries"],
+ "re-enabling versioning changed prior null, historical or marker identities/content")
+ return {"reenabled": True, "newVersionId": put["VersionId"], "allOriginalVersionsStillAddressable": True}
+
+
+def branch_plan(case_id, branch, snapshot, bucket, expected, fixture):
+ """Plans are derived from ACK order and decoded physical roles, never disk numbers."""
+ if case_id == "heal.scanner-cold-object-repair":
+ key = f"objects/{8 + 9 * SCANNER_BRANCHES.index(branch):04d}.bin"
+ selected = [entry for entry in expected if entry["key"] == key]
+ require(len(selected) == 1 and selected[0]["versionId"] == "null", "scanner fixture lacks a unique cold target")
+ target = choose_target(snapshot, bucket, selected[0], "parity" if branch == "cold-bitrot" else "data")
+ return {"scope": {"bucket": bucket, "key": key}, "recursive": False, "expected": selected,
+ "faults": [target], "proofs": [target], "requiredRepairs": {version_identity(target)},
+ "metadataRepairs": set(), "kind": "bitflip" if branch == "cold-bitrot" else "missing",
+ "offset": 32 if branch == "cold-bitrot" else None}
+ if case_id == "heal.disk-readonly-and-permission":
+ key = f"objects/{8 + 9 * BRANCHES[case_id].index(branch):04d}.bin"
+ selected = [entry for entry in expected if entry["key"] == key]
+ require(len(selected) == 1 and selected[0]["versionId"] == "null", "unwritable fixture lacks a unique unversioned target")
+ target = choose_target(snapshot, bucket, selected[0], "data")
+ return {"scope": {"bucket": bucket, "key": key}, "recursive": False, "expected": selected,
+ "faults": [target], "proofs": [target], "requiredRepairs": {version_identity(target)},
+ "metadataRepairs": set(), "driveGate": branch}
+ if case_id == "heal.read-repair-attribution":
+ key = "objects/0001.bin" if fixture == "DV" else f"objects/{8 + 9 * READ_BRANCHES.index(branch):04d}.bin"
+ data = [entry for entry in expected if entry["key"] == key and not entry["deleteMarker"]]
+ require(len(data) == (3 if fixture == "DV" else 1), "read target lacks its acknowledged version history")
+ require(branch != "historical-version-read" or not data[0]["isLatest"], "historical read selected a current version")
+ target = choose_target(snapshot, bucket, data[0], "data")
+ return {"scope": {"bucket": bucket, "key": key}, "recursive": False, "expected": [data[0]],
+ "faults": [target], "proofs": [target], "requiredRepairs": {version_identity(target)},
+ "metadataRepairs": set(), "kind": "bitflip" if branch == "range-bitrot" else "missing",
+ "offset": 32 if branch == "range-bitrot" else None}
+ if case_id == "heal.metadata-payload-mismatch":
+ return mismatch_plan(branch, snapshot, bucket, expected)
+ if case_id == "heal.object-lock-retention":
+ index = LOCK_BRANCHES.index(branch)
+ key = LOCK_KEYS[index]
+ selected = [entry for entry in expected if entry["key"] == key]
+ require(len(selected) == 3 and all(not entry["deleteMarker"] for entry in selected), "protected fixture lost its three distinct versions")
+ target = choose_target(snapshot, bucket, selected[0 if index == 3 else -1], "parity" if index == 1 else "data")
+ metadata = index >= 2
+ proofs = [target]
+ if metadata:
+ proofs = [choose_target(snapshot, bucket, entry, "data") for entry in selected]
+ proofs = [{**item, **next(shard for shard in item["cohort"]
+ if (shard["node"], shard["disk"]) == (target["node"], target["disk"]))} for item in proofs]
+ return {"scope": {"bucket": bucket, "key": key}, "recursive": True, "expected": selected,
+ "faults": [metadata_target(snapshot, target)] if metadata else [target], "proofs": proofs,
+ "requiredRepairs": set() if metadata else {version_identity(target)},
+ "metadataRepairs": {key} if metadata else set()}
+ branch_index = BRANCHES[case_id].index(branch)
+ key = f"objects/{branch_index:04d}.bin" if fixture == "DV" else f"objects/{8 + 9 * branch_index:04d}.bin"
+ if branch == "versioned-repeat":
+ key = "objects/0000.bin"
+ if case_id == "heal.missing-or-corrupt-xlmeta":
+ key = "objects/0000.bin" if fixture == "DV" else f"objects/{branch_index + 1:04d}.bin"
+ if case_id == "heal.object-metadata-and-tags":
+ key = SEMANTIC_KEYS[branch_index]
+ if case_id == "heal.special-keys-and-pagination":
+ key = SPECIAL_KEYS[(0, 2, 4, 7)[branch_index]]
+ selected = [entry for entry in expected if entry["key"] == key]
+ data = [entry for entry in selected if not entry["deleteMarker"]]
+ if case_id == "heal.special-keys-and-pagination":
+ require(len(data) == 1 and data[0]["versionId"] == "null", "special-key fault lost its exact logical identity")
+ target = choose_target(snapshot, bucket, data[0], "parity" if branch == "adjacent-prefixes" else "data")
+ metadata = branch in {"encoded-slash", "multi-page-list"}
+ return {"scope": {"bucket": bucket, "key": key}, "recursive": False, "expected": selected,
+ "faults": [metadata_target(snapshot, target) if metadata else target], "proofs": [target],
+ "requiredRepairs": set() if metadata else {version_identity(data[0])},
+ "metadataRepairs": {key} if metadata else set()}
+ if case_id == "heal.object-metadata-and-tags":
+ require(len(data) == 3, "metadata/tag fixture lacks three independently acknowledged versions")
+ target = choose_target(snapshot, bucket, data[0] if branch == "user-metadata" else data[-1],
+ "parity" if branch == "versioned-tags" else "data")
+ proofs, faults, required, metadata_keys = [target], [target], {version_identity(target)}, set()
+ if branch == "content-headers":
+ faults, required, metadata_keys = [metadata_target(snapshot, target)], set(), {key}
+ proofs = [choose_target(snapshot, bucket, entry, "data") for entry in data]
+ proofs = [{**item, **next(shard for shard in item["cohort"]
+ if (shard["node"], shard["disk"]) == (target["node"], target["disk"]))} for item in proofs]
+ return {"scope": {"bucket": bucket, "key": key}, "recursive": True, "expected": selected,
+ "faults": faults, "proofs": proofs, "requiredRepairs": required, "metadataRepairs": metadata_keys,
+ "kind": {"user-metadata": "missing", "content-headers": "header", "versioned-tags": "bitflip"}[branch]}
+ if case_id == "heal.missing-or-corrupt-xlmeta":
+ if fixture == "DB":
+ target = map_inline_target(snapshot, bucket, data[0])
+ faults, proofs = [target], [target]
+ else:
+ target = choose_target(snapshot, bucket, data[0], "data")
+ faults = [metadata_target(snapshot, target)]
+ proofs = [choose_target(snapshot, bucket, entry, "data") for entry in data]
+ proofs = [{**item, **next(shard for shard in item["cohort"]
+ if (shard["node"], shard["disk"]) == (target["node"], target["disk"]))} for item in proofs]
+ kind = {"truncated-xlmeta": "truncate", "corrupt-xlmeta": "header", "inline-payload": "bitflip"}.get(branch, "missing")
+ offset = target["inlineValue"]["rawOffset"] + 32 if branch == "inline-payload" else None
+ return {"scope": {"bucket": bucket, "key": key}, "recursive": fixture == "DV", "expected": selected,
+ "faults": faults, "proofs": proofs, "requiredRepairs": {version_identity(data[0])} if branch == "inline-payload" else set(),
+ "metadataRepairs": {key}, "kind": kind, "offset": offset}
+ if case_id == "heal.below-read-quorum":
+ target = choose_target(snapshot, bucket, data[0], "data")
+ read_quorum = target["logical"]["erasure"]["data_blocks"]
+ ordered = sorted(target["cohort"], key=lambda entry: entry["index"])
+ retained, missing = ordered[:read_quorum - 1], ordered[read_quorum - 1:]
+ require(len(retained) == read_quorum - 1 and missing, "below-quorum fault did not retain exactly R-1 authorities")
+ return {"scope": {"bucket": bucket, "key": key}, "recursive": True, "expected": selected,
+ "faults": [{**target, **shard} for shard in missing], "proofs": [target],
+ "requiredRepairs": set(), "metadataRepairs": set(), "negative": True,
+ "readQuorum": read_quorum, "retainedShards": retained}
+ if case_id == "heal.versioning-suspended-null":
+ historical = [entry for entry in data if entry["versionId"] != "null"]
+ require(len(historical) == 3, "suspended fixture lost enabled-period historical versions")
+ if branch == "null-delete-marker":
+ old = choose_target(snapshot, bucket, historical[0], "data")
+ proofs = [choose_target(snapshot, bucket, entry, "data") for entry in historical]
+ proofs = [{**item, **next(shard for shard in item["cohort"]
+ if (shard["node"], shard["disk"]) == (old["node"], old["disk"]))} for item in proofs]
+ require(any(entry["versionId"] == "null" and entry["deleteMarker"] and entry["isLatest"]
+ for entry in selected), "null-marker branch lacks its current marker")
+ faults, required, metadata_keys = [metadata_target(snapshot, old)], set(), {key}
+ else:
+ null = next((entry for entry in data if entry["versionId"] == "null" and entry["isLatest"]), None)
+ require(null is not None, "suspended fixture lost its live null identity")
+ target = choose_target(snapshot, bucket, null, "data" if branch_index == 0 else "parity")
+ faults, proofs, required, metadata_keys = [target], [target], {version_identity(null)}, set()
+ return {"scope": {"bucket": bucket, "key": key}, "recursive": True, "expected": selected,
+ "faults": faults, "proofs": proofs, "requiredRepairs": required, "metadataRepairs": metadata_keys}
+ require(len(data) == (3 if fixture == "DV" else 1), "branch is missing its distinct acknowledged payload versions")
+ old = choose_target(snapshot, bucket, data[0], "data")
+ if case_id == "heal.repeat-idempotency":
+ if fixture == "DV":
+ proofs = [choose_target(snapshot, bucket, entry, "data") for entry in data]
+ # A whole xl.meta contains all three versions and the marker. Keep
+ # every version's target-drive maintenance references in the oracle.
+ proofs = [{**item, **next(shard for shard in item["cohort"]
+ if (shard["node"], shard["disk"]) == (old["node"], old["disk"]))} for item in proofs]
+ faults = [metadata_target(snapshot, old)]
+ required, metadata_keys = set(), {key}
+ else:
+ proofs, faults, required, metadata_keys = [old], [old], {version_identity(data[0])}, set()
+ return {"scope": {"bucket": bucket, "key": key}, "recursive": fixture == "DV",
+ "expected": selected, "faults": faults, "proofs": proofs,
+ "requiredRepairs": required, "metadataRepairs": metadata_keys}
+ current = choose_target(snapshot, bucket, data[-1], "parity", excluded_drives={(old["node"], old["disk"])})
+ faults, proofs, metadata_keys = [old], [old], set()
+ if branch == "current-version-part":
+ require(data[-1]["isLatest"] is True, "current-version branch selected a historical version")
+ faults, proofs = [current], [current]
+ elif branch == "recursive-all-versions":
+ # Missing historical part plus another drive's complete metadata tests
+ # both F1 and F3 with an intact same-version authority quorum.
+ faults, proofs = [old, metadata_target(snapshot, current)], [old, current]
+ metadata_keys = {key}
+ elif branch == "multiple-version-targets":
+ faults, proofs = [old, current], [old, current]
+ recursive_bucket = branch == "recursive-all-versions"
+ return {"scope": {"bucket": bucket, "key": "" if recursive_bucket else key}, "recursive": True,
+ "expected": expected if recursive_bucket else selected, "faults": faults, "proofs": proofs,
+ "requiredRepairs": {version_identity(item) for item in faults if not item["relativePath"].endswith("/xl.meta")},
+ "metadataRepairs": metadata_keys}
+
+
+def verify_version_s3(probe, fleet, bucket, expected, baseline, label, *, fixture="DV"):
+ current = fleet.record(label, probe.inventory(bucket, unversioned_current_tags=fixture in {"DB", "DK"},
+ page_size=17 if fixture == "DK" else None))
+ comparison = compare_inventories(baseline, current)
+ if fixture == "DK":
+ comparison["pagination"] = verify_pagination(current)
+ ranges = []
+ for entry in expected:
+ if entry["deleteMarker"]:
+ continue
+ head = fleet.record(label + "-head", probe.s3("head_object", {
+ "Bucket": bucket, "Key": entry["key"], "VersionId": entry["versionId"]}))
+ observed = next(item for item in baseline["entries"] if version_identity(item) == version_identity(entry))
+ require(head["acknowledged"] and all(head["response"].get(name) == value
+ for name, value in observed["properties"].items()), "version HEAD differs from frozen public properties")
+ if entry["payload"]["size"] < 1024**2:
+ continue
+ nonce = next(index for index, item in enumerate(
+ item for item in expected if item["key"] == entry["key"] and not item["deleteMarker"])
+ if item["versionId"] == entry["versionId"])
+ nonce = entry.get("payloadNonce", nonce)
+ body = semantic_body(bucket, entry["key"], nonce) if fixture == "DS" else payload(
+ f"{bucket}/{entry['key']}/{nonce}", entry["payload"]["size"])
+ ranges.append({"key": entry["key"], "versionId": entry["versionId"], "proof": verify_ranges(
+ probe, fleet, bucket, entry["key"], version_id=entry["versionId"],
+ expected_body=body)})
+ return {"inventory": comparison, "historicalRanges": ranges}
+
+
+def run_topology(args, topology, nodes, fixture):
+ directory = args.evidence / fixture / topology
+ directory.mkdir(parents=True)
+ run_id = f"gh-{os.environ['GITHUB_RUN_ID']}-{os.environ.get('GITHUB_RUN_ATTEMPT', '1')}-{topology.lower()}"
+ target_restart = args.case_id in {"heal.target-graceful-restart", "heal.target-process-crash"}
+ fleet = Fleet(nodes, directory, run_id, args.tool_directory, resource_profile="cursor" if target_restart else "standard",
+ runtime_profile="scanner" if args.case_id == "heal.scanner-cold-object-repair" else
+ "read-repair" if args.case_id == "heal.read-repair-attribution" else
+ "replacement" if args.case_id == "heal.replacement-auto-rebuild" or target_restart else
+ "stale-rejoin" if args.case_id == "heal.stale-version-rejoin" else "manual",
+ require_replacement_admission=args.case_id == "heal.disk-replacement-formatted")
+ if target_restart:
+ fleet.node_runtime_profiles = {node: "replacement-target" for node in nodes[1:]}
+ candidate = json.loads(args.candidate.read_text())
+ tool_manifest = fleet.record("inspector-build", verify_inspector(args.tool_directory, candidate))
+ contract = verify_quorum_contract(tool_manifest, candidate) if args.case_id in {
+ "heal.below-read-quorum", "heal.metadata-payload-mismatch", "heal.stale-version-rejoin",
+ "heal.disk-loss-and-repair"} else None
+ size_rule = verify_size_contract(tool_manifest, candidate) if args.case_id == "heal.object-size-boundaries" else None
+ bucket = "rv-heal-" + hashlib.sha256(run_id.encode()).hexdigest()[:16]
+ access, secret = secrets.token_hex(16), secrets.token_hex(24)
+ os.environ.update(HEAL_ACCESS_KEY=access, HEAL_SECRET_KEY=secret)
+ binding = {"schemaVersion": 1, "runId": run_id, "jumpHostOnly": True, "exclusiveHealService": True,
+ "leaseExpiresAt": (datetime.now(timezone.utc) + timedelta(minutes=45 if size_rule or target_restart else 30)).isoformat(), "bucketPrefix": bucket,
+ "endpoints": {f"node{index}": f"http://rustfs-node{index}:9000" for index in range(1, len(nodes) + 1)},
+ "defaultEndpoint": "node1", "candidateCommit": candidate["source_sha"], "harnessCommit": args.testing_commit,
+ "candidatePackageSha256": candidate["package_sha256"], "candidateBinarySha256": args.binary_sha,
+ "rcBinarySha256": hashlib.sha256(args.rc_bin.read_bytes()).hexdigest(),
+ "maxInventoryObjects": 2048 if target_restart else 512, "maxInventoryBytes": 2 * 1024**3 if target_restart else 512 * 1024**2}
+ fleet.record("binding", binding)
+ recipe = select(args.case_id)[0]
+ require(list(BRANCHES[args.case_id]) == recipe["variants"], "executor omitted a source version branch")
+ branches = [branch for branch in BRANCHES[args.case_id]
+ if (args.case_id != "heal.repeat-idempotency" or (branch == "versioned-repeat") == (fixture == "DV"))
+ and (args.case_id != "heal.below-read-quorum" or (branch == "r-minus-1-historical") == (fixture == "DV"))
+ and (args.case_id != "heal.read-repair-attribution" or (branch == "historical-version-read") == (fixture == "DV"))
+ and (args.case_id != "heal.missing-or-corrupt-xlmeta" or (branch == "versioned-xlmeta") == (fixture == "DV"))]
+ if args.case_id == "heal.stale-version-rejoin":
+ branches = [branch for branch in STALE_BRANCHES if (branch == "unversioned-overwrite") == (fixture == "DB")]
+ if args.case_id == "heal.disk-replacement-formatted":
+ branches = [branch for branch in REPLACEMENT_BRANCHES if (branch == "new-disk-data") == (fixture == "DB")]
+ if args.case_id == "heal.replacement-auto-rebuild":
+ branches = [branch for branch in AUTO_REPLACEMENT_BRANCHES if (branch == "replacement-unversioned") == (fixture == "DB")]
+ if target_restart:
+ branches = [branch for index, branch in enumerate(BRANCHES[args.case_id]) if (index == 0) == (fixture == "DB")]
+ result = {"topology": topology, "fixture": fixture, "verdict": "RUNNING", "branches": {}, "cleanup": "PENDING"}
+ active, admissions = [], []
+ probe = Probe(binding)
+ lock_context = {} if args.case_id == "heal.object-lock-retention" else None
+ try:
+ fleet.deploy(args.binary_sha, bucket, access, secret)
+ processes = fleet.snapshot("deployed-topology")
+ fleet.record("topology-proof", validate_topology(processes, topology))
+ if args.case_id == "heal.multiple-shard-loss":
+ wait_all_members_online(probe, fleet, processes, "before-seed")
+ scanner = args.case_id == "heal.scanner-cold-object-repair"
+ if scanner:
+ fleet.record("scanner-registered-budget", {"perBranchSeconds": 240, "sourceBranches": list(SCANNER_BRANCHES),
+ "actualProcessConfiguration": {node: value["backgroundConfig"] for node, value in processes.items()},
+ "otherAutomaticSourcesDisabled": True, "targetReadsAfterFaultBeforePhysicalProof": 0})
+ recorder = Recorder(probe, directory / "fixture")
+ public, instances, sizes = None, None, None
+ if size_rule:
+ expected = seed(recorder, bucket, "DB", 1, [VERSION_SIZE])
+ calibration = fleet.snapshot("size-calibration")
+ target = choose_target(calibration, bucket, expected[0], "data")
+ sizes = boundary_sizes(size_rule, target, processes)
+ fleet.record("candidate-size-contract", {"sourceContract": size_rule, "parameters": sizes})
+ extra, instances = seed_boundaries(recorder, bucket, sizes)
+ expected += extra
+ elif lock_context is not None:
+ expected = seed_locked(recorder, fleet, bucket, lock_context)
+ elif args.case_id == "heal.metadata-payload-mismatch":
+ expected = seed_mismatch(recorder, bucket)
+ elif args.case_id == "heal.delete-marker-semantics":
+ expected = seed_markers(recorder, bucket, VERSION_SIZE)
+ instances = list(MARKER_INSTANCES)
+ elif target_restart:
+ expected = seed(recorder, bucket, fixture, 1025 if fixture == "DB" else 64, [1024**2 + 73])
+ fleet.record("replacement-interruption-budget", {"dataObjects": 1025 if fixture == "DB" else 192, "objectBytes": 1024**2 + 73, "requiresActualPartialPhysicalProgress": True, "runtimeMinutes": 45})
+ elif fixture == "DK":
+ expected = seed_special_keys(recorder, bucket)
+ elif fixture == "DS":
+ expected, public = seed_semantics(recorder, bucket)
+ else:
+ expected = seed(recorder, bucket, fixture,
+ 16 if args.case_id == "heal.multiple-shard-loss" else 4 if fixture == "DV" or args.case_id == "heal.stale-version-rejoin" else 128,
+ [VERSION_SIZE] if args.case_id in {"heal.multiple-shard-loss", "heal.stale-version-rejoin"} or fixture == "DV" else SIZES)
+ if args.case_id == "heal.versioning-suspended-null":
+ expected = suspend_fixture(recorder, bucket, expected)
+ if args.case_id == "heal.stale-version-rejoin":
+ for key in {entry["key"] for entry in expected}:
+ for nonce, entry in enumerate(entry for entry in expected if entry["key"] == key and not entry["deleteMarker"]):
+ entry["payloadNonce"] = nonce
+ baseline = fleet.record("s3-baseline", probe.inventory(bucket, unversioned_current_tags=fixture in {"DB", "DK"},
+ page_size=17 if fixture == "DK" else None))
+ fleet.record("fixture-manifest", {"expected": expected, **verify_seed(expected, baseline)})
+ if lock_context is not None:
+ fleet.record("lock-baseline-proof", prove_locked(probe, fleet, bucket, lock_context, label="lock-baseline"))
+ if fixture == "DK":
+ fleet.record("special-key-pagination-baseline", verify_pagination(baseline))
+ if public is not None:
+ fleet.record("semantic-baseline", verify_semantics(public, baseline))
+ if scanner:
+ healthy_before = fleet.snapshot("scanner-healthy-physical-before")
+ healthy_window = JournalWindow(fleet, "scanner-healthy", event_targets=SOURCE_TARGETS)
+ for branch in branches:
+ plan = branch_plan(args.case_id, branch, healthy_before, bucket, expected, fixture)
+ status = scanner_task(probe, fleet, healthy_window, bucket, plan["expected"][0], branch, healthy=True)
+ fleet.record("scanner-healthy-responsibility", validate_version_receipts(status, bucket, plan["expected"], 2))
+ require(physical_content(healthy_before) == physical_content(fleet.snapshot("scanner-healthy-physical-after")),
+ "healthy scanner preflight changed acknowledged object files")
+ if args.case_id == "heal.multiple-shard-loss":
+ fleet.stop()
+ instances = multi_loss_instances(fleet.snapshot("multi-loss-geometry", quiescent=True), bucket, expected, fixture)
+ fleet.record("multi-loss-parameter-matrix", {"instances": instances, "allLossCountsAndRoleCompositions": True})
+ branches = [entry["name"] for entry in instances]
+ fleet.start()
+ if size_rule:
+ branches = [entry["name"] for entry in instances]
+ fleet.record("boundary-parameter-matrix", {"instances": instances, "sourceBranches": list(SIZE_BRANCHES)})
+ if args.case_id == "heal.delete-marker-semantics":
+ branches = [entry["name"] for entry in instances]
+ fleet.record("marker-parameter-matrix", {"instances": instances, "sourceBranches": list(MARKER_BRANCHES)})
+ restart_after_failure = False
+ for branch in branches:
+ registrations = []
+ metadata_faults = {}
+ gate_node, gated_proof = None, None
+ stale_marker_started = False
+ try:
+ if lock_context is not None:
+ fleet.record("lock-before-fault", prove_locked(probe, fleet, bucket, lock_context,
+ key=LOCK_KEYS[LOCK_BRANCHES.index(branch)], label="lock-before-" + branch))
+ if args.case_id == "heal.multiple-shard-loss":
+ if restart_after_failure:
+ fleet.start()
+ restart_after_failure = False
+ wait_all_members_online(probe, fleet, processes, "before-fault-" + branch)
+ fleet.stop()
+ if args.case_id in {"heal.disk-replacement-formatted", "heal.replacement-auto-rebuild", "heal.target-graceful-restart", "heal.target-process-crash"}:
+ stale_marker_started = True
+ result["branches"][branch] = replacement_branch(recorder, fleet, bucket, branch, expected, baseline, active, admissions)
+ stale_marker_started = False
+ continue
+ if args.case_id == "heal.disk-loss-and-repair":
+ stale_marker_started = True # ACKs may create a new authority; never restore an older fixture.
+ expected, baseline, proof = disk_loss_branch(recorder, fleet, bucket, branch, expected, active, admissions, contract)
+ result["branches"][branch] = proof
+ stale_marker_started = False
+ continue
+ if args.case_id == "heal.stale-version-rejoin":
+ stale_marker_started = True
+ expected, baseline, proof = stale_branch(recorder, fleet, bucket, branch, expected, active, admissions, contract)
+ result["branches"][branch] = proof
+ stale_marker_started = False
+ continue
+ before = fleet.snapshot("physical-before-" + branch, quiescent=True)
+ scanner_window = JournalWindow(fleet, "scanner-fault-" + branch, event_targets=SOURCE_TARGETS) if scanner else None
+ if size_rule:
+ plan = boundary_plan(next(entry for entry in instances if entry["name"] == branch), before, bucket, expected)
+ elif args.case_id == "heal.delete-marker-semantics":
+ plan = marker_plan(next(entry for entry in instances if entry["name"] == branch), before, bucket, expected)
+ elif instances:
+ plan = multi_loss_plan(next(entry for entry in instances if entry["name"] == branch), before, bucket, expected, fixture)
+ else:
+ plan = branch_plan(args.case_id, branch, before, bucket, expected, fixture)
+ fleet.record("fault-plan-" + branch, {**plan, "requiredRepairs": sorted(plan["requiredRepairs"]),
+ "metadataRepairs": sorted(plan["metadataRepairs"])})
+ for target in plan["proofs"]:
+ verify_proof(fleet, target, "baseline-bitrot-" + branch)
+ if plan.get("donorProof"):
+ verify_proof(fleet, plan["donorProof"], "baseline-donor-bitrot-" + branch)
+ prior = before
+ if plan.get("staleMarker"):
+ stale_marker_started = True
+ before, prior, expected, baseline = prepare_stale(recorder, fleet, bucket, plan, before, expected)
+ for index, target in enumerate(plan["faults"]):
+ params = {"disk": target["disk"], "faultId": bounded_fault_id(recipe["code"], branch, index)}
+ registration = target["node"], params
+ fleet.faults.append(registration)
+ registrations.append(registration)
+ receipt = fleet.record("fault-" + branch, fleet.remote(target["node"], "apply-fault",
+ **params, relativePath=target["relativePath"], expectedSha256=target["sha256"],
+ kind=plan.get("kind", "missing"), offset=plan.get("offset"),
+ **({"donorRelativePath": target["donorRelativePath"], "donorSha256": target["donorSha256"]}
+ if plan.get("donorProof") else {})))
+ if plan.get("donorProof"):
+ require(receipt.get("donor") == {"relativePath": target["donorRelativePath"],
+ "sha256": target["donorSha256"], "size": target["donorSize"]}
+ and receipt.get("fault") == {"sha256": target["donorSha256"], "size": target["donorSize"]},
+ "replacement receipt did not witness the exact frozen donor bytes")
+ if target["relativePath"].endswith("/xl.meta") and receipt.get("fault"):
+ metadata_faults.setdefault(target["node"], {})[target["relativePath"]] = receipt["fault"]
+ witnessed = fleet.snapshot("physical-fault-" + branch, quiescent=True, metadata_faults=metadata_faults)
+ validate_fault(prior, witnessed, target, receipt)
+ prior = witnessed
+ if plan.get("driveGate"):
+ gate_node = plan["proofs"][0]["node"]
+ fleet.drive_gates.add(gate_node)
+ fleet.record("drive-gate-armed", fleet.remote(gate_node, "drive-gate",
+ disk=plan["proofs"][0]["disk"], mode=plan["driveGate"]))
+ fleet.start()
+ if args.case_id == "heal.multiple-shard-loss":
+ wait_all_members_online(probe, fleet, before, "before-task-" + branch)
+ if gate_node is not None:
+ fleet.record("service-write-denial", fleet.remote(gate_node, "drive-gate-probe"))
+ witness = fleet.snapshot("physical-before-trigger-" + branch, metadata_faults=metadata_faults)
+ if not scanner:
+ require(physical_content(prior) == physical_content(witness),
+ "ATTRIBUTION_NOT_OBSERVED: version fault changed before the manual token")
+ if plan.get("negative"):
+ target = plan["proofs"][0]
+ fleet.record("quorum-contract", {"contract": contract, "readQuorum": plan["readQuorum"],
+ "availableSameVersionShards": len(plan["retainedShards"]), "poolIndex": 0, "setIndex": 0})
+ denied = fleet.record("negative-get", probe.s3("get_object", {
+ "Bucket": bucket, "Key": target["key"], "VersionId": target["versionId"]}))
+ validate_unrecoverable_get(denied, contract)
+ require(physical_content(prior) == physical_content(fleet.snapshot("physical-after-negative-get")),
+ "negative read widened damage or silently healed below-quorum fragments")
+ admissions_before_attempt = len(admissions)
+ if args.case_id == "heal.read-repair-attribution":
+ status = tracked_read_task(probe, fleet, bucket, branch, plan["expected"][0])
+ elif scanner:
+ status = scanner_task(probe, fleet, scanner_window, bucket, plan["expected"][0], branch)
+ else:
+ status = run_task(probe, fleet, plan["scope"], 2, active,
+ option_overrides={"recursive": plan["recursive"]}, admissions=admissions,
+ allow_coordination_rejection=(args.case_id == "heal.disk-readonly-and-permission"
+ and branch == "permission-denied"))
+ permission_live_rejoin = (gate_node is not None
+ and args.case_id == "heal.disk-readonly-and-permission" and branch == "permission-denied")
+ if permission_live_rejoin:
+ # Keep the candidate alive: the old stop/clear/start sequence
+ # allowed a restarted process to restore the part before the
+ # fresh writable Admin token could be attributed.
+ repaired = fleet.snapshot("physical-after-" + branch, metadata_faults=metadata_faults)
+ else:
+ fleet.stop()
+ repaired = fleet.snapshot("physical-after-" + branch, quiescent=True,
+ metadata_faults=metadata_faults)
+ if gate_node is not None:
+ require(physical_content(prior) == physical_content(repaired),
+ "unwritable window changed object files or wrote outside the target")
+ if status.get("admissionState") == "coordination-rejected":
+ rejection = validate_coordination_rejection(status, path=status.get("path"))
+ logs = {node: fleet.record("coordination-rejection-logs", {"node": node,
+ **fleet.remote(node, "logs")}) for node in fleet.nodes}
+ rejection["logProof"] = validate_coordination_rejection_logs(
+ {"request": status["requestWindow"], "peers": logs})
+ rejection["physicalUnchanged"] = True
+ gated_proof = rejection
+ else:
+ gated_proof = validate_unwritable(status, bucket, plan["expected"][0])
+ fleet.record("unwritable-responsibility", gated_proof)
+ if permission_live_rejoin:
+ release_proof = release_permission_gate_live(fleet, gate_node, repaired, metadata_faults)
+ fleet.record("same-process-writable-repair-boundary", release_proof)
+ else:
+ fleet.record("drive-gate-released", fleet.remote(gate_node, "drive-gate", mode="clear"))
+ fleet.drive_gates.remove(gate_node)
+ gate_node = None
+ if not permission_live_rejoin:
+ fleet.start()
+ restored_status = run_task(probe, fleet, plan["scope"], 2, active,
+ option_overrides={"recursive": False}, admissions=admissions)
+ if status.get("admissionState") == "coordination-rejected":
+ require(len(admissions) == admissions_before_attempt + 1
+ and admissions[-1]["clientToken"] not in {item["clientToken"]
+ for item in admissions[:admissions_before_attempt]},
+ "restored writable repair lacks a fresh unique task identity")
+ else:
+ require(len(admissions) >= 2 and admissions[-1]["clientToken"] != admissions[-2]["clientToken"],
+ "restored writable repair reused the failed task identity")
+ status = restored_status
+ fleet.stop()
+ repaired = fleet.snapshot("physical-writable-repair", quiescent=True)
+ negative_proof = None
+ if plan.get("negative"):
+ require(physical_content(prior) == physical_content(repaired),
+ "below-quorum Heal deleted metadata, changed good shards or falsely persisted repair")
+ negative_proof = validate_failed_version(status, bucket, plan["expected"], plan["proofs"][0])
+ fleet.record("negative-before-rescue-proof", negative_proof)
+ for registration in reversed(registrations):
+ node, params = registration
+ fleet.record("negative-rescue", fleet.remote(node, "recover-fault", **params))
+ fleet.faults.remove(registration)
+ registrations.clear()
+ recovered = fleet.snapshot("physical-backup-recovered-" + branch, quiescent=True)
+ require(physical_content(before) == physical_content(recovered), "negative fixture rescue differs from original")
+ fleet.start()
+ status = run_task(probe, fleet, plan["scope"], 2, active,
+ option_overrides={"recursive": True}, admissions=admissions)
+ fleet.stop()
+ repaired = fleet.snapshot("physical-after-negative-recovery-task", quiescent=True)
+ physical = validate_repaired_physical(before, repaired, plan["proofs"][0], repair_targets=plan["proofs"])
+ bitrot = [verify_proof(fleet, target, "repaired-bitrot-" + branch) for target in plan["proofs"]]
+ fleet.start()
+ s3 = verify_version_s3(probe, fleet, bucket, expected, baseline, "s3-after-" + branch, fixture=fixture)
+ if lock_context is not None:
+ s3["protection"] = prove_locked(probe, fleet, bucket, lock_context,
+ key=LOCK_KEYS[LOCK_BRANCHES.index(branch)], label="lock-after-" + branch)
+ if args.case_id == "heal.delete-marker-semantics":
+ s3["markerSemantics"] = verify_markers(probe, fleet, bucket, plan["scope"]["key"], expected)
+ if size_rule:
+ s3["actualBoundaryRanges"] = verify_boundary_ranges(probe, fleet, bucket, expected, sizes["actualECBlockSize"])
+ # These faults have now been independently repaired and fully
+ # proved. Finalization discards their backups; it never writes
+ # old metadata over a legitimate product re-encoding.
+ for registration in registrations:
+ fleet.faults.remove(registration)
+ registrations.clear()
+ fleet.record("fault-finalization-proof", {"variant": branch, "allOriginalPartBytesVerified": True,
+ "decodedVersionReferencesAndS3Equal": True, "backupRestorationUsed": False,
+ "canonicalVerdict": "still requires the exact responsibility oracle"})
+ responsibility = validate_version_receipts(status, bucket, plan["expected"], 2,
+ required_repairs=plan["requiredRepairs"], metadata_repairs=plan["metadataRepairs"])
+ if args.case_id == "heal.delete-marker-semantics":
+ expected, baseline, s3["markerRemoval"] = reveal_current(
+ recorder, fleet, bucket, plan["scope"]["key"], expected, baseline)
+ repeats = []
+ if args.case_id == "heal.repeat-idempotency":
+ fleet.stop()
+ post_heal = fleet.snapshot("post-heal-repeat-baseline-" + branch, quiescent=True)
+ fleet.start()
+ modes = (1,) if branch == "normal-repeat" else (2,)
+ if branch in {"repeat-after-restart", "versioned-repeat"}:
+ modes = (1, 2)
+ first_token = admissions[-1]["clientToken"]
+ for mode in modes:
+ # Restart the same run-owned units, keeping disks and
+ # candidate identity, before the process repeat branch.
+ if branch == "repeat-after-restart":
+ old_processes = fleet.snapshot("before-process-restart")
+ fleet.stop()
+ fleet.start()
+ new_processes = fleet.snapshot("restarted-process")
+ require(all(old_processes[node]["unit"] == new_processes[node]["unit"]
+ and old_processes[node]["pid"] != new_processes[node]["pid"]
+ for node in nodes), "process repeat did not restart the same original units")
+ fleet.record("process-restart-proof", {"before": old_processes, "after": new_processes})
+ repeat = run_task(probe, fleet, plan["scope"], mode, active,
+ option_overrides={"recursive": plan["recursive"]}, admissions=admissions)
+ require(admissions[-1]["clientToken"] != first_token
+ and len({entry["clientToken"] for entry in admissions}) == len(admissions),
+ "repeat task reused a token instead of admitting independent responsibility")
+ proof = validate_version_receipts(repeat, bucket, plan["expected"], mode)
+ fleet.stop()
+ after_repeat = fleet.snapshot("physical-repeat-" + branch, quiescent=True)
+ validate_repaired_physical(post_heal, after_repeat, plan["proofs"][0], repair_targets=plan["proofs"])
+ for target in plan["proofs"]:
+ verify_proof(fleet, target, "repeat-bitrot-" + branch)
+ fleet.start()
+ proof["s3"] = verify_version_s3(probe, fleet, bucket, expected, baseline, "s3-repeat-" + branch, fixture=fixture)
+ repeats.append(proof)
+ result["branches"][branch] = {"verdict": "PASS", "physical": physical, "bitrot": bitrot,
+ "s3": s3, "responsibility": responsibility, "repeats": repeats}
+ if instances:
+ result["branches"][branch]["parameters"] = plan["parameters"]
+ if negative_proof is not None:
+ result["branches"][branch]["negative"] = negative_proof
+ result["branches"][branch]["physical"]["backupUsedAfterFailureProof"] = True
+ result["branches"][branch]["physical"]["productRepairClaimed"] = False
+ if gated_proof is not None:
+ result["branches"][branch]["unwritableWindow"] = gated_proof
+ except (Exception, KeyboardInterrupt) as exc:
+ result["branches"][branch] = {"verdict": "FAIL", "failure": {"type": type(exc).__name__, "message": str(exc)}}
+ try:
+ fleet.stop()
+ if args.case_id in {"heal.disk-replacement-formatted", "heal.replacement-auto-rebuild", "heal.target-graceful-restart", "heal.target-process-crash"}:
+ for node in fleet.nodes:
+ fleet.record("replacement-failure-format-and-original", {"node": node,
+ **fleet.remote(node, "replacement-state")})
+ if gate_node is not None:
+ fleet.record("gate-error-rescue", fleet.remote(gate_node, "drive-gate", mode="clear"))
+ fleet.drive_gates.remove(gate_node)
+ fleet.snapshot("physical-failure-" + branch, quiescent=True, metadata_faults=metadata_faults)
+ for node in tuple(fleet.drive_gates):
+ fleet.record("marker-gate-error-rescue", fleet.remote(node, "drive-gate", mode="clear"))
+ fleet.drive_gates.remove(node)
+ for registration in reversed(registrations):
+ node, params = registration
+ fleet.record("rescue-" + branch, fleet.remote(node, "recover-fault", **params))
+ fleet.faults.remove(registration)
+ except Exception as recovery_error:
+ result["branches"][branch]["recoveryError"] = str(recovery_error)
+ raise
+ if isinstance(exc, KeyboardInterrupt) or active or stale_marker_started:
+ raise
+ restart_after_failure = args.case_id == "heal.multiple-shard-loss"
+ result["verdict"] = "PASS" if len(result["branches"]) == len(branches) and all(
+ item["verdict"] == "PASS" for item in result["branches"].values()) else "FAIL"
+ if args.case_id == "heal.versioning-suspended-null" and result["verdict"] == "PASS":
+ result["reenable"] = reenable_and_verify(recorder, fleet, bucket, baseline)
+ except ReplacementAdmissionBlocked as exc:
+ result.update(verdict="BLOCKED", failure={"type": type(exc).__name__, "message": str(exc)},
+ replacementAdmissionBlocked=True, admissionEvidence=exc.evidence,
+ deploymentSkipped=True, fixtureSeedSkipped=True)
+ except (Exception, KeyboardInterrupt) as exc:
+ result.update(verdict="FAIL", failure={"type": type(exc).__name__, "message": str(exc)})
+ finally:
+ for path, token in active:
+ try:
+ fleet.record("stop-unresolved", probe.admin(path, [("clientToken", token), ("forceStop", "true")], b""))
+ except Exception as exc:
+ fleet.record("stop-error", {"error": str(exc)})
+ cleaned = fleet.cleanup() if lock_context is None else cleanup_locked_fleet(probe, fleet, bucket, lock_context)
+ result["cleanup"] = "PASS" if cleaned else "FAIL"
+ fleet.record("topology-result", result)
+ return result
+
+
+def main():
+ def interrupted(signum, frame):
+ raise KeyboardInterrupt(f"Heal workflow interrupted by signal {signum}")
+ signal.signal(signal.SIGTERM, interrupted)
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--candidate", type=Path, required=True)
+ parser.add_argument("--case-id", choices=tuple(BRANCHES), required=True)
+ parser.add_argument("--testing-commit", required=True)
+ parser.add_argument("--binary-sha", required=True)
+ parser.add_argument("--rc-bin", type=Path, required=True)
+ parser.add_argument("--evidence", type=Path, required=True)
+ parser.add_argument("--tool-directory", type=Path, required=True)
+ args = parser.parse_args()
+ nodes = os.environ.get("RUSTFS_NODES", "vm000 vm001 vm002 vm003").split()
+ require(len(nodes) >= 4 and len(set(nodes)) == len(nodes)
+ and all(re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", node) for node in nodes), "four current Heal nodes required")
+ require(bool(re.fullmatch(r"[0-9a-f]{64}", args.binary_sha)), "invalid candidate binary digest")
+ require(bool(re.fullmatch(r"[1-9][0-9]*", os.environ.get("GITHUB_RUN_ID", ""))), "workflow fleet identity required")
+ fixtures = ("DB", "DV") if args.case_id in {"heal.repeat-idempotency", "heal.below-read-quorum", "heal.missing-or-corrupt-xlmeta", "heal.multiple-shard-loss", "heal.read-repair-attribution", "heal.stale-version-rejoin", "heal.disk-replacement-formatted", "heal.replacement-auto-rebuild", "heal.target-graceful-restart", "heal.target-process-crash"} else ("DV",)
+ if args.case_id == "heal.object-metadata-and-tags":
+ fixtures = ("DS",)
+ if args.case_id == "heal.special-keys-and-pagination":
+ fixtures = ("DK",)
+ if args.case_id == "heal.object-size-boundaries":
+ fixtures = ("DB",)
+ if args.case_id == "heal.object-lock-retention":
+ fixtures = ("DO",)
+ if args.case_id in {"heal.disk-readonly-and-permission", "heal.scanner-cold-object-repair", "heal.disk-loss-and-repair"}:
+ fixtures = ("DB",)
+ allowed_topologies = set(select(args.case_id)[0]["contract"]["probes"]["topology"].split(","))
+ require(allowed_topologies <= {"H1", "H2"}, "version executor lacks a required topology")
+ topologies = [(name, selected) for name, selected in (("H1", nodes[:1]), ("H2", nodes[:4])) if name in allowed_topologies]
+ results = []
+ try:
+ for topology, selected in topologies:
+ for fixture in fixtures:
+ results.append(run_topology(args, topology, selected, fixture))
+ if results[-1].get("replacementAdmissionBlocked") is True:
+ break
+ if results[-1]["cleanup"] != "PASS":
+ break
+ if results and results[-1].get("replacementAdmissionBlocked") is True:
+ break
+ if results[-1]["cleanup"] != "PASS":
+ break
+ finally:
+ complete = len(results) == len(topologies) * len(fixtures) and all(
+ item["verdict"] == "PASS" and item["cleanup"] == "PASS" for item in results)
+ replacement_admission_blocked = (args.case_id == "heal.disk-replacement-formatted" and bool(results)
+ and all(item.get("replacementAdmissionBlocked") is True and item.get("cleanup") == "PASS"
+ for item in results))
+ save(args.evidence / "healthy-summary.json", {"caseId": args.case_id, "results": results,
+ "cleanup": "PASS" if results and all(item["cleanup"] == "PASS" for item in results) else "FAIL",
+ "complete": complete, "replacementAdmissionBlocked": replacement_admission_blocked,
+ "canonicalPassClaimed": False})
+ if replacement_admission_blocked:
+ print("heal.disk-replacement-formatted: BLOCKED before deployment or fixture seed by mount admission preflight")
+ return
+ require(complete, "version case requires every H1/H2 source branch; inspect topology-result evidence")
+
+
+if __name__ == "__main__":
+ try:
+ main()
+ except Exception as exc:
+ print(f"Heal version validation failed: {exc}", file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_heal/write_quorum.py b/scripts/validation_heal/write_quorum.py
new file mode 100644
index 00000000..331c1506
--- /dev/null
+++ b/scripts/validation_heal/write_quorum.py
@@ -0,0 +1,399 @@
+#!/usr/bin/env python3
+"""B08: independently witnessed write and distributed-lock quorum loss."""
+
+import argparse
+import copy
+from datetime import datetime, timedelta, timezone
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import secrets
+import signal
+import sys
+from urllib import request
+from urllib.error import URLError
+
+try:
+ from .heal_probe import Probe, payload, require, save
+ from .heal_workload import Recorder, seed, verify_seed
+ from .healthy import Fleet, physical_content, validate_topology
+ from .inspector import verify as verify_inspector
+ from .matrix import select
+ from .shards import (SIZES, files, map_target, run_task, validate_fault,
+ validate_coordination_rejection, validate_coordination_rejection_logs,
+ validate_repaired_physical, verify_cohort)
+ from .versions import validate_version_receipts, verify_quorum_contract, verify_version_s3
+ from .drive_gate import validate_unwritable
+ from .quorum_boundary import LOCK_REFERENCE, lock_reference
+ from .stop import idle_fleet
+except ImportError:
+ from heal_probe import Probe, payload, require, save
+ from heal_workload import Recorder, seed, verify_seed
+ from healthy import Fleet, physical_content, validate_topology
+ from inspector import verify as verify_inspector
+ from matrix import select
+ from shards import (SIZES, files, map_target, run_task, validate_fault,
+ validate_coordination_rejection, validate_coordination_rejection_logs,
+ validate_repaired_physical, verify_cohort)
+ from versions import validate_version_receipts, verify_quorum_contract, verify_version_s3
+ from drive_gate import validate_unwritable
+ from quorum_boundary import LOCK_REFERENCE, lock_reference
+ from stop import idle_fleet
+
+
+CASE_ID = "heal.below-write-quorum"
+BRANCHES = ("read-present-write-absent", "lock-quorum-absent")
+REFUSALS = {"XMinioWriteQuorum", "XMinioLockTimeout", "InternalError", "ServiceUnavailable", "SlowDown", "RequestTimeout"}
+LOCAL_LOCK_REASON = ("H1 uses the candidate's in-process local manager, not a distributed client quorum. "
+ "Losing its only process also removes the read endpoint; distributed peer-loss is applicable on H2 only.")
+STORAGE_REFERENCE = {"candidateCommit": LOCK_REFERENCE["candidateCommit"],
+ "sourceFiles": {"crates/ecstore/src/config/storageclass.rs":
+ "e7c29544e2798c246e78d978f60f103bd6446eccd560c5c9b490316bca64bf9c"},
+ "H2LockFixture": "explicit EC:8 only for the fresh run-owned sixteen-drive lock-loss branch",
+ "WriteFixture": "all N readable authorities; independent read-only gates remove W even when R equals W"}
+
+STORAGE_REFERENCES = {
+ STORAGE_REFERENCE['candidateCommit']: STORAGE_REFERENCE,
+ '1a3f038c90704f9ad9693eddcf7d861c9530f13c': {
+ **STORAGE_REFERENCE, 'candidateCommit': '1a3f038c90704f9ad9693eddcf7d861c9530f13c'},
+}
+
+
+def source_reference(candidate_commit):
+ locks = lock_reference(candidate_commit)
+ require(candidate_commit in STORAGE_REFERENCES, "B08 storage boundary is not reviewed for this candidate")
+ return {"lock": locks, "storage": copy.deepcopy(STORAGE_REFERENCES[candidate_commit])}
+
+
+def plan_for(snapshot, target, branch, contract):
+ require(branch in BRANCHES and contract.get("recognized") is True
+ and contract.get("writeQuorumRule") == "data-plus-one-if-equal-parity", "unrecognized B08 contract")
+ cohort = target["cohort"]
+ geometry = target["logical"]["erasure"]
+ read, parity = geometry["data_blocks"], geometry["parity_blocks"]
+ write, total = read + int(read == parity), read + parity
+ hosts = list(snapshot)
+ require(len(hosts) in {1, 4} and total == len(cohort) == 4 * len(hosts)
+ and len({(row["node"], row["disk"]) for row in cohort}) == total
+ and all({row["disk"] for row in cohort if row["node"] == node} == set(range(4)) for node in hosts),
+ "B08 requires actual full H1/H2 member identities")
+ require(total - 1 >= read, "layout cannot retain R payload sources after the original missing part")
+ lock = {"mode": "local" if len(hosts) == 1 else "distributed", "clients": len(hosts),
+ "sharedQuorum": (len(hosts) + 1) // 2, "exclusiveQuorum": len(hosts) // 2 + 1}
+ if branch == "lock-quorum-absent" and len(hosts) == 1:
+ return {"applicable": False, "reason": LOCAL_LOCK_REASON, "readQuorum": read,
+ "writeQuorum": write, "lock": lock}
+ ordered = sorted(cohort, key=lambda row: (hosts.index(row["node"]), row["disk"]))
+ if branch == "read-present-write-absent":
+ # Include the precise missing target in the read-only set. All N
+ # metadata authorities and lock clients remain readable/online.
+ selected = [next(row for row in ordered if (row["node"], row["disk"]) == (target["node"], target["disk"]))]
+ selected += [row for row in ordered if (row["node"], row["disk"]) != (target["node"], target["disk"])][:total - write]
+ gates = {node: sorted(row["disk"] for row in selected if row["node"] == node) for node in hosts}
+ gates = {node: disks for node, disks in gates.items() if disks}
+ offline = []
+ sources, metadata, writable, clients = total - 1, total, write - 1, len(hosts)
+ else:
+ offline = hosts[lock["exclusiveQuorum"] - 1:]
+ require(len(offline) == 2, "H2 must lose exactly two distinct lock clients")
+ # The missing target belongs to an offline member, preserving all R
+ # payload sources on the survivors. Do not subtract it twice.
+ require(target["node"] in offline, "lock-negative missing target must be on an unavailable member")
+ gates = {}
+ sources = metadata = writable = total - 4 * len(offline)
+ clients = len(hosts) - len(offline)
+ require(sources >= read and writable < write and metadata >= read
+ and (clients >= lock["exclusiveQuorum"] if gates else lock["sharedQuorum"] <= clients < lock["exclusiveQuorum"]),
+ "B08 fault does not preserve R or independently remove W/lock admission")
+ return {"applicable": True, "readQuorum": read, "writeQuorum": write, "totalDrives": total,
+ "readablePayloadSources": sources, "readableMetadataAuthorities": metadata,
+ "writableDrives": writable, "lock": {**lock, "availableClients": clients},
+ "gates": gates, "offlineNodes": offline,
+ "backend": "service-private-read-only-mounts" if gates else "exact-owned-peer-process-stop",
+ "hostNetworkRulesChanged": False, "hostMountRemounted": False}
+
+
+def expected_reads(snapshot, node, relative):
+ indexed = files(snapshot)
+ result = []
+ for disk in range(4):
+ entry = indexed.get((node, disk, relative))
+ require(entry is not None, "quorum witness lacks original metadata on a selected disk")
+ result.append({name: entry[name] for name in ("size", "sha256")})
+ return result
+
+
+def partition_snapshot(fleet, plan, label):
+ """Keep every physical member while attesting each actual process state."""
+ offline = set(plan['offlineNodes'])
+ require(offline <= set(fleet.nodes), 'quorum snapshot has a foreign stopped node')
+ return fleet.record(label, {node: fleet.remote(node, 'snapshot', quiescent=node in offline)
+ for node in fleet.nodes})
+
+
+def validate_gate_witness(observed, plan, online):
+ require(set(observed) == set(online), "quorum probe omitted an available candidate")
+ rows = [(node, row) for node, sample in observed.items() for row in sample["disks"]]
+ require(len(rows) == 4 * len(online) and len({(node, row["disk"]) for node, row in rows}) == len(rows)
+ and all(row["readableMetadata"] is True and row["sameServiceUidAndMountNamespace"] is True for _, row in rows),
+ "quorum gate witness omitted or duplicated real metadata authorities")
+ readonly = {(node, disk) for node, disks in plan["gates"].items() for disk in disks}
+ require(all(type(row["writable"]) is bool and row["writable"] is ((node, row["disk"]) not in readonly)
+ and row["writeErrno"] == (30 if (node, row["disk"]) in readonly else 0) for node, row in rows)
+ and sum(row["writable"] for _, row in rows) == plan["writableDrives"],
+ "measured service write refusals differ from the frozen W boundary")
+ return {"measuredReadableMetadataAuthorities": len(rows), "measuredWritableDrives": plan["writableDrives"],
+ "diskCountsAreNotLockAdmission": True}
+
+
+def negative_put(observed):
+ require(observed.get("acknowledged") is False and observed.get("clientCallInvoked") is True,
+ "quorum-negative PUT returned a success ACK or was never invoked")
+ if observed.get("resultUnknown") is True:
+ return {"state": "UNKNOWN_PENDING_RESOLUTION", "retryAllowed": False}
+ response = observed.get("response") or {}
+ require(observed.get("resultUnknown") is False and response.get("ResponseMetadata", {}).get("HTTPStatusCode") in {500, 503}
+ and response.get("Error", {}).get("Code") in REFUSALS,
+ "quorum-negative PUT lacks a structured candidate refusal")
+ return {"state": "REFUSED", "retryAllowed": False, "code": response["Error"]["Code"]}
+
+
+def resolve_put(observed, reads, expected_payload):
+ require(bool(reads), "unique PUT outcome was not queried after recovery")
+ states = []
+ for read in reads:
+ if read.get("acknowledged") is True and read.get("resultUnknown") is False:
+ require(read.get("payload") == expected_payload, "uncertain PUT resolved to incorrect committed bytes")
+ states.append("COMMITTED")
+ else:
+ response = read.get("response") or {}
+ require(read.get("acknowledged") is False and read.get("resultUnknown") is False
+ and response.get("ResponseMetadata", {}).get("HTTPStatusCode") == 404
+ and response.get("Error", {}).get("Code") == "NoSuchKey", "unique PUT outcome remains unresolved")
+ states.append("ABSENT")
+ require(len(set(states)) == 1, "peers disagree on the unique PUT commit outcome")
+ if observed.get("acknowledged") is True:
+ require(states[0] == "COMMITTED", "a success PUT ACK cannot be redeemed on the recovered peers")
+ else:
+ require(observed.get("resultUnknown") is True or states[0] == "ABSENT", "refused PUT nevertheless committed")
+ return {"state": states[0], "allPeersAgree": True, "retryIssued": False, "successAckClaimed": False}
+
+
+def endpoint_refused(binding, node_index):
+ endpoint = binding["endpoints"][f"node{node_index + 1}"]
+ try:
+ with request.build_opener(request.ProxyHandler({})).open(endpoint + "/health/ready", timeout=2):
+ raise ValueError("stopped lock peer still accepts requests")
+ except URLError as exc:
+ require(isinstance(exc.reason, ConnectionRefusedError), "lock peer failure is not witnessed endpoint refusal")
+ return {"endpoint": endpoint, "connectionRefused": True, "lockAndDiskEndpointUnavailable": True}
+
+
+def run_branch(args, topology, nodes, branch):
+ directory = args.evidence / topology / branch
+ directory.mkdir(parents=True)
+ run_id = f"gh-{os.environ['GITHUB_RUN_ID']}-{os.environ.get('GITHUB_RUN_ATTEMPT', '1')}-{topology.lower()}"
+ lock_fixture = topology == "H2" and branch == "lock-quorum-absent"
+ fleet = Fleet(nodes, directory, run_id, args.tool_directory, runtime_profile="lock-quorum" if lock_fixture else "manual")
+ candidate = json.loads(args.candidate.read_text())
+ reference = source_reference(candidate["source_sha"])
+ bucket = "rv-heal-" + hashlib.sha256(run_id.encode()).hexdigest()[:16]
+ access, secret = secrets.token_hex(16), secrets.token_hex(24)
+ os.environ.update(HEAL_ACCESS_KEY=access, HEAL_SECRET_KEY=secret)
+ binding = {"schemaVersion": 1, "runId": run_id, "jumpHostOnly": True, "exclusiveHealService": True,
+ "leaseExpiresAt": (datetime.now(timezone.utc) + timedelta(minutes=30)).isoformat(), "bucketPrefix": bucket,
+ "endpoints": {f"node{index}": f"http://rustfs-node{index}:9000" for index in range(1, len(nodes) + 1)},
+ "defaultEndpoint": "node1", "candidateCommit": candidate["source_sha"], "harnessCommit": args.testing_commit,
+ "candidatePackageSha256": candidate["package_sha256"], "candidateBinarySha256": args.binary_sha,
+ "rcBinarySha256": hashlib.sha256(args.rc_bin.read_bytes()).hexdigest(), "maxInventoryObjects": 256,
+ "maxInventoryBytes": 512 * 1024**2}
+ fleet.record("lock-policy-source-contract", reference["lock"])
+ fleet.record("storage-boundary-source-contract", reference["storage"])
+ contract = verify_quorum_contract(fleet.record("inspector-build", verify_inspector(args.tool_directory, candidate)), candidate)
+ fleet.record("binding", binding)
+ probe, active, admissions = Probe(binding), [], []
+ result = {"topology": topology, "branch": branch, "verdict": "RUNNING", "cleanup": "PENDING"}
+ put_observed, put_key, put_payload, original = None, None, None, None
+ try:
+ fleet.deploy(args.binary_sha, bucket, access, secret)
+ if lock_fixture:
+ processes = fleet.snapshot("lock-fixture-actual-storage-configuration")
+ require(all(value["storageClassEnvironment"]["RUSTFS_STORAGE_CLASS_STANDARD"] == "EC:8"
+ for value in processes.values()), "lock fixture did not load its actual sixteen-drive eight-parity configuration")
+ expected = seed(Recorder(probe, directory / "fixture"), bucket, "DB", 128, SIZES)
+ baseline = fleet.record("S3-baseline", probe.inventory(bucket, unversioned_current_tags=True))
+ fleet.record("ACK-manifest", {"expected": expected, **verify_seed(expected, baseline)})
+ fleet.stop()
+ original = fleet.snapshot("physical-baseline", quiescent=True)
+ validate_topology(original, topology)
+ selected = next(entry for entry in expected if entry["payload"]["size"] > 1024**2)
+ target = map_target(original, bucket, selected["key"], "data", 1)
+ if branch == "lock-quorum-absent" and topology == "H2":
+ shard = next(row for row in target["cohort"] if row["node"] == nodes[-1])
+ target = {**target, **shard}
+ plan = fleet.record("frozen-quorum-plan", plan_for(original, target, branch, contract))
+ if not plan["applicable"]:
+ result.update(verdict="NOT_APPLICABLE", reason=plan["reason"])
+ return result
+ params = {"disk": target["disk"], "faultId": "B08-missing-target"}
+ registration = target["node"], params
+ fleet.faults.append(registration)
+ receipt = fleet.record("target-fault", fleet.remote(target["node"], "apply-fault", **params,
+ relativePath=target["relativePath"], kind="missing", expectedSha256=target["sha256"]))
+ faulted = fleet.snapshot("physical-fault", quiescent=True)
+ validate_fault(original, faulted, target, receipt)
+ for node, disks in plan["gates"].items():
+ fleet.quorum_gates.add(node)
+ fleet.record("write-gate-intent", {"node": node, **fleet.remote(node, "quorum-gate", disks=disks)})
+ online = [node for node in nodes if node not in plan["offlineNodes"]]
+ if plan["offlineNodes"]:
+ # Start the intact cluster first, then isolate its exact two owned
+ # peer processes. No host firewall, mount or VM is changed.
+ fleet.start()
+ for node in plan["offlineNodes"]:
+ stopped = fleet.record("lock-peer-stopped", {"node": node, **fleet.remote(node, "stop")})
+ require(stopped.get("quiescent") is True and stopped.get("service", {}).get("MainPID") == "0",
+ "lock peer process did not actually exit")
+ fleet.record("lock-peer-endpoint-refused", endpoint_refused(binding, nodes.index(node)))
+ else:
+ fleet.start()
+ relative = f"{bucket}/{selected['key']}/xl.meta"
+ def witness(label):
+ raw = fleet.record(label, {node: fleet.remote(node, "quorum-gate-probe", relativePath=relative,
+ expectedReads=expected_reads(original, node, relative)) for node in online})
+ return validate_gate_witness(raw, plan, online)
+ fleet.record("quorums-before-operations", witness("service-quorum-before"))
+ require(physical_content(faulted) == physical_content(partition_snapshot(fleet, plan, "physical-before-operations")),
+ "quorum loss changed the fault before its negative operations")
+ read = fleet.record("read-without-write-admission", probe.s3("get_object", {"Bucket": bucket, "Key": selected["key"]}))
+ require(read.get("acknowledged") is True and read.get("resultUnknown") is False
+ and read.get("payload") == selected["payload"], "read quorum did not serve exact acknowledged bytes")
+ put_key = "quorum-negative/unique-put.bin"
+ put_body = payload(f"{bucket}/{put_key}/0", 5 * 1024**2 + 97)
+ put_payload = {"size": len(put_body), "sha256": hashlib.sha256(put_body).hexdigest()}
+ put_observed = fleet.record("single-negative-PUT-no-retry", probe.s3("put_object", {"Bucket": bucket, "Key": put_key,
+ "Metadata": {"heal-fixture": "DL"}, "Tagging": "suite=heal&fixture=DL"}, put_body))
+ result["put"] = negative_put(put_observed)
+ status = run_task(probe, fleet, {"bucket": bucket, "key": selected["key"]}, 2, active,
+ admissions=admissions, allow_coordination_rejection=(topology == "H2"))
+ coordination_rejected = status.get("admissionState") == "coordination-rejected"
+ result["negativeResponsibility"] = validate_coordination_rejection(status) if coordination_rejected \
+ else validate_unwritable(status, bucket, selected)
+ result["negativeResponsibility"]["physicalMutationClaimed"] = False
+ fleet.record("quorums-after-operations", witness("service-quorum-after"))
+ for node in plan["offlineNodes"]:
+ fleet.record("lock-peer-still-refused", endpoint_refused(binding, nodes.index(node)))
+ # Evidence must precede any source return, gate removal, backup rescue
+ # or target GET on the recovered cluster.
+ fleet.stop()
+ scene = fleet.snapshot("physical-negative-before-recovery", quiescent=True)
+ require(physical_content(faulted) == physical_content(scene), "negative write/Heal mutated target or intact controls")
+ logs = {node: fleet.record("negative-quorum-peer-logs", {"node": node, **fleet.remote(node, "logs")})
+ for node in nodes}
+ if coordination_rejected:
+ result["coordinationRejectionLogs"] = validate_coordination_rejection_logs(
+ {"request": status["requestWindow"], "peers": logs})
+ for node in tuple(fleet.quorum_gates):
+ fleet.record("release-write-quorum-gate", fleet.remote(node, "quorum-gate", mode="clear"))
+ fleet.quorum_gates.remove(node)
+ fleet.start()
+ require(physical_content(faulted) == physical_content(fleet.snapshot("physical-source-return-before-repair")),
+ "source return repaired the target before the independent manual recovery task")
+ recovered = run_task(probe, fleet, {"bucket": bucket, "key": selected["key"]}, 2, active,
+ status_endpoints=tuple(binding["endpoints"])[1:], admissions=admissions)
+ if coordination_rejected:
+ require(len(admissions) == 1 and admissions[0]["clientToken"],
+ "recovered quorum repair lacks its first admitted task identity")
+ else:
+ require(len(admissions) == 2 and admissions[-1]["clientToken"] != admissions[-2]["clientToken"],
+ "recovered quorum repair reused the negative-window task identity")
+ result["recoveryResponsibility"] = validate_version_receipts(recovered, bucket, [selected], 2,
+ required_repairs={(selected["key"], "null")})
+ idle_fleet(fleet, binding)
+ fleet.stop()
+ after = fleet.snapshot("physical-reconstructed-before-GET", quiescent=True)
+ result["physical"] = validate_repaired_physical(original, after, target)
+ result["streaming"] = verify_cohort(fleet, target, "all-original-cohort-bitrot")
+ fleet.faults.remove(registration)
+ fleet.start()
+ reads = [fleet.record("unique-PUT-resolution", Probe(binding, endpoint).s3("get_object", {
+ "Bucket": bucket, "Key": put_key})) for endpoint in binding["endpoints"]]
+ result["putResolution"] = resolve_put(put_observed, reads, put_payload)
+ # The complete physical no-mutation oracle also proves this particular
+ # unknown call did not commit. No uncertain call is silently discarded.
+ require(result["putResolution"]["state"] == "ABSENT", "unique PUT committed despite the proven W/lock loss")
+ result["s3"] = {endpoint: verify_version_s3(Probe(binding, endpoint), fleet, bucket, expected, baseline,
+ "all-peer-S3-" + endpoint, fixture="DB") for endpoint in binding["endpoints"]}
+ result["verdict"] = "PASS"
+ except (Exception, KeyboardInterrupt) as exc:
+ result.update(verdict="FAIL", failure={"type": type(exc).__name__, "message": str(exc)})
+ finally:
+ for path, token in active:
+ try: fleet.record("stop-unresolved", probe.admin(path, [("clientToken", token), ("forceStop", "true")], b""))
+ except Exception as exc: fleet.record("stop-unresolved-error", {"error": str(exc)})
+ # An unknown unique PUT is resolved even if a later assertion failed.
+ # Recovery cannot convert that original failed branch to PASS.
+ if put_observed and put_key and "putResolution" not in result:
+ try:
+ fleet.stop()
+ for node in tuple(fleet.quorum_gates):
+ fleet.record("failed-branch-release-gate", fleet.remote(node, "quorum-gate", mode="clear"))
+ fleet.quorum_gates.remove(node)
+ fleet.start()
+ reads = [fleet.record("failed-branch-unique-PUT-resolution", Probe(binding, endpoint).s3("get_object", {
+ "Bucket": bucket, "Key": put_key})) for endpoint in binding["endpoints"]]
+ result["putResolution"] = resolve_put(put_observed, reads, put_payload)
+ except Exception as exc:
+ result["putResolution"] = {"state": "UNRESOLVED", "error": str(exc)}
+ if result.get("putResolution", {}).get("state") == "UNRESOLVED":
+ fleet.stop()
+ fleet.record("retained-unresolved-PUT", {"state": "DIRTY", "namespacesRetained": True,
+ "reason": "the original unique request has no complete all-peer resolution"})
+ clean = False
+ else:
+ clean = fleet.cleanup()
+ result["cleanup"] = "PASS" if clean else "FAIL"
+ fleet.record("branch-result", result)
+ return result
+
+
+def main():
+ def interrupted(signum, frame):
+ raise KeyboardInterrupt(f"B08 workflow interrupted by signal {signum}")
+ signal.signal(signal.SIGTERM, interrupted)
+ parser = argparse.ArgumentParser(description=__doc__)
+ for name in ("candidate", "rc-bin", "evidence", "tool-directory"):
+ parser.add_argument("--" + name, type=Path, required=True)
+ parser.add_argument("--case-id", choices=[CASE_ID], required=True)
+ parser.add_argument("--testing-commit", required=True)
+ parser.add_argument("--binary-sha", required=True)
+ args = parser.parse_args()
+ require(re.fullmatch(r"[0-9a-f]{64}", args.binary_sha) and re.fullmatch(r"[1-9][0-9]*", os.environ.get("GITHUB_RUN_ID", "")),
+ "immutable workflow candidate identity required")
+ require(select(CASE_ID)[0]["variants"] == list(BRANCHES), "B08 source branch omitted")
+ nodes = os.environ.get("RUSTFS_NODES", "vm000 vm001 vm002 vm003").split()
+ require(len(nodes) >= 4 and len(nodes) == len(set(nodes)) and all(re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", n) for n in nodes),
+ "four current Heal nodes required")
+ results = []
+ try:
+ for topology, selected in (("H1", nodes[:1]), ("H2", nodes[:4])):
+ for branch in BRANCHES:
+ results.append(run_branch(args, topology, selected, branch))
+ if results[-1]["cleanup"] != "PASS": break
+ if results[-1]["cleanup"] != "PASS": break
+ finally:
+ complete = len(results) == 4 and all(row["cleanup"] == "PASS" and (row["verdict"] == "PASS"
+ or row["topology"] == "H1" and row["branch"] == "lock-quorum-absent"
+ and row["verdict"] == "NOT_APPLICABLE" and row.get("reason") == LOCAL_LOCK_REASON) for row in results)
+ save(args.evidence / "healthy-summary.json", {"caseId": CASE_ID, "results": results, "complete": complete,
+ "cleanup": "PASS" if results and all(row["cleanup"] == "PASS" for row in results) else "FAIL"})
+ require(complete, "B08 requires full independent write/lock negative evidence, product recovery and all-peer ACK verification")
+
+
+if __name__ == "__main__":
+ try: main()
+ except Exception as exc:
+ print(f"B08 write/lock quorum validation failed: {exc}", file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_ilm/case_plan.py b/scripts/validation_ilm/case_plan.py
new file mode 100644
index 00000000..f81d22f8
--- /dev/null
+++ b/scripts/validation_ilm/case_plan.py
@@ -0,0 +1,30 @@
+"""Select ILM cases from the canonical auto-testing catalog."""
+
+from __future__ import annotations
+
+import json
+from pathlib import Path
+from typing import Iterable
+
+
+CATALOG = Path(__file__).resolve().parents[2] / "validation-cases.json"
+
+
+def ilm_case_ids() -> tuple[str, ...]:
+ catalog = json.loads(CATALOG.read_text(encoding="utf-8"))
+ modules = [module for module in catalog["modules"] if module["id"] == "ilm-expiration"]
+ if len(modules) != 1:
+ raise ValueError("canonical ILM module is missing or duplicated")
+ return tuple(case["id"] for case in modules[0]["cases"])
+
+
+ILM_CASE_IDS = ilm_case_ids()
+
+
+def ilm_harness_scored_cases(requested_cases: Iterable[str] | None) -> tuple[str, ...]:
+ if requested_cases is None:
+ return ILM_CASE_IDS
+ selected = tuple(requested_cases)
+ if len(selected) != len(set(selected)) or set(selected) - set(ILM_CASE_IDS):
+ raise ValueError("duplicate or unknown canonical ILM case")
+ return tuple(case_id for case_id in ILM_CASE_IDS if case_id in selected)
diff --git a/scripts/validation_ilm/ilm_behavior.py b/scripts/validation_ilm/ilm_behavior.py
new file mode 100644
index 00000000..dc7af7d5
--- /dev/null
+++ b/scripts/validation_ilm/ilm_behavior.py
@@ -0,0 +1,2677 @@
+#!/usr/bin/env python3
+"""Run destructive-in-a-test-bucket RustFS ILM behavior checks."""
+
+from __future__ import annotations
+
+import argparse
+import base64
+import hashlib
+import json
+import math
+import os
+import re
+import secrets
+import shlex
+import shutil
+import signal
+import subprocess
+import sys
+import tempfile
+import threading
+import time
+import xml.etree.ElementTree as ET
+from dataclasses import asdict, dataclass, field
+from datetime import UTC, datetime
+from pathlib import Path
+from typing import Any, Callable
+from urllib import error as urllib_error
+from urllib import request as urllib_request
+from urllib.parse import quote, unquote, urlparse
+
+from ilm_timing import (
+ BoundedS3Client,
+ CycleProgress,
+ DueWindow,
+ ServerClock,
+ expiry_window,
+ parse_expiry_date,
+ time_budget,
+)
+
+from case_plan import (
+ ILM_CASE_IDS as CASE_IDS,
+ ilm_harness_scored_cases,
+)
+from rc_binary import resolve_rc_binary
+
+try:
+ import boto3
+ import botocore
+ from botocore.auth import SigV4Auth
+ from botocore.awsrequest import AWSRequest
+ from botocore.config import Config
+ from botocore.credentials import Credentials
+ from botocore.exceptions import (
+ ClientError,
+ ConnectTimeoutError,
+ EndpointConnectionError,
+ ReadTimeoutError,
+ )
+except ImportError as exc: # pragma: no cover - dependency guidance
+ raise SystemExit(
+ "boto3 is required for ILM validation"
+ ) from exc
+
+
+INFRA_EXCEPTIONS = (ConnectTimeoutError, EndpointConnectionError, ReadTimeoutError)
+INFRA_ERROR_CODES = {
+ "AccessDenied",
+ "InvalidAccessKeyId",
+ "InvalidToken",
+ "RequestTimeout",
+ "ServiceUnavailable",
+ "SignatureDoesNotMatch",
+ "SlowDown",
+}
+
+S3_DELETE_RETRYABLE_ERROR_CODES = {
+ "InternalError",
+ "RequestTimeout",
+ "ServiceUnavailable",
+ "SlowDown",
+}
+
+def utc_now() -> str:
+ return datetime.now(UTC).isoformat().replace("+00:00", "Z")
+
+
+def json_default(value: Any) -> Any:
+ if isinstance(value, datetime):
+ return value.astimezone(UTC).isoformat().replace("+00:00", "Z")
+ if isinstance(value, bytes):
+ return {
+ "bytes": len(value),
+ "sha256": hashlib.sha256(value).hexdigest(),
+ }
+ return str(value)
+
+
+def safe_name(value: str) -> str:
+ return "".join(char if char.isalnum() or char in "-." else "-" for char in value)
+
+
+@dataclass
+class CaseResult:
+ id: str
+ state: str
+ message: str
+ started_at: str
+ finished_at: str
+ duration_seconds: float
+ evidence: list[str] = field(default_factory=list)
+
+
+class CaseExecutionAndCleanupError(Exception):
+ def __init__(
+ self,
+ primary_phase: str,
+ primary_error: Exception,
+ cleanup_phase: str,
+ cleanup_error: Exception,
+ ) -> None:
+ self.primary_phase = primary_phase
+ self.primary_error = primary_error
+ self.cleanup_phase = cleanup_phase
+ self.cleanup_error = cleanup_error
+ super().__init__(
+ f"{primary_phase} failed: {type(primary_error).__name__}: "
+ f"{primary_error}; cleanup {cleanup_phase} failed: "
+ f"{type(cleanup_error).__name__}: {cleanup_error}"
+ )
+
+
+class EventRecorder:
+ def __init__(self, path: Path) -> None:
+ self.path = path
+ self.lock = threading.Lock()
+ self.path.parent.mkdir(parents=True, exist_ok=True)
+
+ def record(self, event: str, **details: Any) -> None:
+ item = {"timestamp": utc_now(), "monotonic": time.monotonic(), "event": event, "details": details}
+ with self.lock:
+ with self.path.open("a", encoding="utf-8") as handle:
+ handle.write(
+ json.dumps(item, default=json_default, sort_keys=True) + "\n"
+ )
+
+
+class IlmHarness:
+ def __init__(self, args: argparse.Namespace) -> None:
+ self.args = args
+ self.evidence_dir = args.evidence_dir.resolve()
+ self.evidence_dir.mkdir(parents=True, exist_ok=True)
+ self.case_dir = self.evidence_dir / "cases"
+ self.case_dir.mkdir(parents=True, exist_ok=True)
+ self.events = EventRecorder(self.evidence_dir / "events.jsonl")
+ self.results: list[CaseResult] = []
+ self.created_buckets: set[str] = set()
+ self.uncertain_buckets: set[str] = set()
+ self.state_lock = threading.Lock()
+ self.rc_home = Path(tempfile.mkdtemp(prefix="rustfs-ilm-rc-"))
+ alias_base = f"ilm-{safe_name(args.run_id)[-16:]}"
+ self.rc_aliases = tuple(
+ (f"{alias_base}-{index}", endpoint)
+ for index, endpoint in enumerate(args.scanner_endpoint)
+ )
+ self.rc_alias = self.rc_aliases[0][0]
+ self.rc_env = os.environ.copy()
+ self.rc_env["RC_CONFIG_DIR"] = str(self.rc_home)
+ self.rc_bin = args.rc_bin
+ self.rc_version = "unavailable"
+ self.rc_binary_sha256 = "unavailable"
+ self.expected_rc_binary_sha256 = os.environ.get(
+ "RUSTFS_EXPECTED_RC_SHA256", ""
+ ).lower()
+ self.clock: ServerClock | None = None
+ self.readiness: dict[str, Any] = {"state": "not_started"}
+ self.blocked_reason: str | None = None
+ self.case_buckets: set[str] = set()
+ self.client = boto3.client(
+ "s3",
+ endpoint_url=args.endpoint,
+ aws_access_key_id=args.access_key,
+ aws_secret_access_key=args.secret_key,
+ region_name=args.region,
+ config=Config(
+ signature_version="s3v4",
+ s3={"addressing_style": "path"},
+ retries={"total_max_attempts": 1, "mode": "standard"},
+ connect_timeout=5,
+ read_timeout=args.request_timeout,
+ ),
+ )
+ self.client = BoundedS3Client(self.client, args.request_timeout)
+ self.node_clients = {args.endpoint: self.client}
+ for endpoint in args.scanner_endpoint:
+ if endpoint not in self.node_clients:
+ self.node_clients[endpoint] = BoundedS3Client(boto3.client(
+ "s3", endpoint_url=endpoint, region_name=args.region,
+ aws_access_key_id=args.access_key, aws_secret_access_key=args.secret_key,
+ config=Config(signature_version="s3v4", s3={"addressing_style": "path"},
+ retries={"total_max_attempts": 1, "mode": "standard"},
+ connect_timeout=5, read_timeout=args.request_timeout),
+ ), args.request_timeout)
+ self.tier_client = None
+ if args.tier_endpoint and args.tier_access_key and args.tier_secret_key:
+ self.tier_client = boto3.client(
+ "s3",
+ endpoint_url=args.tier_endpoint,
+ aws_access_key_id=args.tier_access_key,
+ aws_secret_access_key=args.tier_secret_key,
+ region_name=args.region,
+ config=Config(
+ signature_version="s3v4",
+ s3={"addressing_style": "path"},
+ retries={"total_max_attempts": 1, "mode": "standard"},
+ connect_timeout=5,
+ read_timeout=args.request_timeout,
+ ),
+ )
+ self.tier_client = BoundedS3Client(self.tier_client, args.request_timeout)
+
+ def close(self) -> None:
+ for client in self.node_clients.values():
+ client.close()
+ if self.tier_client is not None:
+ self.tier_client.close()
+ shutil.rmtree(self.rc_home, ignore_errors=True)
+
+ def evidence_path(self, case_id: str, name: str) -> Path:
+ directory = self.case_dir / safe_name(case_id)
+ directory.mkdir(parents=True, exist_ok=True)
+ return directory / name
+
+ def save_json(self, case_id: str, name: str, value: Any) -> str:
+ path = self.evidence_path(case_id, name)
+ path.write_text(
+ json.dumps(value, default=json_default, indent=2, sort_keys=True) + "\n",
+ encoding="utf-8",
+ )
+ return str(path.relative_to(self.evidence_dir))
+
+ def save_text(self, case_id: str, name: str, value: str) -> str:
+ path = self.evidence_path(case_id, name)
+ path.write_text(value, encoding="utf-8")
+ return str(path.relative_to(self.evidence_dir))
+
+ def redact(self, value: str) -> str:
+ redacted = value
+ for name in (
+ "access_key",
+ "secret_key",
+ "tier_access_key",
+ "tier_secret_key",
+ ):
+ secret = getattr(self.args, name, None)
+ if isinstance(secret, str) and secret:
+ redacted = redacted.replace(secret, "[REDACTED]")
+ return redacted
+
+ def save_phase_error(
+ self,
+ case_id: str,
+ name: str,
+ phase: str,
+ exc: Exception,
+ ) -> str:
+ state = self.classify_exception(exc)
+ message = self.redact(f"{type(exc).__name__}: {exc}")
+ self.events.record(
+ name.removesuffix(".json").replace("-", "."),
+ case=case_id,
+ phase=phase,
+ state=state,
+ errorType=type(exc).__name__,
+ message=message,
+ )
+ return self.save_json(
+ case_id,
+ name,
+ {
+ "phase": phase,
+ "state": state,
+ "errorType": type(exc).__name__,
+ "message": message,
+ },
+ )
+
+ def bucket_name(self, case_id: str) -> str:
+ digest = hashlib.sha256(
+ f"{self.args.run_id}:{case_id}".encode("utf-8")
+ ).hexdigest()[:12]
+ prefix = safe_name(self.args.bucket_prefix.lower()).strip("-")[:45]
+ return f"{prefix}-{digest}"
+
+ def create_bucket(self, bucket: str) -> None:
+ self.events.record("bucket.create.start", bucket=bucket)
+ try:
+ self.client.create_bucket(Bucket=bucket)
+ except Exception:
+ # A lost response can leave a remote creation in progress. Do not
+ # claim clean isolation or continue to another fixture in this run.
+ self.uncertain_buckets.add(bucket)
+ self.blocked_reason = f"bucket creation was not confirmed for {bucket}; isolation requires inspection"
+ raise
+ with self.state_lock:
+ self.created_buckets.add(bucket)
+ self.case_buckets.add(bucket)
+ self.events.record("bucket.create.complete", bucket=bucket)
+
+ def put_lifecycle(
+ self, case_id: str, bucket: str, configuration: dict[str, Any]
+ ) -> list[str]:
+ evidence = [
+ self.save_json(case_id, "lifecycle-request.json", configuration)
+ ]
+ response = self.client.put_bucket_lifecycle_configuration(
+ Bucket=bucket,
+ LifecycleConfiguration=configuration,
+ )
+ evidence.append(self.save_json(case_id, "lifecycle-put-response.json", response))
+ self.events.record(
+ "lifecycle.put",
+ bucket=bucket,
+ rule_ids=[rule.get("ID") for rule in configuration["Rules"]],
+ )
+ return evidence
+
+ def get_lifecycle(self, bucket: str) -> dict[str, Any]:
+ return self.client.get_bucket_lifecycle_configuration(Bucket=bucket)
+
+ def raw_s3_request(
+ self,
+ method: str,
+ bucket: str,
+ *,
+ query: str = "",
+ body: bytes | None = None,
+ content_type: str | None = None,
+ allow_error: bool = False,
+ ) -> tuple[int, dict[str, str], bytes]:
+ url = f"{self.args.endpoint.rstrip('/')}/{quote(bucket, safe='')}"
+ if query:
+ url = f"{url}?{query}"
+ headers: dict[str, str] = {}
+ if content_type:
+ headers["Content-Type"] = content_type
+ payload = body if body is not None else b""
+ headers["x-amz-content-sha256"] = hashlib.sha256(payload).hexdigest()
+ if body is not None:
+ digest = hashlib.md5(body, usedforsecurity=False).digest()
+ headers["Content-MD5"] = base64.b64encode(digest).decode("ascii")
+ signed = AWSRequest(method=method, url=url, data=body, headers=headers)
+ SigV4Auth(
+ Credentials(self.args.access_key, self.args.secret_key),
+ "s3",
+ self.args.region,
+ ).add_auth(signed)
+ outgoing = urllib_request.Request(
+ url,
+ data=body,
+ headers=dict(signed.headers.items()),
+ method=method,
+ )
+ opener = urllib_request.build_opener(urllib_request.ProxyHandler({}))
+ with time_budget(self.args.request_timeout, "raw S3 request", TimeoutError):
+ try:
+ with opener.open(outgoing, timeout=self.args.request_timeout) as response:
+ return response.status, dict(response.headers.items()), response.read()
+ except urllib_error.HTTPError as exc:
+ with exc:
+ response_body = exc.read()
+ if allow_error:
+ return exc.code, dict(exc.headers.items()), response_body
+ raise AssertionError(
+ f"raw S3 {method} request failed with HTTP {exc.code}: "
+ f"{response_body.decode('utf-8', errors='replace')[:500]}"
+ ) from exc
+
+ def configure_rc(self) -> None:
+ requested_rc_bin = self.rc_bin
+ try:
+ self.rc_bin, self.rc_binary_sha256 = resolve_rc_binary(requested_rc_bin)
+ except FileNotFoundError as exc:
+ raise RuntimeError(
+ f"required rc binary not found: {requested_rc_bin}"
+ ) from exc
+ version = subprocess.run(
+ [self.rc_bin, "--version"],
+ env=self.rc_env,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True,
+ check=False,
+ timeout=getattr(self.args, "request_timeout", 10),
+ )
+ if version.returncode != 0:
+ raise RuntimeError(
+ f"rc --version failed with exit {version.returncode}: "
+ f"{version.stderr.strip()}"
+ )
+ self.rc_version = (version.stdout or version.stderr).strip()
+ if (
+ self.expected_rc_binary_sha256
+ and self.rc_binary_sha256.lower() != self.expected_rc_binary_sha256
+ ):
+ raise RuntimeError(
+ "resolved rc binary SHA-256 differs from RUSTFS_EXPECTED_RC_SHA256"
+ )
+ self.events.record(
+ "rc.client.identified",
+ binaryPath=self.rc_bin,
+ version=self.rc_version,
+ binarySha256=self.rc_binary_sha256,
+ )
+ for alias, endpoint in self.rc_aliases:
+ command = [
+ self.rc_bin,
+ "--json",
+ "--no-color",
+ "alias",
+ "set",
+ alias,
+ endpoint,
+ self.args.access_key,
+ self.args.secret_key,
+ "--bucket-lookup",
+ "path",
+ ]
+ deadline = time.monotonic() + min(self.args.timeout, 60)
+ attempts = 0
+ while True:
+ attempts += 1
+ result = subprocess.run(
+ command,
+ env=self.rc_env,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.PIPE,
+ text=True,
+ check=False,
+ timeout=getattr(self.args, "request_timeout", 10),
+ )
+ if result.returncode == 0:
+ break
+ error = result.stderr.strip()
+ try:
+ error_document = json.loads(error)
+ except json.JSONDecodeError:
+ error_document = {}
+ retryable = (
+ isinstance(error_document, dict)
+ and isinstance(error_document.get("details"), dict)
+ and error_document["details"].get("retryable") is True
+ )
+ if not retryable or time.monotonic() >= deadline:
+ raise RuntimeError(
+ f"rc alias setup failed for {endpoint}: {error}"
+ )
+ self.events.record(
+ "rc.alias.retry",
+ alias=alias,
+ endpoint=endpoint,
+ attempt=attempts,
+ error=error[:500],
+ )
+ time.sleep(self.args.poll_interval)
+ self.events.record(
+ "rc.alias.configured",
+ alias=alias,
+ endpoint=endpoint,
+ attempts=attempts,
+ )
+
+ def wait_for_s3_connectivity(self) -> None:
+ deadline = time.monotonic() + min(self.args.timeout, 60)
+ attempts = 0
+ while True:
+ attempts += 1
+ try:
+ self.client.list_buckets()
+ self.events.record(
+ "s3.connectivity.ready",
+ endpoint=self.args.endpoint,
+ attempts=attempts,
+ )
+ return
+ except (*INFRA_EXCEPTIONS, ClientError) as exc:
+ retryable = isinstance(exc, INFRA_EXCEPTIONS)
+ if isinstance(exc, ClientError):
+ response = exc.response
+ status = response.get("ResponseMetadata", {}).get(
+ "HTTPStatusCode", 0
+ )
+ code = response.get("Error", {}).get("Code", "")
+ retryable = status >= 500 or code in {
+ "RequestTimeout",
+ "ServiceUnavailable",
+ "SlowDown",
+ }
+ if not retryable or time.monotonic() >= deadline:
+ raise
+ self.events.record(
+ "s3.connectivity.retry",
+ endpoint=self.args.endpoint,
+ attempt=attempts,
+ error=f"{type(exc).__name__}: {exc}"[:500],
+ )
+ time.sleep(self.args.poll_interval)
+
+ def rc_lifecycle_list(self, case_id: str, bucket: str) -> str:
+ command = [
+ self.rc_bin,
+ "--json",
+ "--no-color",
+ "bucket",
+ "lifecycle",
+ "rule",
+ "list",
+ "--force",
+ f"{self.rc_alias}/{bucket}",
+ ]
+ result = subprocess.run(
+ command,
+ env=self.rc_env,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True,
+ check=False,
+ timeout=getattr(self.args, "request_timeout", 10),
+ )
+ output = result.stdout
+ path = self.evidence_path(case_id, "rc-lifecycle-list.json")
+ path.write_text(output, encoding="utf-8")
+ if result.returncode != 0:
+ raise AssertionError(
+ f"rc lifecycle list failed with exit {result.returncode}: "
+ f"{result.stderr.strip()}"
+ )
+ self.events.record("rc.lifecycle.list", bucket=bucket)
+ return output
+
+ def rc_command(
+ self,
+ case_id: str,
+ name: str,
+ arguments: list[str],
+ *,
+ expect_success: bool = True,
+ secrets_to_redact: tuple[str, ...] = (),
+ ) -> subprocess.CompletedProcess[str]:
+ command = [self.rc_bin, "--json", "--no-color", *arguments]
+ result = subprocess.run(
+ command,
+ env=self.rc_env,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True,
+ check=False,
+ timeout=getattr(self.args, "request_timeout", 10),
+ )
+ sanitized = [
+ "[REDACTED]" if token in secrets_to_redact else token for token in command
+ ]
+ output = {
+ "command": sanitized,
+ "exitCode": result.returncode,
+ "stdout": result.stdout,
+ "stderr": result.stderr,
+ }
+ text = json.dumps(output, indent=2, sort_keys=True) + "\n"
+ for secret in secrets_to_redact:
+ text = text.replace(secret, "[REDACTED]")
+ self.evidence_path(case_id, f"{name}.json").write_text(
+ text, encoding="utf-8"
+ )
+ if expect_success and result.returncode != 0:
+ error = result.stderr
+ for secret in secrets_to_redact:
+ error = error.replace(secret, "[REDACTED]")
+ raise AssertionError(
+ f"rc {name} failed with exit {result.returncode}: {error.strip()}"
+ )
+ if not expect_success and result.returncode == 0:
+ raise AssertionError(f"rc {name} unexpectedly succeeded")
+ return result
+
+ @staticmethod
+ def rc_error_document(stderr: str) -> dict[str, Any]:
+ try:
+ parsed = json.loads(stderr.strip())
+ except json.JSONDecodeError:
+ return {}
+ return parsed if isinstance(parsed, dict) else {}
+
+ @classmethod
+ def rc_error_mentions(cls, stderr: str, code: str) -> bool:
+ if code in stderr:
+ return True
+ document = cls.rc_error_document(stderr)
+ if document.get("error") == code:
+ return True
+ details = document.get("details")
+ if isinstance(details, dict):
+ return any(
+ isinstance(value, str) and code in value
+ for value in details.values()
+ )
+ return False
+
+ def rc_tier_remove_eventually(
+ self,
+ case_id: str,
+ tier_name: str,
+ *,
+ timeout: float | None = None,
+ ) -> subprocess.CompletedProcess[str]:
+ command = [
+ self.rc_bin,
+ "--json",
+ "--no-color",
+ "ilm",
+ "tier",
+ "remove",
+ tier_name,
+ self.rc_alias,
+ "--force",
+ ]
+ deadline = time.monotonic() + (self.args.timeout if timeout is None else timeout)
+ attempts = 0
+ attempt_summaries: list[dict[str, Any]] = []
+ while True:
+ attempts += 1
+ remaining = deadline - time.monotonic()
+ with time_budget(remaining, "rc tier removal"):
+ result = subprocess.run(
+ command,
+ env=self.rc_env,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True,
+ check=False,
+ timeout=min(getattr(self.args, "request_timeout", 10), remaining),
+ )
+ attempt_name = f"tier-remove-attempt-{attempts}.json"
+ attempt_summary = {
+ "attempt": attempts,
+ "exitCode": result.returncode,
+ "stdout": result.stdout,
+ "stderr": result.stderr,
+ }
+ attempt_summaries.append(attempt_summary)
+ self.evidence_path(case_id, attempt_name).write_text(
+ json.dumps(
+ {"command": command, **attempt_summary},
+ indent=2,
+ sort_keys=True,
+ )
+ + "\n",
+ encoding="utf-8",
+ )
+ self.evidence_path(case_id, "tier-remove.json").write_text(
+ json.dumps(
+ {
+ "command": command,
+ "attempts": attempt_summaries,
+ "exitCode": result.returncode,
+ "stdout": result.stdout,
+ "stderr": result.stderr,
+ },
+ indent=2,
+ sort_keys=True,
+ )
+ + "\n",
+ encoding="utf-8",
+ )
+ if time.monotonic() >= deadline:
+ raise AssertionError("rc tier removal completed after its deadline")
+ if result.returncode == 0:
+ self.events.record(
+ "rc.tier_remove.complete",
+ case=case_id,
+ tier=tier_name,
+ attempts=attempts,
+ )
+ return result
+
+ retryable = self.rc_error_mentions(
+ result.stderr,
+ "XMinioAdminTierBackendInUse",
+ )
+ if not retryable or time.monotonic() >= deadline:
+ error = result.stderr.strip()
+ self.events.record(
+ "rc.tier_remove.failed",
+ case=case_id,
+ tier=tier_name,
+ attempts=attempts,
+ retryable=retryable,
+ error=error[:500],
+ )
+ raise AssertionError(
+ f"rc tier-remove failed with exit {result.returncode}: {error}"
+ )
+
+ self.events.record(
+ "rc.tier_remove.retry",
+ case=case_id,
+ tier=tier_name,
+ attempt=attempts,
+ error=result.stderr.strip()[:500],
+ )
+ time.sleep(min(self.args.poll_interval, max(0, deadline - time.monotonic())))
+
+ @staticmethod
+ def client_error_code(exc: ClientError) -> str:
+ response = getattr(exc, "response", {})
+ if not isinstance(response, dict):
+ return ""
+ error = response.get("Error", {})
+ if not isinstance(error, dict):
+ return ""
+ code = error.get("Code")
+ return code if isinstance(code, str) else ""
+
+ def delete_object_eventually(
+ self,
+ case_id: str,
+ bucket: str,
+ key: str,
+ *,
+ evidence_name: str,
+ timeout: float | None = None,
+ ) -> dict[str, Any]:
+ deadline = time.monotonic() + (self.args.timeout if timeout is None else timeout)
+ attempts = 0
+ attempt_summaries: list[dict[str, Any]] = []
+ while True:
+ attempts += 1
+ try:
+ with time_budget(deadline - time.monotonic(), "S3 object deletion"):
+ response = self.client.delete_object(Bucket=bucket, Key=key)
+ except ClientError as exc:
+ code = self.client_error_code(exc)
+ retryable = code in S3_DELETE_RETRYABLE_ERROR_CODES
+ attempt_summary = {
+ "attempt": attempts,
+ "state": "failed",
+ "errorCode": code,
+ "errorMessage": str(exc),
+ "retryable": retryable,
+ }
+ attempt_summaries.append(attempt_summary)
+ self.save_json(
+ case_id,
+ f"{evidence_name}-attempt-{attempts}.json",
+ attempt_summary,
+ )
+ self.save_json(
+ case_id,
+ f"{evidence_name}.json",
+ {
+ "bucket": bucket,
+ "key": key,
+ "attempts": attempt_summaries,
+ "state": "failed",
+ },
+ )
+ if not retryable or time.monotonic() >= deadline:
+ self.events.record(
+ "s3.delete_object.failed",
+ case=case_id,
+ bucket=bucket,
+ key=key,
+ attempts=attempts,
+ errorCode=code,
+ retryable=retryable,
+ )
+ raise
+ self.events.record(
+ "s3.delete_object.retry",
+ case=case_id,
+ bucket=bucket,
+ key=key,
+ attempt=attempts,
+ errorCode=code,
+ )
+ time.sleep(min(self.args.poll_interval, max(0, deadline - time.monotonic())))
+ continue
+
+ attempt_summary = {
+ "attempt": attempts,
+ "state": "success",
+ "response": response,
+ }
+ attempt_summaries.append(attempt_summary)
+ self.save_json(
+ case_id,
+ f"{evidence_name}-attempt-{attempts}.json",
+ attempt_summary,
+ )
+ summary = {
+ "bucket": bucket,
+ "key": key,
+ "attempts": attempt_summaries,
+ "state": "success",
+ "response": response,
+ }
+ self.save_json(case_id, f"{evidence_name}.json", summary)
+ if time.monotonic() >= deadline:
+ raise AssertionError("S3 object deletion completed after its deadline")
+ self.events.record(
+ "s3.delete_object.complete",
+ case=case_id,
+ bucket=bucket,
+ key=key,
+ attempts=attempts,
+ )
+ return summary
+
+ def rc_scanner_status(self, case_id: str, name: str) -> dict[str, Any]:
+ nodes: list[dict[str, Any]] = []
+ errors: list[str] = []
+ for alias, endpoint in self.rc_aliases:
+ command = [
+ self.rc_bin,
+ "--json",
+ "--no-color",
+ "admin",
+ "scanner",
+ "status",
+ alias,
+ ]
+ result = subprocess.run(
+ command,
+ env=self.rc_env,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True,
+ check=False,
+ timeout=getattr(self.args, "request_timeout", 10),
+ )
+ if result.returncode != 0:
+ error = (
+ f"{endpoint}: rc scanner status exited {result.returncode}: "
+ f"{result.stderr.strip()}"
+ )
+ nodes.append({"endpoint": endpoint, "error": error})
+ errors.append(error)
+ continue
+ try:
+ status = json.loads(result.stdout)
+ except json.JSONDecodeError:
+ error = f"{endpoint}: rc scanner status did not return JSON"
+ nodes.append({"endpoint": endpoint, "error": error})
+ errors.append(error)
+ continue
+ nodes.append({"endpoint": endpoint, "status": status})
+ cluster_status = {"nodes": nodes}
+ self.save_json(case_id, name, cluster_status)
+ leaders = [
+ node["endpoint"]
+ for node in nodes
+ if node.get("status", {})
+ .get("data", {})
+ .get("metrics", {})
+ .get("leader_lock_held_by_this_process")
+ is True
+ ]
+ observations = []
+ for node in nodes:
+ data = node.get("status", {}).get("data", {})
+ metrics = data.get("metrics", {})
+ observations.append({
+ "endpoint": node["endpoint"], "error": node.get("error"),
+ "enabled": data.get("enabled"), "health": data.get("health"),
+ "freshness": data.get("freshness", {}),
+ "executionRole": data.get("cycle_schedule", {}).get("execution_role"),
+ "leaderHeldHere": metrics.get("leader_lock_held_by_this_process"),
+ "leaderLock": metrics.get("leader_lock_state"),
+ "lastCycleResult": metrics.get("last_cycle_result"),
+ "lifetimeCycles": metrics.get("life_time_ops", {}).get("scan_cycle"),
+ "currentCycle": metrics.get("current_cycle"),
+ "currentStarted": metrics.get("current_started"),
+ })
+ self.events.record(
+ "rc.scanner.status",
+ case=case_id,
+ endpoints=[node["endpoint"] for node in nodes],
+ leaders=leaders,
+ observations=observations,
+ )
+ if errors:
+ raise RuntimeError("; ".join(errors))
+ return cluster_status
+
+ def calibrate_clock(self, case_id: str) -> None:
+ samples = []
+ for endpoint, client in self.node_clients.items():
+ started = time.time()
+ response = client.list_buckets()
+ finished = time.time()
+ samples.append((endpoint, ServerClock.response_date(response), started, finished))
+ self.clock = ServerClock(samples, max_skew=self.args.max_clock_skew)
+ self.clock_sample_index = getattr(self, "clock_sample_index", 0) + 1
+ self.save_json(case_id, f"clock-{self.clock_sample_index:04d}.json", self.clock.as_dict())
+
+ def prepared_days(self, allowance: float = 0) -> int:
+ # Independent of how long previous cases took; choose before creating data.
+ setup = (self.args.setup_budget + allowance + 2 * self.args.max_clock_skew
+ + 2 * self.args.poll_interval)
+ return max(1, math.ceil(setup / self.args.debug_day_seconds))
+
+ def object_window(self, case_id: str, bucket: str, key: str, days: int) -> tuple[DueWindow, dict[str, Any]]:
+ assert self.clock is not None
+ started = time.time()
+ head = self.client.head_object(Bucket=bucket, Key=key)
+ self.clock.validate_response(head, started, time.time())
+ due = expiry_window(head["LastModified"], days, self.args.debug_day_seconds, self.args.process_seconds)
+ self.save_json(case_id, f"timing-{safe_name(bucket)}-{safe_name(key)}.json", {
+ "bucket": bucket, "key": key, "days": days,
+ "lastModified": head["LastModified"], "due": due.as_dict(),
+ })
+ return due, head
+
+ def assert_body(self, bucket: str, key: str, expected: bytes, **kwargs: Any) -> None:
+ assert self.clock is not None
+ started = time.time()
+ response = self.client.get_object(Bucket=bucket, Key=key, **kwargs)
+ body = response["Body"].read()
+ self.clock.validate_response(response, started, time.time())
+ if body != expected:
+ raise AssertionError(f"object content changed: {bucket}/{key}")
+
+ def object_absent(self, bucket: str, key: str) -> bool:
+ try:
+ self.client.head_object(Bucket=bucket, Key=key)
+ except ClientError as exc:
+ if self.client_error_code(exc) in {"NoSuchKey", "404", "NotFound"}:
+ return True
+ raise
+ return False
+
+ def confirm_configuration(
+ self, case_id: str, bucket: str, expected: dict[str, Any] | None
+ ) -> None:
+ def matches() -> tuple[bool, Any]:
+ views = {}
+ for endpoint, client in self.node_clients.items():
+ try:
+ response = client.get_bucket_lifecycle_configuration(Bucket=bucket)
+ views[endpoint] = response.get("Rules", [])
+ except ClientError as exc:
+ if self.client_error_code(exc) not in {"NoSuchLifecycleConfiguration", "NoSuchLifecycle"}:
+ raise
+ views[endpoint] = None
+ expected_rules = None if expected is None else expected["Rules"]
+ return all(view == expected_rules for view in views.values()), views
+
+ final = self.wait_until(case_id, f"lifecycle configuration for {bucket} on every source node", matches,
+ timeout=self.args.setup_budget)
+ phase = "absent" if expected is None else "present"
+ self.save_json(case_id, f"configuration-node-views-{bucket}-{phase}.json",
+ {"bucket": bucket, "expected": expected, "views": final})
+
+ def observe_protected_object(
+ self, case_id: str, bucket: str, key: str, body: bytes,
+ due: DueWindow, control_bucket: str, control_key: str,
+ ) -> list[str]:
+ assert self.clock is not None
+ progress = CycleProgress()
+ observation_started: float | None = None
+
+ def preserved() -> tuple[bool, Any]:
+ nonlocal observation_started
+ self.assert_body(bucket, key, body)
+ due_passed = self.clock.passed(due)
+ if due_passed and observation_started is None:
+ observation_started = time.monotonic()
+ cycles = 0
+ control_expired = False
+ if observation_started is not None:
+ cluster = self.rc_scanner_status(case_id, "negative-scanner-latest.json")
+ cycles = progress.observe(cluster)
+ control_expired = self.object_absent(control_bucket, control_key)
+ observed_for = 0 if observation_started is None else time.monotonic() - observation_started
+ detail = {"pastDue": due_passed, "newSuccessfulCycles": cycles,
+ "positiveControlExpired": control_expired, "observedSeconds": observed_for}
+ complete = (due_passed and cycles >= 2 and control_expired
+ and observed_for >= self.args.negative_window)
+ if complete:
+ self.assert_body(bucket, key, body)
+ return complete, detail
+
+ final = self.wait_until(case_id, "protected object with active expiry control", preserved,
+ timeout=self.clock.timeout_after(due, self.args.timeout))
+ return [self.save_json(case_id, "negative-observation-final.json", final)]
+
+ def wait_until(
+ self,
+ case_id: str,
+ description: str,
+ predicate: Callable[[], tuple[bool, Any]],
+ timeout: float | None = None,
+ ) -> Any:
+ budget = self.args.timeout if timeout is None else timeout
+ deadline = time.monotonic() + budget
+ attempts = 0
+ last_detail: Any = None
+ while time.monotonic() < deadline:
+ attempts += 1
+ with time_budget(deadline - time.monotonic(), description):
+ complete, last_detail = predicate()
+ self.events.record(
+ "poll.observation", case=case_id, description=description,
+ attempts=attempts, complete=complete, detail=last_detail,
+ )
+ if time.monotonic() >= deadline:
+ break
+ if complete:
+ self.events.record(
+ "poll.complete",
+ case=case_id,
+ description=description,
+ attempts=attempts,
+ detail=last_detail,
+ )
+ return last_detail
+ time.sleep(min(self.args.poll_interval, max(0, deadline - time.monotonic())))
+ self.events.record(
+ "poll.timeout",
+ case=case_id,
+ description=description,
+ attempts=attempts,
+ detail=last_detail,
+ )
+ self.save_json(
+ case_id,
+ f"poll-timeout-{safe_name(description)}.json",
+ {
+ "description": description,
+ "attempts": attempts,
+ "lastDetail": last_detail,
+ },
+ )
+ raise AssertionError(
+ f"timed out waiting for {description}; last observation: {last_detail}"
+ )
+
+ def cleanup_bucket(self, bucket: str) -> None:
+ self.events.record("bucket.cleanup.start", bucket=bucket)
+ try:
+ self.client.delete_bucket_lifecycle(Bucket=bucket)
+ except ClientError as exc:
+ if exc.response.get("Error", {}).get("Code") not in {
+ "NoSuchBucket",
+ "NoSuchLifecycleConfiguration",
+ }:
+ raise
+
+ try:
+ uploads = self.client.list_multipart_uploads(Bucket=bucket).get(
+ "Uploads", []
+ )
+ for upload in uploads:
+ self.client.abort_multipart_upload(
+ Bucket=bucket,
+ Key=upload["Key"],
+ UploadId=upload["UploadId"],
+ )
+ except ClientError as exc:
+ if exc.response.get("Error", {}).get("Code") != "NoSuchBucket":
+ raise
+
+ deadline = time.monotonic() + max(30.0, min(self.args.timeout, 60.0))
+ attempts = 0
+ while True:
+ attempts += 1
+ version_items: list[dict[str, str]] = []
+ object_items: list[dict[str, str]] = []
+ try:
+ version_response = self.client.list_object_versions(Bucket=bucket)
+ version_items = [
+ {"Key": item["Key"], "VersionId": item["VersionId"]}
+ for group in ("Versions", "DeleteMarkers")
+ for item in version_response.get(group, [])
+ ]
+ if version_items:
+ self.client.delete_objects(
+ Bucket=bucket,
+ Delete={"Objects": version_items, "Quiet": True},
+ )
+
+ object_response = self.client.list_objects_v2(Bucket=bucket)
+ object_items = [
+ {"Key": item["Key"]}
+ for item in object_response.get("Contents", [])
+ ]
+ if object_items:
+ self.client.delete_objects(
+ Bucket=bucket,
+ Delete={"Objects": object_items, "Quiet": True},
+ )
+ self.client.delete_bucket(Bucket=bucket)
+ break
+ except ClientError as exc:
+ code = exc.response.get("Error", {}).get("Code")
+ if code == "NoSuchBucket":
+ break
+ if code != "BucketNotEmpty" or time.monotonic() >= deadline:
+ raise
+ self.events.record(
+ "bucket.cleanup.retry",
+ bucket=bucket,
+ attempt=attempts,
+ errorCode=code,
+ visibleVersions=len(version_items),
+ visibleObjects=len(object_items),
+ )
+ time.sleep(self.args.poll_interval)
+ with self.state_lock:
+ self.created_buckets.discard(bucket)
+ self.events.record(
+ "bucket.cleanup.complete",
+ bucket=bucket,
+ attempts=attempts,
+ )
+
+ def case_scanner_status_readiness(
+ self, case_id: str, bucket: str
+ ) -> list[str]:
+ del bucket
+ progress = CycleProgress()
+
+ def scanner_ready() -> tuple[bool, Any]:
+ cluster = self.rc_scanner_status(
+ case_id, "scanner-readiness-latest.json"
+ )
+ details: list[dict[str, Any]] = []
+ configured = True
+ leaders: list[dict[str, Any]] = []
+ for node in cluster["nodes"]:
+ data = node["status"].get("data", {})
+ metrics = data.get("metrics", {})
+ runtime = data.get("runtime_config", {})
+ cycle = runtime.get("cycle_interval_seconds", {})
+ start_delay = runtime.get("start_delay_seconds", {})
+ schedule = data.get("cycle_schedule", {})
+ node_detail = {
+ "endpoint": node["endpoint"],
+ "health": data.get("health"),
+ "enabled": data.get("enabled"),
+ "freshness": data.get("freshness", {}).get("state"),
+ "leaderLock": metrics.get("leader_lock_state"),
+ "leaderHeldHere": metrics.get(
+ "leader_lock_held_by_this_process"
+ ),
+ "lastCycleResult": metrics.get("last_cycle_result"),
+ "lastCyclePartialReason": metrics.get(
+ "last_cycle_partial_reason"
+ ),
+ "cycleInterval": cycle,
+ "startDelay": start_delay,
+ "executionRole": schedule.get("execution_role"),
+ "effectiveIntervalSeconds": schedule.get(
+ "effective_interval_seconds"
+ ),
+ }
+ details.append(node_detail)
+ configured = configured and (
+ data.get("enabled") is True
+ and cycle.get("value") == self.args.scanner_cycle_seconds
+ and cycle.get("source") == "env"
+ and start_delay.get("value") == 0
+ and start_delay.get("source") == "env"
+ )
+ if metrics.get("leader_lock_held_by_this_process") is True:
+ leaders.append(node_detail)
+ healthy_leader = (
+ len(leaders) == 1
+ and leaders[0]["health"] == "healthy"
+ and leaders[0]["freshness"] == "fresh"
+ and leaders[0]["leaderLock"] == "acquired"
+ and leaders[0]["executionRole"] == "leader"
+ and leaders[0]["lastCycleResult"] == "success"
+ )
+ cycles = progress.observe(cluster)
+ detail = {"leaderCount": len(leaders), "nodes": details, "newSuccessfulCycles": cycles}
+ return configured and healthy_leader and cycles >= 1, detail
+
+ final = self.wait_until(
+ case_id,
+ "a successful scanner cycle with the configured runtime",
+ scanner_ready,
+ )
+ return [self.save_json(case_id, "scanner-readiness-final.json", final)]
+
+ def case_configuration_roundtrip(self, case_id: str, bucket: str) -> list[str]:
+ configuration = {
+ "Rules": [
+ {
+ "ID": "roundtrip-expire",
+ "Status": "Enabled",
+ "Filter": {"Prefix": "expire/"},
+ "Expiration": {"Days": 1},
+ },
+ {
+ "ID": "roundtrip-disabled",
+ "Status": "Disabled",
+ "Filter": {"Prefix": "disabled/"},
+ "Expiration": {"Days": 5},
+ },
+ ]
+ }
+ evidence = self.put_lifecycle(case_id, bucket, configuration)
+ response = self.get_lifecycle(bucket)
+ evidence.append(self.save_json(case_id, "lifecycle-get-response.json", response))
+ observed = {
+ (rule.get("ID"), rule.get("Status"), rule.get("Expiration", {}).get("Days"))
+ for rule in response.get("Rules", [])
+ }
+ expected = {
+ ("roundtrip-expire", "Enabled", 1),
+ ("roundtrip-disabled", "Disabled", 5),
+ }
+ if not expected.issubset(observed):
+ raise AssertionError(
+ f"lifecycle API round-trip mismatch: expected {expected}, got {observed}"
+ )
+ rc_output = self.rc_lifecycle_list(case_id, bucket)
+ for rule_id in ("roundtrip-expire", "roundtrip-disabled"):
+ if rule_id not in rc_output:
+ raise AssertionError(f"rc output does not contain rule {rule_id!r}")
+ evidence.append(
+ str(
+ self.evidence_path(case_id, "rc-lifecycle-list.json").relative_to(
+ self.evidence_dir
+ )
+ )
+ )
+ self.client.delete_bucket_lifecycle(Bucket=bucket)
+ try:
+ self.get_lifecycle(bucket)
+ except ClientError as exc:
+ code = exc.response.get("Error", {}).get("Code")
+ if code not in {"NoSuchLifecycleConfiguration", "NoSuchLifecycle"}:
+ raise
+ else:
+ raise AssertionError("deleted lifecycle configuration is still readable")
+ return evidence
+
+ def case_invalid_configuration(self, case_id: str, bucket: str) -> list[str]:
+ fixtures = {
+ "negative-days": """
+
+ invalid-daysEnabled
+ -1
+
+""",
+ "invalid-status": """
+
+ invalid-statusSometimes
+ 1
+
+""",
+ }
+ evidence: list[str] = []
+ observations: dict[str, Any] = {}
+ for name, lifecycle_xml in fixtures.items():
+ status, headers, body = self.raw_s3_request(
+ "PUT",
+ bucket,
+ query="lifecycle",
+ body=lifecycle_xml.encode("utf-8"),
+ content_type="application/xml",
+ allow_error=True,
+ )
+ observations[name] = {
+ "httpStatus": status,
+ "headers": headers,
+ "body": body.decode("utf-8", errors="replace"),
+ }
+ if status < 400 or status >= 500:
+ raise AssertionError(
+ f"invalid lifecycle {name} returned HTTP {status}, expected a 4xx rejection"
+ )
+ try:
+ self.get_lifecycle(bucket)
+ except ClientError as exc:
+ code = exc.response.get("Error", {}).get("Code", "")
+ if code not in {"NoSuchLifecycleConfiguration", "NoSuchLifecycle"}:
+ raise
+ observations["storedConfiguration"] = {"error": code}
+ else:
+ raise AssertionError("an invalid lifecycle configuration was partially persisted")
+ evidence.append(self.save_json(case_id, "invalid-configurations.json", observations))
+ return evidence
+
+ def case_current_expiration(self, case_id: str, bucket: str) -> list[str]:
+ assert self.clock is not None
+ days = self.prepared_days()
+ expiring = {
+ "expire/one.txt": b"current-expiration-one",
+ "expire/two.txt": b"current-expiration-two",
+ }
+ control_key = "keep/control.txt"
+ control_body = b"control-object-must-survive"
+ due_by_key: dict[str, DueWindow] = {}
+ for key, body in expiring.items():
+ self.client.put_object(Bucket=bucket, Key=key, Body=body)
+ due_by_key[key], _ = self.object_window(case_id, bucket, key, days)
+ self.client.put_object(Bucket=bucket, Key=control_key, Body=control_body)
+ configuration = {
+ "Rules": [
+ {
+ "ID": "expire-current",
+ "Status": "Enabled",
+ "Filter": {"Prefix": "expire/"},
+ "Expiration": {"Days": days},
+ }
+ ]
+ }
+ evidence = self.put_lifecycle(case_id, bucket, configuration)
+ self.confirm_configuration(case_id, bucket, configuration)
+ due = DueWindow(min(item.earliest for item in due_by_key.values()),
+ max(item.latest for item in due_by_key.values()))
+ self.clock.require_before(due, margin=self.args.request_timeout)
+ header_due, head = self.object_window(case_id, bucket, "expire/one.txt", days)
+ evidence.append(self.save_json(case_id, "head-before-expiry.json", head))
+ self.clock.require_before(due, margin=self.args.poll_interval)
+ expiration = str(head.get("Expiration", ""))
+ date_match = re.search(r'expiry-date="([^"]+)"', expiration)
+ id_match = re.search(r'rule-id="([^"]+)"', expiration)
+ if not date_match or not id_match or unquote(id_match.group(1)) != "expire-current":
+ raise AssertionError(f"missing or incorrect expiration header: {expiration}")
+ expires = parse_expiry_date(date_match.group(1))
+ if not header_due.earliest <= expires <= header_due.latest:
+ raise AssertionError("expiration header disagrees with independent Days calculation")
+
+ def expired() -> tuple[bool, Any]:
+ response = self.client.list_objects_v2(Bucket=bucket, Prefix="expire/")
+ keys = [item["Key"] for item in response.get("Contents", [])]
+ for key, object_due in due_by_key.items():
+ if key not in keys and self.clock.now().latest < object_due.earliest:
+ raise AssertionError(f"object expired before its Days deadline: {key}")
+ return not keys, {"remaining": keys}
+
+ final = self.wait_until(case_id, "current objects to expire", expired,
+ timeout=self.clock.timeout_after(due, self.args.timeout))
+ evidence.append(self.save_json(case_id, "expiration-final.json", final))
+ body = self.client.get_object(Bucket=bucket, Key=control_key)["Body"].read()
+ if body != control_body:
+ raise AssertionError("control object changed while expiring filtered objects")
+ return evidence
+
+ def case_tag_filter_expiration(self, case_id: str, bucket: str) -> list[str]:
+ matching_key = "tag-filter/matching.txt"
+ control_key = "tag-filter/control.txt"
+ matching_body = b"tag-filter-matching"
+ control_body = b"tag-filter-control"
+ self.client.put_object(
+ Bucket=bucket,
+ Key=matching_key,
+ Body=matching_body,
+ Tagging="retention=short",
+ )
+ self.client.put_object(
+ Bucket=bucket,
+ Key=control_key,
+ Body=control_body,
+ Tagging="retention=long",
+ )
+ configuration = {
+ "Rules": [
+ {
+ "ID": "expire-by-tag",
+ "Status": "Enabled",
+ "Filter": {"Tag": {"Key": "retention", "Value": "short"}},
+ "Expiration": {"Days": 1},
+ }
+ ]
+ }
+ evidence = self.put_lifecycle(case_id, bucket, configuration)
+
+ def expired() -> tuple[bool, Any]:
+ response = self.client.list_objects_v2(Bucket=bucket, Prefix="tag-filter/")
+ keys = [item["Key"] for item in response.get("Contents", [])]
+ return matching_key not in keys and control_key in keys, {"keys": keys}
+
+ final = self.wait_until(case_id, "tag-filtered object to expire", expired)
+ evidence.append(self.save_json(case_id, "tag-filter-final.json", final))
+ body = self.client.get_object(Bucket=bucket, Key=control_key)["Body"].read()
+ if body != control_body:
+ raise AssertionError("tag-filter control object changed")
+ return evidence
+
+ def case_disabled_rule(self, case_id: str, bucket: str) -> list[str]:
+ assert self.clock is not None
+ days = self.prepared_days()
+ key = "disabled/object.txt"
+ body = b"disabled-rule-must-not-run"
+ self.client.put_object(Bucket=bucket, Key=key, Body=body)
+ due, _ = self.object_window(case_id, bucket, key, days)
+ control_key = "enabled/control.txt"
+ self.client.put_object(Bucket=bucket, Key=control_key, Body=b"enabled-control")
+ control_due, _ = self.object_window(case_id, bucket, control_key, days)
+ configuration = {
+ "Rules": [
+ {
+ "ID": "disabled-expiration",
+ "Status": "Disabled",
+ "Filter": {"Prefix": "disabled/"},
+ "Expiration": {"Days": days},
+ },
+ {"ID": "enabled-positive-control", "Status": "Enabled",
+ "Filter": {"Prefix": "enabled/"}, "Expiration": {"Days": days}},
+ ]
+ }
+ evidence = self.put_lifecycle(case_id, bucket, configuration)
+ self.confirm_configuration(case_id, bucket, configuration)
+ self.clock.require_before(due, margin=self.args.request_timeout)
+ self.assert_body(bucket, key, body)
+ self.clock.require_before(due, margin=self.args.poll_interval)
+ window = DueWindow(due.earliest, max(due.latest, control_due.latest))
+ evidence.extend(self.observe_protected_object(case_id, bucket, key, body, window, bucket, control_key))
+ return evidence
+
+ def case_rule_removal(self, case_id: str, bucket: str) -> list[str]:
+ assert self.clock is not None
+ days = self.prepared_days()
+ key = "removed-rule/object.txt"
+ body = b"removed-rule-must-not-run"
+ self.client.put_object(Bucket=bucket, Key=key, Body=body)
+ due, _ = self.object_window(case_id, bucket, key, days)
+ control_bucket = self.bucket_name(f"{case_id}.control")
+ self.create_bucket(control_bucket)
+ self.client.put_object(Bucket=control_bucket, Key=key, Body=b"enabled-control")
+ control_due, _ = self.object_window(case_id, control_bucket, key, days)
+ configuration = {
+ "Rules": [
+ {
+ "ID": "remove-before-due",
+ "Status": "Enabled",
+ "Filter": {"Prefix": "removed-rule/"},
+ "Expiration": {"Days": days},
+ }
+ ]
+ }
+ evidence = self.put_lifecycle(case_id, bucket, configuration)
+ self.confirm_configuration(case_id, bucket, configuration)
+ self.client.put_bucket_lifecycle_configuration(Bucket=control_bucket, LifecycleConfiguration=configuration)
+ self.confirm_configuration(case_id, control_bucket, configuration)
+ self.clock.require_before(due, margin=self.args.poll_interval)
+ deleted = self.client.delete_bucket_lifecycle(Bucket=bucket)
+ evidence.append(self.save_json(case_id, "lifecycle-delete-response.json", deleted))
+ self.confirm_configuration(case_id, bucket, None)
+ self.clock.require_before(due, margin=self.args.request_timeout)
+ self.assert_body(bucket, key, body)
+ self.clock.require_before(due, margin=self.args.poll_interval)
+ window = DueWindow(due.earliest, max(due.latest, control_due.latest))
+ evidence.extend(self.observe_protected_object(case_id, bucket, key, body, window, control_bucket, key))
+ return evidence
+
+ def case_versioned_current_expiration(self, case_id: str, bucket: str) -> list[str]:
+ self.enable_versioning(bucket)
+ key = "versioned-current/object.txt"
+ put = self.client.put_object(Bucket=bucket, Key=key, Body=b"versioned-current")
+ configuration = {
+ "Rules": [
+ {
+ "ID": "expire-versioned-current",
+ "Status": "Enabled",
+ "Filter": {"Prefix": "versioned-current/"},
+ "Expiration": {"Days": 1},
+ }
+ ]
+ }
+ evidence = self.put_lifecycle(case_id, bucket, configuration)
+
+ def marker_created() -> tuple[bool, Any]:
+ response = self.client.list_object_versions(Bucket=bucket, Prefix=key)
+ markers = [
+ item
+ for item in response.get("DeleteMarkers", [])
+ if item["Key"] == key and item["IsLatest"]
+ ]
+ versions = [
+ item for item in response.get("Versions", []) if item["Key"] == key
+ ]
+ return bool(markers) and any(
+ item["VersionId"] == put.get("VersionId") for item in versions
+ ), {"deleteMarkers": markers, "versions": versions}
+
+ final = self.wait_until(
+ case_id,
+ "versioned current expiration delete marker",
+ marker_created,
+ timeout=max(self.args.timeout, self.args.debug_day_seconds * 12),
+ )
+ try:
+ self.client.get_object(Bucket=bucket, Key=key)
+ except ClientError as exc:
+ if exc.response.get("Error", {}).get("Code") not in {"NoSuchKey", "404", "NotFound"}:
+ raise
+ else:
+ raise AssertionError("versioned current object remains visible after expiration")
+ original = self.client.get_object(
+ Bucket=bucket, Key=key, VersionId=put["VersionId"]
+ )["Body"].read()
+ if original != b"versioned-current":
+ raise AssertionError("expiration changed the retained original version")
+ evidence.append(self.save_json(case_id, "versioned-current-final.json", final))
+ return evidence
+
+ def enable_versioning(self, bucket: str) -> None:
+ self.client.put_bucket_versioning(
+ Bucket=bucket,
+ VersioningConfiguration={"Status": "Enabled"},
+ )
+ observed = self.client.get_bucket_versioning(Bucket=bucket).get("Status")
+ if observed != "Enabled":
+ raise AssertionError(f"versioning did not become enabled: {observed!r}")
+
+ def case_noncurrent_expiration(self, case_id: str, bucket: str) -> list[str]:
+ self.enable_versioning(bucket)
+ key = "versioned/object.txt"
+ v1 = self.client.put_object(Bucket=bucket, Key=key, Body=b"version-one")[
+ "VersionId"
+ ]
+ v2 = self.client.put_object(Bucket=bucket, Key=key, Body=b"version-two")[
+ "VersionId"
+ ]
+ configuration = {
+ "Rules": [
+ {
+ "ID": "expire-noncurrent",
+ "Status": "Enabled",
+ "Filter": {"Prefix": "versioned/"},
+ "NoncurrentVersionExpiration": {"NoncurrentDays": 1},
+ }
+ ]
+ }
+ evidence = self.put_lifecycle(case_id, bucket, configuration)
+ evidence.append(
+ self.save_json(
+ case_id,
+ "versions-before.json",
+ self.client.list_object_versions(Bucket=bucket, Prefix=key),
+ )
+ )
+
+ def old_version_removed() -> tuple[bool, Any]:
+ response = self.client.list_object_versions(Bucket=bucket, Prefix=key)
+ versions = [
+ {
+ "versionId": item["VersionId"],
+ "isLatest": item["IsLatest"],
+ }
+ for item in response.get("Versions", [])
+ if item["Key"] == key
+ ]
+ ids = {item["versionId"] for item in versions}
+ return v1 not in ids and v2 in ids, {"versions": versions}
+
+ final = self.wait_until(
+ case_id, "noncurrent version to expire", old_version_removed
+ )
+ evidence.append(self.save_json(case_id, "versions-final.json", final))
+ body = self.client.get_object(Bucket=bucket, Key=key)["Body"].read()
+ if body != b"version-two":
+ raise AssertionError("latest version changed during noncurrent expiration")
+ return evidence
+
+ def case_newer_noncurrent_retention(
+ self, case_id: str, bucket: str
+ ) -> list[str]:
+ self.enable_versioning(bucket)
+ key = "newer-noncurrent/object.txt"
+ version_ids = [
+ self.client.put_object(
+ Bucket=bucket,
+ Key=key,
+ Body=f"version-{number}".encode("ascii"),
+ )["VersionId"]
+ for number in range(1, 5)
+ ]
+ configuration = {
+ "Rules": [
+ {
+ "ID": "retain-two-newer-noncurrent",
+ "Status": "Enabled",
+ "Filter": {"Prefix": "newer-noncurrent/"},
+ "NoncurrentVersionExpiration": {
+ "NoncurrentDays": 1,
+ "NewerNoncurrentVersions": 2,
+ },
+ }
+ ]
+ }
+ evidence = self.put_lifecycle(case_id, bucket, configuration)
+ evidence.append(
+ self.save_json(
+ case_id,
+ "newer-noncurrent-before.json",
+ self.client.list_object_versions(Bucket=bucket, Prefix=key),
+ )
+ )
+ expected = set(version_ids[-3:])
+
+ def retained() -> tuple[bool, Any]:
+ response = self.client.list_object_versions(Bucket=bucket, Prefix=key)
+ observed = {
+ item["VersionId"]
+ for item in response.get("Versions", [])
+ if item["Key"] == key
+ }
+ return observed == expected, {
+ "expected": sorted(expected),
+ "observed": sorted(observed),
+ "expired": version_ids[0],
+ }
+
+ final = self.wait_until(
+ case_id,
+ "NewerNoncurrentVersions retention",
+ retained,
+ timeout=max(self.args.timeout, self.args.debug_day_seconds * 12),
+ )
+ latest = self.client.get_object(Bucket=bucket, Key=key)["Body"].read()
+ if latest != b"version-4":
+ raise AssertionError("latest object changed while retaining noncurrent versions")
+ evidence.append(self.save_json(case_id, "newer-noncurrent-final.json", final))
+ return evidence
+
+ def case_delete_all_versions(self, case_id: str, bucket: str) -> list[str]:
+ self.enable_versioning(bucket)
+ key = "all-versions/object.txt"
+ version_ids = [
+ self.client.put_object(
+ Bucket=bucket,
+ Key=key,
+ Body=f"version-{number}".encode("ascii"),
+ )["VersionId"]
+ for number in range(1, 4)
+ ]
+ lifecycle_xml = """
+
+
+ delete-all-versions
+ Enabled
+ all-versions/
+
+ 1
+ true
+
+
+
+"""
+ evidence = [
+ self.save_text(case_id, "lifecycle-request.xml", lifecycle_xml)
+ ]
+ status, headers, _ = self.raw_s3_request(
+ "PUT",
+ bucket,
+ query="lifecycle",
+ body=lifecycle_xml.encode("utf-8"),
+ content_type="application/xml",
+ )
+ evidence.append(
+ self.save_json(
+ case_id,
+ "lifecycle-put-response.json",
+ {"httpStatus": status, "headers": headers},
+ )
+ )
+ _, _, stored_xml = self.raw_s3_request(
+ "GET", bucket, query="lifecycle"
+ )
+ stored_text = stored_xml.decode("utf-8")
+ evidence.append(
+ self.save_text(case_id, "lifecycle-get-response.xml", stored_text)
+ )
+ if "true" not in stored_text:
+ raise AssertionError(
+ "stored lifecycle configuration lost ExpiredObjectAllVersions"
+ )
+ evidence.append(
+ self.save_json(
+ case_id,
+ "versions-before.json",
+ self.client.list_object_versions(Bucket=bucket, Prefix=key),
+ )
+ )
+
+ def all_versions_removed() -> tuple[bool, Any]:
+ response = self.client.list_object_versions(Bucket=bucket, Prefix=key)
+ versions = [
+ item["VersionId"]
+ for item in response.get("Versions", [])
+ if item["Key"] == key
+ ]
+ markers = [
+ item["VersionId"]
+ for item in response.get("DeleteMarkers", [])
+ if item["Key"] == key
+ ]
+ return not versions and not markers, {
+ "expectedVersionIds": version_ids,
+ "versions": versions,
+ "deleteMarkers": markers,
+ }
+
+ final = self.wait_until(
+ case_id,
+ "all versions to expire",
+ all_versions_removed,
+ timeout=max(self.args.timeout, self.args.debug_day_seconds * 12),
+ )
+ evidence.append(self.save_json(case_id, "versions-final.json", final))
+ return evidence
+
+ def case_delete_marker_expiration(self, case_id: str, bucket: str) -> list[str]:
+ self.enable_versioning(bucket)
+ key = "markers/object.txt"
+ configuration = {
+ "Rules": [
+ {
+ "ID": "expire-delete-marker",
+ "Status": "Enabled",
+ "Filter": {"Prefix": "markers/"},
+ "Expiration": {"ExpiredObjectDeleteMarker": True},
+ "NoncurrentVersionExpiration": {"NoncurrentDays": 1},
+ }
+ ]
+ }
+ evidence = self.put_lifecycle(case_id, bucket, configuration)
+ self.client.put_object(Bucket=bucket, Key=key, Body=b"marker-source")
+ self.client.delete_object(Bucket=bucket, Key=key)
+ evidence.append(
+ self.save_json(
+ case_id,
+ "markers-before.json",
+ self.client.list_object_versions(Bucket=bucket, Prefix=key),
+ )
+ )
+
+ def marker_chain_removed() -> tuple[bool, Any]:
+ response = self.client.list_object_versions(Bucket=bucket, Prefix=key)
+ versions = [
+ item["VersionId"]
+ for item in response.get("Versions", [])
+ if item["Key"] == key
+ ]
+ markers = [
+ item["VersionId"]
+ for item in response.get("DeleteMarkers", [])
+ if item["Key"] == key
+ ]
+ return not versions and not markers, {
+ "versions": versions,
+ "deleteMarkers": markers,
+ }
+
+ final = self.wait_until(
+ case_id,
+ "noncurrent object and expired delete marker to be removed",
+ marker_chain_removed,
+ timeout=max(self.args.timeout, self.args.debug_day_seconds * 12),
+ )
+ evidence.append(self.save_json(case_id, "markers-final.json", final))
+ return evidence
+
+ def case_multipart_expiration(self, case_id: str, bucket: str) -> list[str]:
+ key = "multipart/incomplete.bin"
+ configuration = {
+ "Rules": [
+ {
+ "ID": "abort-incomplete-multipart",
+ "Status": "Enabled",
+ "Filter": {"Prefix": "multipart/"},
+ "AbortIncompleteMultipartUpload": {"DaysAfterInitiation": 1},
+ }
+ ]
+ }
+ evidence = self.put_lifecycle(case_id, bucket, configuration)
+ created = self.client.create_multipart_upload(Bucket=bucket, Key=key)
+ upload_id = created["UploadId"]
+ self.client.upload_part(
+ Bucket=bucket,
+ Key=key,
+ UploadId=upload_id,
+ PartNumber=1,
+ Body=b"x" * (5 * 1024 * 1024),
+ )
+ evidence.append(
+ self.save_json(
+ case_id,
+ "multipart-before.json",
+ self.client.list_multipart_uploads(Bucket=bucket, Prefix="multipart/"),
+ )
+ )
+
+ def upload_removed() -> tuple[bool, Any]:
+ response = self.client.list_multipart_uploads(
+ Bucket=bucket, Prefix="multipart/"
+ )
+ uploads = [
+ {"key": item["Key"], "uploadId": item["UploadId"]}
+ for item in response.get("Uploads", [])
+ ]
+ return not any(item["uploadId"] == upload_id for item in uploads), {
+ "uploads": uploads
+ }
+
+ final = self.wait_until(
+ case_id,
+ "incomplete multipart upload to be aborted",
+ upload_removed,
+ timeout=max(self.args.timeout, self.args.debug_day_seconds * 12),
+ )
+ evidence.append(self.save_json(case_id, "multipart-final.json", final))
+ return evidence
+
+ def case_transition_and_restore(self, case_id: str, bucket: str) -> list[str]:
+ if self.tier_client is None:
+ raise RuntimeError(
+ "transition-and-restore requires tier endpoint and credentials"
+ )
+ digest = hashlib.sha256(
+ f"{self.args.run_id}:{case_id}".encode("utf-8")
+ ).hexdigest()[:12]
+ tier_name = f"RV{digest.upper()}"
+ tier_bucket = f"rv-tier-{digest}"
+ key = "transition/archive.bin"
+ body = (b"rustfs-transition-restore-" * 4096) + secrets.token_bytes(32)
+ tier_created = False
+ tier_added = False
+ evidence: list[str] = []
+ phase = "remote-tier-bucket-create"
+ primary_error: Exception | None = None
+ primary_phase = ""
+ cleanup_error: Exception | None = None
+ cleanup_phase = ""
+ try:
+ self.tier_client.create_bucket(Bucket=tier_bucket)
+ tier_created = True
+ phase = "tier-add"
+ add = self.rc_command(
+ case_id,
+ "tier-add",
+ [
+ "ilm",
+ "tier",
+ "add",
+ "rustfs",
+ tier_name,
+ self.rc_alias,
+ "--endpoint",
+ self.args.tier_endpoint,
+ "--access-key",
+ self.args.tier_access_key,
+ "--secret-key",
+ self.args.tier_secret_key,
+ "--bucket",
+ tier_bucket,
+ ],
+ secrets_to_redact=(self.args.tier_secret_key,),
+ )
+ tier_added = add.returncode == 0
+ evidence.append(
+ str(
+ self.evidence_path(case_id, "tier-add.json").relative_to(
+ self.evidence_dir
+ )
+ )
+ )
+ phase = "source-put"
+ put = self.client.put_object(Bucket=bucket, Key=key, Body=body)
+ configuration = {
+ "Rules": [
+ {
+ "ID": "transition-to-real-tier",
+ "Status": "Enabled",
+ "Filter": {"Prefix": "transition/"},
+ "Transitions": [{"Days": 1, "StorageClass": tier_name}],
+ }
+ ]
+ }
+ phase = "lifecycle-put"
+ evidence.extend(self.put_lifecycle(case_id, bucket, configuration))
+ evidence.append(
+ self.save_json(
+ case_id,
+ "transition-source-put.json",
+ {
+ "versionId": put.get("VersionId"),
+ "bytes": len(body),
+ "sha256": hashlib.sha256(body).hexdigest(),
+ },
+ )
+ )
+
+ def transitioned() -> tuple[bool, Any]:
+ head = self.client.head_object(Bucket=bucket, Key=key)
+ observed = head.get("StorageClass") or head.get("storage-class")
+ return str(observed or "").upper() == tier_name, {
+ "storageClass": observed,
+ "restore": head.get("Restore"),
+ "contentLength": head.get("ContentLength"),
+ }
+
+ phase = "transition-wait"
+ final = self.wait_until(
+ case_id,
+ "object transition to remote tier",
+ transitioned,
+ timeout=max(self.args.timeout, self.args.debug_day_seconds * 18),
+ )
+ evidence.append(self.save_json(case_id, "transition-final.json", final))
+ phase = "transparent-read"
+ transparent = self.client.get_object(Bucket=bucket, Key=key)["Body"].read()
+ if transparent != body:
+ raise AssertionError("transparent read changed transitioned object bytes")
+ evidence.append(
+ self.save_json(
+ case_id,
+ "transition-transparent-read.json",
+ {
+ "bytes": len(transparent),
+ "sha256": hashlib.sha256(transparent).hexdigest(),
+ },
+ )
+ )
+ phase = "remote-object-list"
+ remote_objects = self.tier_client.list_objects_v2(Bucket=tier_bucket)
+ if not remote_objects.get("Contents"):
+ raise AssertionError("remote tier bucket contains no transitioned object")
+ evidence.append(
+ self.save_json(case_id, "tier-remote-objects.json", remote_objects)
+ )
+ phase = "restore-request"
+ self.rc_command(
+ case_id,
+ "tier-restore",
+ [
+ "ilm",
+ "restore",
+ f"{self.rc_alias}/{bucket}/{key}",
+ "--days",
+ "1",
+ "--force",
+ ],
+ )
+ evidence.append(
+ str(
+ self.evidence_path(case_id, "tier-restore.json").relative_to(
+ self.evidence_dir
+ )
+ )
+ )
+
+ def restored() -> tuple[bool, Any]:
+ head = self.client.head_object(Bucket=bucket, Key=key)
+ restore = str(head.get("Restore", ""))
+ return bool(restore) and 'ongoing-request="false"' in restore, {
+ "restore": restore,
+ "storageClass": head.get("StorageClass"),
+ }
+
+ phase = "restore-wait"
+ restore_final = self.wait_until(
+ case_id,
+ "transitioned object restore completion",
+ restored,
+ timeout=max(self.args.timeout, self.args.debug_day_seconds * 18),
+ )
+ evidence.append(
+ self.save_json(case_id, "restore-final.json", restore_final)
+ )
+ phase = "restored-read"
+ restored_body = self.client.get_object(Bucket=bucket, Key=key)["Body"].read()
+ if restored_body != body:
+ raise AssertionError("restored object bytes differ from original")
+ except Exception as exc:
+ primary_error = exc
+ primary_phase = phase
+ evidence.append(
+ self.save_phase_error(
+ case_id,
+ "transition-primary-error.json",
+ primary_phase,
+ exc,
+ )
+ )
+ finally:
+ cleanup_phase = "lifecycle-delete"
+ try:
+ try:
+ self.client.delete_bucket_lifecycle(Bucket=bucket)
+ except ClientError:
+ pass
+ if tier_added:
+ # A transitioned object is an authoritative reference to its tier.
+ # Remove the source object and wait for its remote payload cleanup
+ # before asking RustFS to remove the tier configuration. --force
+ # bypasses lifecycle-config references, but deliberately does not
+ # bypass live object references.
+ cleanup_phase = "source-delete"
+ self.delete_object_eventually(
+ case_id,
+ bucket,
+ key,
+ evidence_name="transition-source-delete",
+ timeout=max(
+ self.args.timeout,
+ self.args.debug_day_seconds * 12,
+ ),
+ )
+ evidence.append(
+ str(
+ self.evidence_path(
+ case_id,
+ "transition-source-delete.json",
+ ).relative_to(self.evidence_dir)
+ )
+ )
+
+ def remote_tier_empty() -> tuple[bool, Any]:
+ response = self.tier_client.list_objects_v2(Bucket=tier_bucket)
+ remaining = [
+ {"key": item["Key"], "size": item.get("Size")}
+ for item in response.get("Contents", [])
+ ]
+ return not remaining, {"remaining": remaining}
+
+ cleanup_phase = "remote-object-cleanup-wait"
+ cleanup_final = self.wait_until(
+ case_id,
+ "transitioned object cleanup from remote tier",
+ remote_tier_empty,
+ timeout=max(
+ self.args.timeout,
+ self.args.debug_day_seconds * 12,
+ ),
+ )
+ evidence.append(
+ self.save_json(
+ case_id,
+ "tier-object-cleanup-final.json",
+ cleanup_final,
+ )
+ )
+ cleanup_phase = "tier-remove"
+ self.rc_tier_remove_eventually(
+ case_id,
+ tier_name,
+ timeout=max(
+ self.args.timeout,
+ self.args.debug_day_seconds * 12,
+ ),
+ )
+ evidence.append(
+ str(
+ self.evidence_path(
+ case_id, "tier-remove.json"
+ ).relative_to(self.evidence_dir)
+ )
+ )
+ if tier_created:
+ cleanup_phase = "remote-tier-bucket-delete"
+ try:
+ versions = self.tier_client.list_object_versions(
+ Bucket=tier_bucket
+ )
+ version_items = [
+ {"Key": item["Key"], "VersionId": item["VersionId"]}
+ for group in ("Versions", "DeleteMarkers")
+ for item in versions.get(group, [])
+ ]
+ if version_items:
+ self.tier_client.delete_objects(
+ Bucket=tier_bucket,
+ Delete={"Objects": version_items, "Quiet": True},
+ )
+ objects = self.tier_client.list_objects_v2(
+ Bucket=tier_bucket
+ ).get("Contents", [])
+ if objects:
+ self.tier_client.delete_objects(
+ Bucket=tier_bucket,
+ Delete={
+ "Objects": [
+ {"Key": item["Key"]} for item in objects
+ ],
+ "Quiet": True,
+ },
+ )
+ self.tier_client.delete_bucket(Bucket=tier_bucket)
+ except ClientError:
+ pass
+ except Exception as exc:
+ cleanup_error = exc
+ evidence.append(
+ self.save_phase_error(
+ case_id,
+ "transition-cleanup-error.json",
+ cleanup_phase,
+ exc,
+ )
+ )
+
+ if primary_error is not None:
+ if cleanup_error is not None:
+ raise CaseExecutionAndCleanupError(
+ primary_phase,
+ primary_error,
+ cleanup_phase,
+ cleanup_error,
+ ) from cleanup_error
+ raise primary_error
+ if cleanup_error is not None:
+ raise cleanup_error
+ return evidence
+
+ def case_restart_persistence(self, case_id: str, bucket: str) -> list[str]:
+ assert self.clock is not None
+ if not self.args.restart_hook:
+ raise RuntimeError("restart-persistence requires --restart-hook")
+ days = self.prepared_days(self.args.restart_timeout + self.args.setup_budget)
+ key = "restart/expire.txt"
+ body = b"survive-restart-then-expire"
+ configuration = {"Rules": [{
+ "ID": "restart-expiry", "Status": "Enabled",
+ "Filter": {"Prefix": "restart/"}, "Expiration": {"Days": days},
+ }]}
+ evidence = self.put_lifecycle(case_id, bucket, configuration)
+ self.confirm_configuration(case_id, bucket, configuration)
+ self.client.put_object(Bucket=bucket, Key=key, Body=body)
+ due, before = self.object_window(case_id, bucket, key, days)
+ self.assert_body(bucket, key, body)
+ self.clock.require_before(due, margin=self.args.restart_timeout + self.args.setup_budget)
+ evidence.append(self.save_json(case_id, "object-before-restart.json", before))
+ command = json.loads(self.args.restart_hook)
+ if not isinstance(command, list) or not command or not all(
+ isinstance(item, str) and item for item in command
+ ):
+ raise RuntimeError("--restart-hook must be a JSON array of command tokens")
+ self.events.record("cluster.restart.start", case=case_id)
+ process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
+ text=True, start_new_session=True)
+ output = ""
+ try:
+ with time_budget(self.args.restart_timeout, "cluster restart", TimeoutError):
+ output, _ = process.communicate(timeout=self.args.restart_timeout)
+ if process.returncode != 0:
+ raise RuntimeError(f"cluster restart hook failed with exit {process.returncode}")
+ except BaseException as primary_error:
+ # Stop the local driver/SSH process group; never leave it running into
+ # the next case. Remote service state remains unknown and is preserved.
+ self.blocked_reason = "restart hook failed; service state requires inspection"
+ if isinstance(primary_error, subprocess.TimeoutExpired) and primary_error.output:
+ output = primary_error.output
+ if isinstance(output, bytes):
+ output = output.decode("utf-8", errors="replace")
+ try:
+ try:
+ os.killpg(process.pid, signal.SIGKILL)
+ except ProcessLookupError:
+ pass
+ output, _ = process.communicate(timeout=5)
+ except Exception as cleanup_error:
+ evidence.append(self.save_json(case_id, "restart-hook-termination-error.json", {
+ "errorType": type(cleanup_error).__name__,
+ "message": self.redact(str(cleanup_error)),
+ }))
+ raise
+ finally:
+ evidence.append(self.save_text(case_id, "restart-hook.log", self.redact(output)))
+ self.events.record("cluster.restart.complete", case=case_id)
+ self.calibrate_clock(case_id)
+ self.confirm_configuration(case_id, bucket, configuration)
+ self.clock.require_before(due, margin=self.args.request_timeout)
+ self.assert_body(bucket, key, body)
+ self.clock.require_before(due, margin=self.args.request_timeout)
+ _, after = self.object_window(case_id, bucket, key, days)
+ if (after["LastModified"], after["ETag"]) != (before["LastModified"], before["ETag"]):
+ raise AssertionError("restart changed the object identity")
+ evidence.append(self.save_json(case_id, "object-after-restart-before-expiry.json", after))
+ evidence.extend(self.case_scanner_status_readiness(case_id, bucket))
+
+ def object_expired() -> tuple[bool, Any]:
+ absent = self.object_absent(bucket, key)
+ if absent and self.clock.now().latest < due.earliest:
+ raise AssertionError("object expired before its Days deadline")
+ return absent, {"absent": absent, "due": due.as_dict()}
+
+ final = self.wait_until(case_id, "object expiry after verified restart", object_expired,
+ timeout=self.clock.timeout_after(due, self.args.timeout))
+ evidence.append(self.save_json(case_id, "restart-expiration-final.json", final))
+ return evidence
+
+ def classify_exception(self, exc: BaseException) -> str:
+ if isinstance(exc, CaseExecutionAndCleanupError):
+ states = {
+ self.classify_exception(exc.primary_error),
+ self.classify_exception(exc.cleanup_error),
+ }
+ return "infra_error" if "infra_error" in states else "fail"
+ if isinstance(exc, AssertionError):
+ return "fail"
+ if isinstance(exc, INFRA_EXCEPTIONS):
+ return "infra_error"
+ if isinstance(exc, ClientError):
+ code = exc.response.get("Error", {}).get("Code")
+ return "infra_error" if code in INFRA_ERROR_CODES else "fail"
+ return "infra_error"
+
+ def run_case(
+ self,
+ case_id: str,
+ function: Callable[[str, str], list[str]],
+ *,
+ create_bucket: bool = True,
+ ) -> None:
+ bucket = self.bucket_name(case_id)
+ self.case_buckets = set()
+ started_at = utc_now()
+ started = time.monotonic()
+ state = "pass"
+ message = "all assertions passed"
+ evidence: list[str] = []
+ bucket_label = f" bucket={bucket}" if create_bucket else ""
+ print(f"[RUN ] {case_id}{bucket_label}", flush=True)
+ self.events.record(
+ "case.start",
+ case=case_id,
+ bucket=bucket if create_bucket else None,
+ )
+ try:
+ with time_budget(self.args.case_timeout, f"case {case_id}"):
+ self.rc_scanner_status(case_id, "scanner-before.json")
+ self.calibrate_clock(case_id)
+ if create_bucket:
+ self.create_bucket(bucket)
+ evidence.extend(function(case_id, bucket))
+ except Exception as exc: # retain the failure before inspecting cleanup
+ state = self.classify_exception(exc)
+ message = self.redact(f"{type(exc).__name__}: {exc}")
+ self.events.record(
+ "case.error",
+ case=case_id,
+ state=state,
+ errorType=type(exc).__name__,
+ message=message,
+ )
+ self.save_json(
+ case_id,
+ "case-error.json",
+ {
+ "state": state,
+ "errorType": type(exc).__name__,
+ "message": message,
+ },
+ )
+ finally:
+ try:
+ self.rc_scanner_status(case_id, "scanner-after.json")
+ except Exception as status_exc:
+ if state == "pass":
+ state = "infra_error"
+ message = (
+ "scanner status collection failed: "
+ f"{type(status_exc).__name__}: {status_exc}"
+ )
+ self.events.record(
+ "rc.scanner.status.error",
+ case=case_id,
+ message=self.redact(str(status_exc)),
+ )
+ should_cleanup = state == "pass" or self.args.cleanup_on_failure
+ owned_buckets = self.case_buckets & self.created_buckets
+ if owned_buckets and should_cleanup:
+ try:
+ with time_budget(self.args.cleanup_timeout, f"cleanup {case_id}", TimeoutError):
+ for owned_bucket in sorted(owned_buckets):
+ self.cleanup_bucket(owned_bucket)
+ except Exception as cleanup_exc:
+ cleanup_state = self.classify_exception(cleanup_exc)
+ if state == "pass":
+ state = cleanup_state
+ elif cleanup_state == "infra_error":
+ state = "infra_error"
+ cleanup_message = self.redact(
+ f"{type(cleanup_exc).__name__}: {cleanup_exc}"
+ )
+ message = f"cleanup failed after {message}: {cleanup_message}"
+ self.events.record(
+ "case.cleanup.error",
+ case=case_id,
+ state=cleanup_state,
+ message=cleanup_message,
+ )
+ self.save_json(
+ case_id,
+ "case-cleanup-error.json",
+ {
+ "state": cleanup_state,
+ "errorType": type(cleanup_exc).__name__,
+ "message": cleanup_message,
+ },
+ )
+ elif owned_buckets:
+ message += f"; preserved failed buckets {', '.join(sorted(owned_buckets))}"
+ if self.case_buckets & self.created_buckets:
+ self.blocked_reason = f"{case_id} left preserved or uncleaned test buckets"
+ if state != "pass" and not self.args.cleanup_on_failure and self.blocked_reason is None:
+ self.blocked_reason = f"{case_id} failed; the run is preserved for inspection"
+
+ case_evidence_dir = self.evidence_path(case_id, "evidence-index").parent
+ evidence = sorted(
+ str(path.relative_to(self.evidence_dir))
+ for path in case_evidence_dir.iterdir()
+ if path.is_file() and path.name != "evidence-index"
+ )
+
+ duration = time.monotonic() - started
+ result = CaseResult(
+ id=case_id,
+ state=state,
+ message=message,
+ started_at=started_at,
+ finished_at=utc_now(),
+ duration_seconds=round(duration, 3),
+ evidence=evidence,
+ )
+ with self.state_lock:
+ self.results.append(result)
+ self.events.record("case.complete", **asdict(result))
+ label = "PASS" if state == "pass" else state.upper()
+ print(f"[{label:5}] {case_id}: {message}", flush=True)
+
+ def cleanup_outcome(self) -> dict[str, Any]:
+ """Describe the logical bucket cleanup completed by the ILM harness."""
+
+ uncertain = sorted(getattr(self, "uncertain_buckets", set()))
+ if uncertain:
+ return {
+ "attempted": False,
+ "state": "infra_error",
+ "message": "bucket creation outcomes are unknown; isolation was not verified",
+ "uncertainBuckets": uncertain,
+ }
+ preserved = sorted(self.created_buckets)
+ if not preserved:
+ return {
+ "attempted": bool(self.results),
+ "state": "pass",
+ "message": "no ILM test buckets remain after logical cleanup",
+ }
+ if self.args.cleanup_on_failure:
+ return {
+ "attempted": True,
+ "state": "infra_error",
+ "message": "ILM cleanup was requested but test buckets remain",
+ "preservedBuckets": preserved,
+ }
+ return {
+ "attempted": False,
+ "state": "preserved",
+ "message": "failed-case ILM buckets were preserved for investigation",
+ "preservedBuckets": preserved,
+ }
+
+ def write_results(self, started_at: str, started: float) -> str:
+ selected_cases = ilm_harness_scored_cases(self.args.case)
+ selection_mode = "filtered" if self.args.case is not None else "full"
+ case_order = {case_id: index for index, case_id in enumerate(CASE_IDS)}
+ self.results.sort(key=lambda item: case_order.get(item.id, len(case_order)))
+ states = {result.state for result in self.results}
+ if "fail" in states:
+ overall = "fail"
+ elif "infra_error" in states:
+ overall = "infra_error"
+ else:
+ overall = "pass"
+ finished_at = utc_now()
+ result_document = {
+ "apiVersion": "validation.rustfs.io/v1alpha1",
+ "kind": "TestRunResult",
+ "metadata": {
+ "runId": self.args.run_id,
+ "environmentId": self.args.environment_id,
+ },
+ "status": {
+ "state": overall,
+ "startedAt": started_at,
+ "finishedAt": finished_at,
+ "cases": [
+ {
+ "id": item.id,
+ "state": item.state,
+ "message": item.message,
+ "evidence": item.evidence,
+ }
+ for item in self.results
+ ],
+ "evidence": ["events.jsonl", "run-manifest.json", "junit.xml"],
+ },
+ }
+ (self.evidence_dir / "result.json").write_text(
+ json.dumps(result_document, indent=2, sort_keys=True) + "\n",
+ encoding="utf-8",
+ )
+ manifest = {
+ "runId": self.args.run_id,
+ "environmentId": self.args.environment_id,
+ "endpoint": self.args.endpoint,
+ "scannerEndpoints": self.args.scanner_endpoint,
+ "startedAt": started_at,
+ "finishedAt": finished_at,
+ "durationSeconds": round(time.monotonic() - started, 3),
+ "debugDaySeconds": self.args.debug_day_seconds,
+ "processSeconds": self.args.process_seconds,
+ "timingBudgets": {
+ "requestSeconds": self.args.request_timeout,
+ "caseSeconds": self.args.case_timeout,
+ "cleanupSeconds": self.args.cleanup_timeout,
+ "setupSeconds": self.args.setup_budget,
+ "restartSeconds": self.args.restart_timeout,
+ "negativeWindowSeconds": self.args.negative_window,
+ "maxClockSkewSeconds": self.args.max_clock_skew,
+ },
+ "readinessOutcome": self.readiness,
+ "executionMode": "serial",
+ "scannerPollIntervalSeconds": self.args.poll_interval,
+ "boto3Version": boto3.__version__,
+ "botocoreVersion": botocore.__version__,
+ "rcBinaryPath": self.rc_bin,
+ "rcVersion": self.rc_version,
+ "rcBinarySha256": self.rc_binary_sha256,
+ "expectedRcBinarySha256": self.expected_rc_binary_sha256 or None,
+ "executionPlan": {
+ "selectionMode": selection_mode,
+ "module": "ilm",
+ "scoredCases": list(selected_cases),
+ "scoredCaseCount": len(selected_cases),
+ "fixtures": [],
+ },
+ "fixtureOutcomes": [],
+ "cleanupOnFailure": self.args.cleanup_on_failure,
+ "cleanupOutcome": self.cleanup_outcome(),
+ "preservedBuckets": sorted(self.created_buckets),
+ }
+ (self.evidence_dir / "run-manifest.json").write_text(
+ json.dumps(manifest, indent=2, sort_keys=True) + "\n",
+ encoding="utf-8",
+ )
+ self.write_junit(overall, time.monotonic() - started)
+ return overall
+
+ def write_junit(self, overall: str, duration: float) -> None:
+ failures = sum(item.state == "fail" for item in self.results)
+ errors = sum(item.state == "infra_error" for item in self.results)
+ suite = ET.Element(
+ "testsuite",
+ {
+ "name": "rustfs-fixed-environment-ilm",
+ "tests": str(len(self.results)),
+ "failures": str(failures),
+ "errors": str(errors),
+ "skipped": "0",
+ "time": f"{duration:.3f}",
+ },
+ )
+ for item in self.results:
+ case = ET.SubElement(
+ suite,
+ "testcase",
+ {
+ "classname": "rustfs.ilm",
+ "name": item.id,
+ "time": f"{item.duration_seconds:.3f}",
+ },
+ )
+ if item.state == "fail":
+ ET.SubElement(case, "failure", {"message": item.message}).text = (
+ item.message
+ )
+ elif item.state == "infra_error":
+ ET.SubElement(case, "error", {"message": item.message}).text = (
+ item.message
+ )
+ ET.SubElement(case, "system-out").text = "\n".join(item.evidence)
+ tree = ET.ElementTree(suite)
+ ET.indent(tree, space=" ")
+ tree.write(
+ self.evidence_dir / "junit.xml",
+ encoding="utf-8",
+ xml_declaration=True,
+ )
+ self.events.record("run.complete", state=overall)
+
+ def record_blocked(self, case_id: str, reason: str, *, state: str = "infra_error") -> None:
+ message = f"blocked: {reason}"
+ evidence = self.save_json(case_id, "blocked.json", {"state": state, "message": message})
+ now = utc_now()
+ self.results.append(CaseResult(case_id, state, message, now, now, 0, [evidence]))
+ self.events.record("case.blocked", case=case_id, state=state, message=message)
+
+ def run(self) -> int:
+ started_at = utc_now()
+ started = time.monotonic()
+ selected_cases = ilm_harness_scored_cases(self.args.case)
+ self.events.record(
+ "run.start",
+ runId=self.args.run_id,
+ environmentId=self.args.environment_id,
+ endpoint=self.args.endpoint,
+ selectionMode="filtered" if self.args.case is not None else "full",
+ scoredCases=list(selected_cases),
+ fixtures=[],
+ )
+ try:
+ with time_budget(self.args.timeout, "ILM preflight"):
+ self.wait_for_s3_connectivity()
+ self.configure_rc()
+ self.calibrate_clock("fixture.scanner-readiness")
+ self.case_scanner_status_readiness("fixture.scanner-readiness", "")
+ self.readiness = {"state": "pass", "message": "clock and new scanner cycle verified"}
+ except Exception as exc:
+ message = self.redact(f"precondition_error: {type(exc).__name__}: {exc}")
+ self.readiness = {"state": self.classify_exception(exc), "message": message}
+ for index, case_id in enumerate(selected_cases):
+ self.record_blocked(case_id, message,
+ state=self.classify_exception(exc) if index == 0 else "infra_error")
+ overall = self.write_results(started_at, started)
+ return 2 if overall == "infra_error" else 1
+
+ cases: dict[str, Callable[[str, str], list[str]]] = {
+ "scanner.status-readiness": self.case_scanner_status_readiness,
+ "ilm.configuration-roundtrip": self.case_configuration_roundtrip,
+ "ilm.invalid-configuration": self.case_invalid_configuration,
+ "ilm.current-expiration": self.case_current_expiration,
+ "ilm.tag-filter-expiration": self.case_tag_filter_expiration,
+ "ilm.disabled-rule": self.case_disabled_rule,
+ "ilm.rule-removal": self.case_rule_removal,
+ "ilm.versioned-current-expiration": self.case_versioned_current_expiration,
+ "ilm.noncurrent-expiration": self.case_noncurrent_expiration,
+ "ilm.newer-noncurrent-retention": self.case_newer_noncurrent_retention,
+ "ilm.delete-all-versions": self.case_delete_all_versions,
+ "ilm.delete-marker-expiration": self.case_delete_marker_expiration,
+ "ilm.multipart-expiration": self.case_multipart_expiration,
+ "ilm.transition-and-restore": self.case_transition_and_restore,
+ "ilm.restart-persistence": self.case_restart_persistence,
+ }
+ for case_id in selected_cases:
+ if self.blocked_reason:
+ self.record_blocked(case_id, self.blocked_reason)
+ continue
+ self.run_case(case_id, cases[case_id], create_bucket=case_id != "scanner.status-readiness")
+
+ overall = self.write_results(started_at, started)
+ print(
+ f"ILM validation {overall}: result={self.evidence_dir / 'result.json'}",
+ flush=True,
+ )
+ return {"pass": 0, "fail": 1, "infra_error": 2}[overall]
+
+
+def parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--endpoint", default=os.environ.get("RUSTFS_ENDPOINT"))
+ parser.add_argument(
+ "--scanner-endpoint",
+ action="append",
+ help="node endpoint queried for scanner status; repeat for every node",
+ )
+ parser.add_argument("--access-key", default=os.environ.get("RUSTFS_ACCESS_KEY"))
+ parser.add_argument("--secret-key", default=os.environ.get("RUSTFS_SECRET_KEY"))
+ parser.add_argument("--region", default=os.environ.get("RUSTFS_REGION", "us-east-1"))
+ parser.add_argument("--environment-id", default="ilm-2x4")
+ parser.add_argument("--run-id", required=True)
+ parser.add_argument("--evidence-dir", required=True, type=Path)
+ parser.add_argument("--bucket-prefix", default="rustfs-ilm-validation")
+ parser.add_argument("--debug-day-seconds", type=int, default=10)
+ parser.add_argument("--process-seconds", type=int, default=1)
+ parser.add_argument("--scanner-cycle-seconds", type=int, default=2)
+ parser.add_argument("--timeout", type=float, default=120)
+ parser.add_argument("--poll-interval", type=float, default=2)
+ parser.add_argument("--request-timeout", type=float, default=10)
+ parser.add_argument("--case-timeout", type=float, default=900)
+ parser.add_argument("--cleanup-timeout", type=float, default=120)
+ parser.add_argument("--setup-budget", type=float, default=40)
+ parser.add_argument("--restart-timeout", type=float, default=120)
+ parser.add_argument("--negative-window", type=float, default=20)
+ parser.add_argument("--max-clock-skew", type=float, default=5)
+ parser.add_argument("--rc-bin", default="rc")
+ parser.add_argument(
+ "--tier-endpoint", default=os.environ.get("RUSTFS_ILM_TIER_ENDPOINT")
+ )
+ parser.add_argument(
+ "--tier-access-key", default=os.environ.get("RUSTFS_ILM_TIER_ACCESS_KEY")
+ )
+ parser.add_argument(
+ "--tier-secret-key", default=os.environ.get("RUSTFS_ILM_TIER_SECRET_KEY")
+ )
+ parser.add_argument(
+ "--restart-hook",
+ help="JSON array of command tokens used to restart the fixed cluster",
+ )
+ parser.add_argument(
+ "--case",
+ action="append",
+ choices=CASE_IDS,
+ help="run only the named case; repeat to select multiple cases",
+ )
+ parser.add_argument(
+ "--cleanup-on-failure",
+ action="store_true",
+ help="delete failed-case buckets instead of preserving them for diagnosis",
+ )
+ args = parser.parse_args()
+ if not args.endpoint or not args.access_key or not args.secret_key:
+ parser.error("endpoint, access key, and secret key are required")
+ tier_values = (args.tier_endpoint, args.tier_access_key, args.tier_secret_key)
+ if any(tier_values) and not all(tier_values):
+ parser.error("tier endpoint, access key, and secret key must be provided together")
+ parsed = urlparse(args.endpoint)
+ if parsed.scheme not in {"http", "https"} or not parsed.netloc:
+ parser.error("endpoint must be an absolute HTTP(S) URL")
+ if (
+ args.debug_day_seconds < 1
+ or args.process_seconds < 1
+ or args.scanner_cycle_seconds < 1
+ or args.timeout <= 0
+ or args.poll_interval <= 0
+ ):
+ parser.error("timing values must be positive")
+ for name in ("timeout", "poll_interval", "request_timeout", "case_timeout",
+ "cleanup_timeout", "setup_budget", "restart_timeout", "negative_window", "max_clock_skew"):
+ if not math.isfinite(getattr(args, name)) or getattr(args, name) <= 0:
+ parser.error(f"{name.replace('_', '-')} must be finite and positive")
+ args.scanner_endpoint = list(
+ dict.fromkeys(args.scanner_endpoint or [args.endpoint])
+ )
+ scanner_hosts: list[str] = []
+ for endpoint in args.scanner_endpoint:
+ scanner_parsed = urlparse(endpoint)
+ if (
+ scanner_parsed.scheme not in {"http", "https"}
+ or not scanner_parsed.netloc
+ ):
+ parser.error("scanner endpoints must be absolute HTTP(S) URLs")
+ scanner_hosts.append(scanner_parsed.hostname or "")
+ no_proxy = os.environ.get("NO_PROXY", os.environ.get("no_proxy", ""))
+ additions = [parsed.hostname or "", *scanner_hosts, "127.0.0.1", "localhost"]
+ if args.tier_endpoint:
+ tier_parsed = urlparse(args.tier_endpoint)
+ if tier_parsed.scheme not in {"http", "https"} or not tier_parsed.netloc:
+ parser.error("tier endpoint must be an absolute HTTP(S) URL")
+ additions.append(tier_parsed.hostname or "")
+ os.environ["NO_PROXY"] = ",".join(filter(None, [no_proxy, *additions]))
+ os.environ["no_proxy"] = os.environ["NO_PROXY"]
+ return args
+
+
+def main() -> int:
+ if hasattr(sys.stdout, "reconfigure"):
+ sys.stdout.reconfigure(line_buffering=True)
+ args = parse_args()
+ harness = IlmHarness(args)
+ try:
+ return harness.run()
+ finally:
+ harness.close()
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/scripts/validation_ilm/ilm_timing.py b/scripts/validation_ilm/ilm_timing.py
new file mode 100644
index 00000000..25a870ac
--- /dev/null
+++ b/scripts/validation_ilm/ilm_timing.py
@@ -0,0 +1,304 @@
+"""Independent time and progress oracles for serial, live ILM validation."""
+
+from __future__ import annotations
+
+import math
+import signal
+import threading
+import time
+from contextlib import contextmanager
+from dataclasses import dataclass
+from datetime import UTC, datetime, timedelta
+from email.utils import parsedate_to_datetime
+from typing import Any, Callable, Iterator
+
+
+class TimingPreconditionError(RuntimeError):
+ """The driver cannot establish the required before/after observation window."""
+
+
+class DeadlineExceeded(AssertionError):
+ """An operation did not satisfy its contract within the original budget."""
+
+
+def parse_expiry_date(value: str) -> datetime:
+ """Read an absolute expiry instant, not certify its wire-format parity.
+
+ AWS examples use HTTP dates; current RustFS also emits RFC 3339 here.
+ Both retain their timezone and must satisfy the same independent due window.
+ """
+ try:
+ try:
+ parsed = parsedate_to_datetime(value)
+ except (ValueError, TypeError, IndexError):
+ parsed = datetime.fromisoformat(value)
+ if parsed.tzinfo is None or parsed.utcoffset() is None:
+ raise ValueError("expiry-date has no timezone")
+ return parsed.astimezone(UTC)
+ except (ValueError, TypeError, IndexError) as exc:
+ raise AssertionError(f"invalid expiry-date: {value!r}") from exc
+
+
+# Real elapsed time is deliberately independent of fake clocks in oracle tests.
+_elapsed = time.monotonic
+_budgets: list[tuple[float, str, type[Exception]]] = []
+
+
+def _alarm(_signum: int, _frame: Any) -> None:
+ _end, label, error = min(_budgets, key=lambda item: item[0])
+ raise error(f"deadline exceeded: {label}")
+
+
+def _arm() -> None:
+ remaining = min(item[0] for item in _budgets) - _elapsed()
+ # An expired parent is already unwinding. Re-arming it would interrupt its
+ # exception handling repeatedly, possibly hiding the original failure.
+ signal.setitimer(signal.ITIMER_REAL, remaining if remaining > 0 else 0)
+
+
+def _check() -> None:
+ if _budgets:
+ end, label, error = min(_budgets, key=lambda item: item[0])
+ if _elapsed() >= end:
+ raise error(f"deadline exceeded: {label}")
+
+
+@contextmanager
+def time_budget(
+ seconds: float, label: str, error: type[Exception] = DeadlineExceeded
+) -> Iterator[None]:
+ """Interrupt stalled I/O and body reads; nested budgets cannot extend parents.
+
+ Live ILM cases run serially on the main thread on the Linux driver. A thread
+ pool timeout would leave the timed-out request mutating the next fixture.
+ """
+ if not math.isfinite(seconds) or seconds <= 0:
+ raise error(f"deadline exceeded: {label}")
+ if threading.current_thread() is not threading.main_thread():
+ raise RuntimeError("ILM time budgets require serial main-thread execution")
+ _check()
+ previous_handler = None
+ if not _budgets:
+ if signal.getitimer(signal.ITIMER_REAL)[0] > 0:
+ raise RuntimeError("ILM cannot replace an existing process alarm")
+ previous_handler = signal.signal(signal.SIGALRM, _alarm)
+ end = _elapsed() + seconds
+ entry = (end, label, error)
+ try:
+ _budgets.append(entry)
+ _arm()
+ yield
+ _check()
+ finally:
+ signal.setitimer(signal.ITIMER_REAL, 0)
+ if entry in _budgets:
+ _budgets.remove(entry)
+ if _budgets:
+ _arm()
+ else:
+ signal.signal(signal.SIGALRM, previous_handler)
+
+
+@dataclass(frozen=True)
+class DueWindow:
+ earliest: datetime
+ latest: datetime
+
+ def as_dict(self) -> dict[str, str]:
+ return {"earliest": self.earliest.isoformat(), "latest": self.latest.isoformat()}
+
+
+def expiry_window(
+ modified: datetime, days: int, day_seconds: int, process_seconds: int = 1
+) -> DueWindow:
+ """Round independently, preserving uncertainty in second-resolution S3 time.
+
+ LastModified may hide subsecond precision. The inclusive upper bound is
+ deliberately conservative, including the next reported second.
+ """
+ if modified.tzinfo is None or days <= 0 or day_seconds <= 0 or process_seconds <= 0:
+ raise TimingPreconditionError("expiry requires aware time and positive Days/intervals")
+ epoch = datetime(1970, 1, 1, tzinfo=UTC)
+ boundary = process_seconds * 1_000_000
+
+ def rounded(value: datetime) -> datetime:
+ delta = value.astimezone(UTC) - epoch + timedelta(seconds=days * day_seconds)
+ micros = (delta.days * 86400 + delta.seconds) * 1_000_000 + delta.microseconds
+ return epoch + timedelta(microseconds=((micros + boundary - 1) // boundary) * boundary)
+
+ return DueWindow(rounded(modified), rounded(modified + timedelta(seconds=1)))
+
+
+class ServerClock:
+ """Map server UTC intervals onto one monotonic driver timeline."""
+
+ def __init__(
+ self,
+ samples: list[tuple[str, datetime, float, float]],
+ *,
+ max_skew: float = 5,
+ wall: Callable[[], float] = time.time,
+ monotonic: Callable[[], float] = time.monotonic,
+ ) -> None:
+ if not samples:
+ raise TimingPreconditionError("no server clock samples")
+ self.wall = wall
+ self.monotonic = monotonic
+ self.anchor_wall = wall()
+ self.anchor_mono = monotonic()
+ self.offset_lo = min(date.timestamp() - end for _, date, _, end in samples)
+ self.offset_hi = max(date.timestamp() + 1 - start for _, date, start, _ in samples)
+ if max(abs(self.offset_lo), abs(self.offset_hi)) > max_skew:
+ raise TimingPreconditionError("server clock skew or sampling latency exceeds budget")
+ self.samples = samples
+
+ @staticmethod
+ def response_date(response: dict[str, Any]) -> datetime:
+ value = response.get("ResponseMetadata", {}).get("HTTPHeaders", {}).get("date")
+ try:
+ result = parsedate_to_datetime(value)
+ if result.tzinfo is None:
+ raise ValueError("Date has no timezone")
+ return result.astimezone(UTC)
+ except (ValueError, TypeError, IndexError) as exc:
+ raise TimingPreconditionError("missing or invalid S3 response Date") from exc
+
+ def now(self) -> DueWindow:
+ projected = self.anchor_wall + self.monotonic() - self.anchor_mono
+ if abs(self.wall() - projected) > 1:
+ raise TimingPreconditionError("driver wall clock jumped during the case")
+ return DueWindow(
+ datetime.fromtimestamp(projected + self.offset_lo, UTC),
+ datetime.fromtimestamp(projected + self.offset_hi, UTC),
+ )
+
+ def validate_response(self, response: dict[str, Any], start: float, end: float) -> None:
+ date = self.response_date(response).timestamp()
+ self.now()
+ if date + 1 < start + self.offset_lo or date > end + self.offset_hi:
+ raise TimingPreconditionError("server Date moved outside the calibrated clock interval")
+
+ def require_before(self, due: DueWindow, *, margin: float = 0) -> None:
+ if self.now().latest + timedelta(seconds=margin) >= due.earliest:
+ raise TimingPreconditionError("missed the required before-expiry observation window")
+
+ def passed(self, due: DueWindow) -> bool:
+ return self.now().earliest > due.latest
+
+ def timeout_after(self, due: DueWindow, allowance: float) -> float:
+ return max(0, (due.latest - self.now().earliest).total_seconds()) + allowance
+
+ def as_dict(self) -> dict[str, Any]:
+ return {
+ "anchorUtc": datetime.fromtimestamp(self.anchor_wall, UTC).isoformat(),
+ "anchorMonotonic": self.anchor_mono,
+ "offsetSeconds": [self.offset_lo, self.offset_hi],
+ "samples": [
+ {"endpoint": endpoint, "date": date.isoformat(), "sentUtc": start, "receivedUtc": end}
+ for endpoint, date, start, end in self.samples
+ ],
+ }
+
+
+def successful_cycle(cluster: dict[str, Any]) -> tuple[str, int, int | None] | None:
+ """Accept only a single fresh healthy leader and a completed success."""
+ leaders = []
+ for node in cluster.get("nodes", []):
+ data = node.get("status", {}).get("data", {})
+ metrics = data.get("metrics", {})
+ if metrics.get("leader_lock_held_by_this_process") is True:
+ leaders.append((node.get("endpoint"), data, metrics))
+ if len(leaders) != 1:
+ return None
+ endpoint, data, metrics = leaders[0]
+ freshness = data.get("freshness", {})
+ ended = freshness.get("last_cycle_end_unix_secs")
+ if not (
+ isinstance(endpoint, str)
+ and data.get("enabled") is True
+ and data.get("health") == "healthy"
+ and freshness.get("state") == "fresh"
+ and metrics.get("last_cycle_result") == "success"
+ and metrics.get("leader_lock_state") == "acquired"
+ and data.get("cycle_schedule", {}).get("execution_role") == "leader"
+ and isinstance(ended, int) and not isinstance(ended, bool) and ended > 0
+ ):
+ return None
+ lifetime_cycles = metrics.get("life_time_ops", {}).get("scan_cycle")
+ if lifetime_cycles is not None and (
+ not isinstance(lifetime_cycles, int) or isinstance(lifetime_cycles, bool) or lifetime_cycles < 0
+ ):
+ return None
+ return endpoint, ended, lifetime_cycles
+
+
+class CycleProgress:
+ """Count distinct observed cycles after a baseline, never historical success."""
+
+ def __init__(self) -> None:
+ self.last: tuple[str, int, int | None] | None = None
+ self.count = 0
+
+ def observe(self, cluster: dict[str, Any]) -> int:
+ current = successful_cycle(cluster)
+ if current is None:
+ # A gap/election may conceal a restart: re-establish the baseline.
+ self.last = None
+ self.count = 0
+ elif (self.last is None or current[0] != self.last[0] or current[1] < self.last[1]
+ or (current[2] is None) != (self.last[2] is None)
+ or (current[2] is not None and self.last[2] is not None and current[2] < self.last[2])):
+ self.last = current
+ self.count = 0
+ elif current[1] > self.last[1]:
+ self.count += 1
+ self.last = current
+ else:
+ # Preserve the latest lifetime counter between completions to detect
+ # an observed same-node reset even when the UTC end keeps advancing.
+ self.last = current
+ return self.count
+
+
+class BoundedBody:
+ def __init__(self, body: Any, seconds: float) -> None:
+ self.body = body
+ self.end = _elapsed() + seconds
+
+ def read(self, *args: Any, **kwargs: Any) -> bytes:
+ try:
+ with time_budget(self.end - _elapsed(), "S3 response body", TimeoutError):
+ result = self.body.read(*args, **kwargs)
+ amount = args[0] if args else kwargs.get("amt")
+ if amount is None or amount < 0 or not result:
+ self.body.close()
+ return result
+ except BaseException:
+ self.body.close()
+ raise
+
+ def close(self) -> None:
+ self.body.close()
+
+
+class BoundedS3Client:
+ """Bound complete SDK calls, including hidden retries and streamed bodies."""
+
+ def __init__(self, client: Any, seconds: float) -> None:
+ self.client = client
+ self.seconds = seconds
+
+ def __getattr__(self, name: str) -> Any:
+ member = getattr(self.client, name)
+ if not callable(member) or name == "close":
+ return member
+
+ def call(*args: Any, **kwargs: Any) -> Any:
+ started = _elapsed()
+ with time_budget(self.seconds, f"S3 {name}", TimeoutError):
+ response = member(*args, **kwargs)
+ if isinstance(response, dict) and "Body" in response:
+ response["Body"] = BoundedBody(response["Body"], self.seconds - (_elapsed() - started))
+ return response
+
+ return call
diff --git a/scripts/validation_ilm/rc_binary.py b/scripts/validation_ilm/rc_binary.py
new file mode 100644
index 00000000..efc53a47
--- /dev/null
+++ b/scripts/validation_ilm/rc_binary.py
@@ -0,0 +1,21 @@
+"""Resolve and identify the rc executable used by live validation."""
+
+from __future__ import annotations
+
+import hashlib
+import shutil
+from pathlib import Path
+
+
+def resolve_rc_binary(selection: str) -> tuple[str, str]:
+ """Return one canonical executable path and its SHA-256 digest."""
+
+ located = shutil.which(selection)
+ if located is None:
+ raise FileNotFoundError(selection)
+ resolved = Path(located).resolve(strict=True)
+ digest = hashlib.sha256()
+ with resolved.open("rb") as handle:
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
+ digest.update(chunk)
+ return str(resolved), digest.hexdigest()
diff --git a/scripts/validation_ilm/requirements.txt b/scripts/validation_ilm/requirements.txt
new file mode 100644
index 00000000..09c149ee
--- /dev/null
+++ b/scripts/validation_ilm/requirements.txt
@@ -0,0 +1,2 @@
+boto3==1.43.77
+botocore==1.43.77
diff --git a/scripts/validation_ilm/restart_cluster.sh b/scripts/validation_ilm/restart_cluster.sh
new file mode 100644
index 00000000..89bd1506
--- /dev/null
+++ b/scripts/validation_ilm/restart_cluster.sh
@@ -0,0 +1,30 @@
+#!/usr/bin/env bash
+set -Eeuo pipefail
+
+if [ "$#" -ne 2 ]; then
+ printf 'usage: %s NODE1 NODE2\n' "$0" >&2
+ exit 2
+fi
+
+ssh_user="${RUSTFS_SSH_USER:-azureuser}"
+ssh_port="${RUSTFS_SSH_PORT:-22}"
+ssh_opts=(-o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new -p "${ssh_port}")
+pids=()
+for node in "$@"; do
+ ssh "${ssh_opts[@]}" "${ssh_user}@${node}" \
+ 'if [ "$(id -u)" -ne 0 ]; then sudo -n systemctl restart rustfs; else systemctl restart rustfs; fi' &
+ pids+=("$!")
+done
+for pid in "${pids[@]}"; do wait "${pid}"; done
+
+for endpoint in http://rustfs-node1:9000 http://rustfs-node2:9000; do
+ ready=0
+ for ((attempt=0; attempt<24; attempt++)); do
+ if curl --noproxy '*' -fsS --max-time 3 "${endpoint}/minio/health/live" >/dev/null; then
+ ready=1
+ break
+ fi
+ sleep 2
+ done
+ [ "${ready}" -eq 1 ] || { printf 'node not ready after restart: %s\n' "${endpoint}" >&2; exit 1; }
+done
diff --git a/scripts/validation_ilm/verify_result.py b/scripts/validation_ilm/verify_result.py
new file mode 100644
index 00000000..76e63c0a
--- /dev/null
+++ b/scripts/validation_ilm/verify_result.py
@@ -0,0 +1,46 @@
+#!/usr/bin/env python3
+"""Check an ILM harness result before granting a canonical PASS."""
+
+from __future__ import annotations
+
+import json
+from pathlib import Path
+import sys
+
+
+def verify(case_id: str, evidence_dir: Path) -> None:
+ result = json.loads((evidence_dir / "result.json").read_text(encoding="utf-8"))
+ manifest = json.loads((evidence_dir / "run-manifest.json").read_text(encoding="utf-8"))
+ cases = result["status"]["cases"]
+ if result["status"]["state"] != "pass" or len(cases) != 1:
+ raise ValueError("ILM run did not pass exactly one case")
+ case = cases[0]
+ if case["id"] != case_id or case["state"] != "pass":
+ raise ValueError("ILM result does not pass the selected case")
+ if manifest["executionPlan"]["scoredCases"] != [case_id]:
+ raise ValueError("ILM execution plan differs from selected case")
+ if manifest["cleanupOutcome"]["state"] != "pass" or manifest["preservedBuckets"]:
+ raise ValueError("ILM test bucket cleanup did not pass")
+ if not case["evidence"]:
+ raise ValueError("ILM case has no assertion evidence")
+ root = evidence_dir.resolve(strict=True)
+ for name in case["evidence"]:
+ path = (root / name).resolve(strict=True)
+ if not path.is_relative_to(root) or not path.is_file():
+ raise ValueError(f"unsafe ILM evidence path: {name}")
+
+
+def main() -> int:
+ if len(sys.argv) != 3:
+ print(f"usage: {sys.argv[0]} CASE_ID EVIDENCE_DIR", file=sys.stderr)
+ return 2
+ try:
+ verify(sys.argv[1], Path(sys.argv[2]))
+ except (OSError, ValueError, KeyError, TypeError, json.JSONDecodeError) as exc:
+ print(f"ILM verification failed: {exc}", file=sys.stderr)
+ return 1
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/scripts/validation_migration_review.py b/scripts/validation_migration_review.py
new file mode 100644
index 00000000..101018ce
--- /dev/null
+++ b/scripts/validation_migration_review.py
@@ -0,0 +1,153 @@
+#!/usr/bin/env python3
+"""Audit test implementation coverage independently of candidate live results."""
+
+from __future__ import annotations
+
+import argparse
+from collections import Counter
+from datetime import date
+import hashlib
+import json
+from pathlib import Path
+import re
+import sys
+
+try:
+ from .validation_catalog import validate_snapshot
+ from .validation_heal.execution_policy import ENVIRONMENT_GATED_CASES
+except ImportError:
+ from validation_catalog import validate_snapshot
+ from validation_heal.execution_policy import ENVIRONMENT_GATED_CASES
+
+ROOT = Path(__file__).resolve().parent.parent
+IMPLEMENTATION_STATES = {"implemented", "environment-gated", "partial"}
+LIVE_STATES = {"accepted", "retained-failure", "pending", "blocked", "disabled"}
+HEX40 = re.compile(r"[0-9a-f]{40}\Z")
+HEX64 = re.compile(r"[0-9a-f]{64}\Z")
+
+
+def require(condition: object, message: str) -> None:
+ if not condition:
+ raise ValueError(message)
+
+
+def local_file(root: Path, name: str) -> Path:
+ require(isinstance(name, str) and bool(name), "missing implementation reference")
+ path = Path(name)
+ require(not path.is_absolute() and ".." not in path.parts, f"nonlocal implementation reference: {name}")
+ result = root / path
+ require(result.is_file() and not result.is_symlink()
+ and result.resolve().is_relative_to(root.resolve()), f"missing implementation reference: {name}")
+ return result
+
+
+def validate_review(review: dict, catalog: dict, root: Path = ROOT) -> None:
+ """Validate an explicit review; never derive implementation state from FAIL."""
+ require(isinstance(review, dict), "implementation review must be an object")
+ validate_snapshot(catalog)
+ require(isinstance(review.get("as_of"), str), "missing implementation review date")
+ require(date.fromisoformat(review["as_of"]).isoformat() == review["as_of"], "invalid implementation review date")
+ require(review.get("schema_version") == 1, "unsupported implementation review schema")
+ require(review.get("source_commit") == catalog["source_commit"]
+ and review.get("source_sha256") == catalog["source_sha256"], "implementation review uses another source catalog")
+ require(isinstance(review.get("reviewed_testing_commit"), str)
+ and HEX40.fullmatch(review["reviewed_testing_commit"]), "missing immutable review base")
+ files = review.get("reviewed_files")
+ require(isinstance(files, dict) and bool(files), "missing reviewed implementation files")
+ for name, digest in files.items():
+ require(isinstance(digest, str) and HEX64.fullmatch(digest), f"invalid review digest: {name}")
+ require(hashlib.sha256(local_file(root, name).read_bytes()).hexdigest() == digest,
+ f"implementation changed since review: {name}")
+ recipes = json.loads((root / "scripts/validation_heal/recipes.json").read_text())
+ variants = {case["id"]: set(case["variants"]) for case in recipes["cases"]}
+ catalog_cases = {case["id"]: case for module in catalog["modules"] for case in module["cases"]}
+ cases = review.get("cases")
+ require(isinstance(cases, list), "missing implementation case reviews")
+ seen = set()
+ for case in cases:
+ require(isinstance(case, dict), "implementation case review must be an object")
+ case_id = case.get("case_id")
+ require(isinstance(case_id, str) and case_id in catalog_cases, f"unknown implementation case: {case_id}")
+ require(case_id not in seen, f"duplicate implementation case: {case_id}")
+ seen.add(case_id)
+ state = case.get("implementation")
+ require(isinstance(state, str) and state in IMPLEMENTATION_STATES, f"invalid implementation state: {case_id}")
+ live = case.get("live", {})
+ require(isinstance(live, dict) and isinstance(live.get("state"), str) and live["state"] in LIVE_STATES, f"invalid live state: {case_id}")
+ require(isinstance(case.get("basis"), str) and bool(case["basis"].strip()), f"missing review basis: {case_id}")
+ require(isinstance(case.get("references"), list) and bool(case["references"]), f"missing implementation references: {case_id}")
+ for name in case["references"]:
+ local_file(root, name)
+ require(name in files, f"unreviewed implementation reference: {case_id}: {name}")
+ require(isinstance(case.get("gaps"), list), f"missing explicit gap list: {case_id}")
+ if state == "partial":
+ require(bool(case["gaps"]) and all(isinstance(gap, str) and gap.strip() for gap in case["gaps"]),
+ f"partial implementation needs actionable gaps: {case_id}")
+ else:
+ require(not case["gaps"], f"implemented case still has implementation gaps: {case_id}")
+ if state == "environment-gated":
+ policy = ENVIRONMENT_GATED_CASES.get(case_id)
+ require(policy is not None and policy["disabledByDefault"] is True
+ and live["state"] == "disabled", f"invalid disabled environment procedure: {case_id}")
+ require(case.get("required_capabilities") == policy["requiredCapabilities"]
+ and case.get("reason_code") == policy["reasonCode"], f"environment requirements drifted: {case_id}")
+ else:
+ require(case_id not in ENVIRONMENT_GATED_CASES, f"environment case lost its gate: {case_id}")
+ if live["state"] == "accepted":
+ require(state == "implemented", f"accepted live case lacks complete implementation: {case_id}")
+ runs = live.get("runs")
+ require(isinstance(runs, list) and bool(runs)
+ and all(type(run) is int and run > 0 for run in runs), f"missing accepted run identity: {case_id}")
+ require(live.get("basis") == "independent-complete-live-audit", f"unsupported acceptance claim: {case_id}")
+ elif state != "environment-gated":
+ require(isinstance(live.get("finding"), str) and bool(live["finding"].strip()), f"missing failure scope: {case_id}")
+ source_variants = case.get("source_variants")
+ require(isinstance(source_variants, dict) and set(source_variants) == variants.get(case_id, {"catalog-assertions"}),
+ f"source branch coverage changed: {case_id}")
+ for branch, references in source_variants.items():
+ require(isinstance(references, list) and (bool(references) or state == "partial"),
+ f"source branch has no implementation: {case_id}: {branch}")
+ require(all(name in case["references"] for name in references), f"unreviewed branch reference: {case_id}: {branch}")
+ require(seen == set(catalog_cases), f"missing implementation reviews: {sorted(set(catalog_cases) - seen)}")
+
+
+def summarize(review: dict) -> dict:
+ cases = review["cases"]
+ implementation = Counter(case["implementation"] for case in cases)
+ live = Counter(case["live"]["state"] for case in cases)
+ return {
+ "as_of": review["as_of"],
+ "total": len(cases),
+ "implementation": {state: implementation[state] for state in sorted(IMPLEMENTATION_STATES)},
+ "live": {state: live[state] for state in sorted(LIVE_STATES)},
+ "remaining_implementation": [{"case_id": case["case_id"], "gaps": case["gaps"]}
+ for case in cases if case["implementation"] == "partial"],
+ "all_implementation_complete": implementation["partial"] == 0,
+ "environment_adapters_required": [case["case_id"] for case in cases
+ if case["implementation"] == "environment-gated"],
+ "all_cases_live_accepted": live["accepted"] == len(cases),
+ "candidate_pass_claimed": False,
+ }
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--review", type=Path, default=ROOT / "validation-migration-review.json")
+ parser.add_argument("--catalog", type=Path, default=ROOT / "validation-cases.json")
+ parser.add_argument("--require-complete", action="store_true",
+ help="fail on implementation gaps; candidate FAIL never changes this decision")
+ args = parser.parse_args()
+ try:
+ review = json.loads(args.review.read_text())
+ catalog = json.loads(args.catalog.read_text())
+ validate_review(review, catalog)
+ summary = summarize(review)
+ print(json.dumps(summary, ensure_ascii=False, indent=2))
+ return int(args.require_complete and not summary["all_implementation_complete"])
+ except (OSError, ValueError) as exc:
+ print(f"implementation review invalid: {exc}", file=sys.stderr)
+ return 2
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/scripts/validation_oidc.py b/scripts/validation_oidc.py
new file mode 100644
index 00000000..a0ac36eb
--- /dev/null
+++ b/scripts/validation_oidc.py
@@ -0,0 +1,276 @@
+#!/usr/bin/env python3
+"""Validate a real Keycloak identity and STS session on the Security fleet."""
+
+from __future__ import annotations
+
+import argparse
+import base64
+from datetime import datetime, timezone
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import secrets
+import socket
+import subprocess
+import sys
+import time
+from urllib import error, parse, request
+import uuid
+import xml.etree.ElementTree as ET
+
+
+KEYCLOAK_IMAGE = "quay.io/keycloak/keycloak@sha256:6a7217a100bd3e5de4063a27a538ef999a3c5a88c4b4ec0ffc0a642aee7b2597"
+HTTP = request.build_opener(request.ProxyHandler({}))
+
+
+def save(path: Path, value: dict) -> None:
+ path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n")
+
+
+def docker(*arguments: str) -> str:
+ result = subprocess.run(["docker", *arguments], capture_output=True, text=True, timeout=300)
+ if result.returncode and "permission denied while trying to connect" in result.stderr.lower():
+ result = subprocess.run(["sudo", "-n", "docker", *arguments], capture_output=True, text=True, timeout=300)
+ if result.returncode:
+ raise RuntimeError(f"Keycloak container operation {arguments[0]} failed ({result.returncode}): {result.stderr.strip()[:600]}")
+ return result.stdout
+
+
+def http(url: str, data: dict | None = None) -> tuple[int, bytes]:
+ body = parse.urlencode(data).encode() if data is not None else None
+ headers = {"Content-Type": "application/x-www-form-urlencoded"} if body is not None else {}
+ try:
+ with HTTP.open(request.Request(url, data=body, headers=headers), timeout=15) as response:
+ return response.status, response.read(2 * 1024 * 1024)
+ except error.HTTPError as response:
+ return response.code, response.read(2 * 1024 * 1024)
+
+
+def setup(directory: Path) -> None:
+ with socket.socket() as sock:
+ sock.bind(("0.0.0.0", 0))
+ port = sock.getsockname()[1]
+ origin = f"http://{os.environ.get('RUSTFS_HEAL_IP', '10.0.0.4')}:{port}"
+ issuer = origin + "/realms/rustfs-validation"
+ state = {"container": "rustfs-validation-oidc-" + secrets.token_hex(8),
+ "issuer": issuer, "config_url": issuer + "/.well-known/openid-configuration",
+ "client_secret": secrets.token_hex(24), "wrong_client_secret": secrets.token_hex(24),
+ "password": secrets.token_hex(24), "subject": str(uuid.uuid4())}
+ state_path = directory / "oidc-state.json"
+ save(state_path, state)
+ state_path.chmod(0o600)
+ fixture = {"realm": "rustfs-validation", "enabled": True, "sslRequired": "none",
+ "accessTokenLifespan": 300, "clients": [],
+ "users": [{"id": state["subject"], "username": "validation-user", "enabled": True,
+ "firstName": "Validation", "lastName": "User", "emailVerified": True,
+ "email": "validation@example.test", "credentials": [
+ {"type": "password", "value": state["password"], "temporary": False}]}]}
+ for client_id, secret_name in (("rustfs-validation", "client_secret"), ("wrong-audience", "wrong_client_secret")):
+ fixture["clients"].append({"clientId": client_id, "secret": state[secret_name], "enabled": True,
+ "protocol": "openid-connect", "publicClient": False,
+ "standardFlowEnabled": False, "directAccessGrantsEnabled": True})
+ fixture_path = directory / "oidc-realm.json"
+ save(fixture_path, fixture)
+ fixture_path.chmod(0o644)
+ docker("run", "--detach", "--rm", "--name", state["container"], "--memory", "1g",
+ "--publish", f"0.0.0.0:{port}:8080", "--env", f"KC_HOSTNAME={origin}",
+ "--volume", f"{fixture_path}:/opt/keycloak/data/import/realm.json:ro",
+ KEYCLOAK_IMAGE, "start-dev", "--import-realm")
+ deadline = time.monotonic() + 180
+ while time.monotonic() < deadline:
+ try:
+ status, body = http(state["config_url"])
+ discovery = json.loads(body) if status == 200 else {}
+ if discovery.get("issuer") == issuer and discovery.get("token_endpoint") and discovery.get("jwks_uri"):
+ save(directory / "validation-oidc-provider.json",
+ {"image": KEYCLOAK_IMAGE, "issuer": issuer, "discovery_sha256": hashlib.sha256(body).hexdigest(),
+ "token_endpoint": discovery["token_endpoint"], "jwks_uri": discovery["jwks_uri"]})
+ return
+ except (OSError, ValueError):
+ pass
+ if docker("inspect", "--format", "{{.State.Running}}", state["container"]).strip() != "true":
+ raise RuntimeError("Keycloak exited before discovery was ready")
+ time.sleep(2)
+ raise RuntimeError("Keycloak discovery did not become ready within 180 seconds")
+
+
+def xml_values(body: bytes) -> dict[str, str]:
+ return {element.tag.rsplit("}", 1)[-1]: element.text or "" for element in ET.fromstring(body).iter()}
+
+
+def token(state: dict, client_id: str, secret_name: str) -> tuple[str, dict]:
+ status, body = http(state["issuer"] + "/protocol/openid-connect/token",
+ {"grant_type": "password", "client_id": client_id,
+ "client_secret": state[secret_name], "username": "validation-user",
+ "password": state["password"], "scope": "openid"})
+ if status != 200:
+ raise RuntimeError(f"Keycloak password login failed (HTTP {status})")
+ encoded = json.loads(body)["id_token"]
+ payload = encoded.split(".")[1]
+ claims = json.loads(base64.urlsafe_b64decode(payload + "=" * (-len(payload) % 4)))
+ audience = claims["aud"] if isinstance(claims["aud"], list) else [claims["aud"]]
+ if (claims["iss"] != state["issuer"] or client_id not in audience or claims["sub"] != state["subject"]
+ or claims.get("preferred_username") != "validation-user"):
+ raise RuntimeError("Keycloak identity claims differ from the configured fixture")
+ return encoded, {name: claims[name] for name in ("iss", "sub", "aud", "exp", "preferred_username")}
+
+
+def sts(endpoint: str, identity_token: str, subject: str, policy: dict | None = None) -> tuple[dict, dict]:
+ data = {"Action": "AssumeRoleWithWebIdentity", "Version": "2011-06-15",
+ "DurationSeconds": "900", "WebIdentityToken": identity_token}
+ if policy is not None:
+ data["Policy"] = json.dumps(policy, separators=(",", ":"))
+ status, body = http(endpoint.rstrip("/") + "/", data)
+ values = xml_values(body)
+ if status != 200:
+ raise RuntimeError(f"valid OIDC identity failed STS (HTTP {status}, code {values.get('Code', 'missing')})")
+ required = ("AccessKeyId", "SecretAccessKey", "SessionToken", "Expiration", "SubjectFromWebIdentityToken")
+ if not all(values.get(name) for name in required) or values["SubjectFromWebIdentityToken"] != subject:
+ raise RuntimeError("STS omitted credentials or returned a different identity subject")
+ expiration = datetime.fromisoformat(values["Expiration"].replace("Z", "+00:00"))
+ remaining = (expiration - datetime.now(timezone.utc)).total_seconds()
+ if not 0 < remaining <= 960:
+ raise RuntimeError("STS session expiration is outside the requested 900 second boundary")
+ credentials = {"AWS_ACCESS_KEY_ID": values["AccessKeyId"], "AWS_SECRET_ACCESS_KEY": values["SecretAccessKey"],
+ "AWS_SESSION_TOKEN": values["SessionToken"]}
+ return credentials, {"subject": subject, "expiration": values["Expiration"]}
+
+
+def aws(endpoint: str, credentials: dict, *arguments: str, denied: bool = False,
+ rejection_codes: frozenset[str] = frozenset({"AccessDenied", "InvalidToken", "InvalidAccessKeyId"})) -> str:
+ environment = os.environ.copy()
+ for name in ("AWS_SESSION_TOKEN", "AWS_SECURITY_TOKEN", "AWS_PROFILE"):
+ environment.pop(name, None)
+ environment.update(credentials)
+ environment.update(AWS_EC2_METADATA_DISABLED="true", AWS_CONFIG_FILE="/dev/null",
+ AWS_SHARED_CREDENTIALS_FILE="/dev/null", AWS_DEFAULT_REGION="us-east-1")
+ result = subprocess.run(["aws", "--endpoint-url", endpoint, "--no-cli-pager", "s3api", *arguments],
+ env=environment, capture_output=True, text=True, timeout=60)
+ if denied:
+ match = re.search(r"An error occurred \(([A-Za-z0-9]+)\)", result.stderr)
+ code = match.group(1) if match else "missing"
+ if not result.returncode or code not in rejection_codes:
+ raise RuntimeError(f"expected access boundary rejection, received {code} (exit {result.returncode})")
+ return code
+ if result.returncode:
+ raise RuntimeError(f"S3 operation {arguments[0]} failed (exit {result.returncode})")
+ return result.stdout
+
+
+def test(directory: Path, endpoint: str) -> None:
+ state = json.loads((directory / "oidc-state.json").read_text())
+ root = {"AWS_ACCESS_KEY_ID": os.environ["RUSTFS_ACCESS_KEY"], "AWS_SECRET_ACCESS_KEY": os.environ["RUSTFS_SECRET_KEY"]}
+ prefix = "val-oidc-" + secrets.token_hex(8)
+ buckets = [prefix + "-allowed", prefix + "-outside"]
+ source = directory / "oidc-source.bin"
+ body = secrets.token_bytes(65536)
+ source.write_bytes(body)
+ observations = {"identity": {}, "checks": {}, "cleanup": "PENDING"}
+ created = []
+ primary = None
+ try:
+ for bucket in buckets:
+ aws(endpoint, root, "create-bucket", "--bucket", bucket)
+ created.append(bucket)
+ aws(endpoint, root, "put-object", "--bucket", bucket, "--key", "object.bin", "--body", str(source))
+ encoded, claims = token(state, "rustfs-validation", "client_secret")
+ observations["identity"] = claims
+ # RustFS exposes the configured username claim as the STS session identity.
+ # The JWT's issuer-scoped subject was independently checked above.
+ identity_credentials, identity_session = sts(endpoint, encoded, claims["preferred_username"])
+ observations["identity_session"] = identity_session
+ identity_read = directory / "oidc-identity-read.bin"
+ aws(endpoint, identity_credentials, "get-object", "--bucket", buckets[0], "--key", "object.bin", str(identity_read))
+ if identity_read.read_bytes() != body:
+ raise RuntimeError("OIDC identity GET returned different object bytes")
+ identity_denial = aws(endpoint, identity_credentials, "put-object", "--bucket", buckets[0],
+ "--key", "forbidden.bin", "--body", str(source), denied=True)
+ if identity_denial != "AccessDenied":
+ raise RuntimeError("readonly identity boundary did not return AccessDenied")
+ observations["checks"]["identity_write_denied"] = identity_denial
+ policy = {"Version": "2012-10-17", "Statement": [{"Effect": "Allow", "Action": "s3:GetObject",
+ "Resource": f"arn:aws:s3:::{buckets[0]}/*"}]}
+ session, session_observation = sts(endpoint, encoded, claims["preferred_username"], policy)
+ observations["session"] = session_observation
+ received = directory / "oidc-read.bin"
+ aws(endpoint, session, "get-object", "--bucket", buckets[0], "--key", "object.bin", str(received))
+ if received.read_bytes() != body:
+ raise RuntimeError("OIDC session GET returned different object bytes")
+ observations["checks"]["allowed_get_sha256"] = hashlib.sha256(body).hexdigest()
+ outside_code = aws(endpoint, session, "get-object", "--bucket", buckets[1], "--key", "object.bin",
+ str(directory / "oidc-denied.bin"), denied=True)
+ if outside_code != "AccessDenied":
+ raise RuntimeError("session policy boundary did not return AccessDenied")
+ observations["checks"]["session_outside_denied"] = outside_code
+ # RustFS auth::check_session_token returns InvalidRequest for absent or
+ # mismatched temporary tokens. Authorization denials above must still
+ # return AccessDenied; a generic request error cannot prove that policy.
+ for name, altered in (("missing_session_token", {key: value for key, value in session.items() if key != "AWS_SESSION_TOKEN"}),
+ ("invalid_session_token", {**session, "AWS_SESSION_TOKEN": "invalid-session-token"})):
+ observations["checks"][name] = aws(endpoint, altered, "get-object", "--bucket", buckets[0], "--key", "object.bin",
+ str(directory / "oidc-invalid.bin"), denied=True,
+ rejection_codes=frozenset({"InvalidRequest"}))
+ parts = encoded.split(".")
+ parts[2] = ("A" if parts[2][0] != "A" else "B") + parts[2][1:]
+ wrong_token, _ = token(state, "wrong-audience", "wrong_client_secret")
+ for name, invalid in (("tampered_identity", ".".join(parts)), ("wrong_audience", wrong_token)):
+ status, response = http(endpoint.rstrip("/") + "/", {"Action": "AssumeRoleWithWebIdentity", "Version": "2011-06-15",
+ "DurationSeconds": "900", "WebIdentityToken": invalid})
+ code = xml_values(response).get("Code")
+ if status != 403 or code != "AccessDenied":
+ raise RuntimeError(f"invalid OIDC identity accepted or rejected incorrectly (HTTP {status}, code {code})")
+ observations["checks"][name] = {"http_status": status, "code": code}
+ for bucket in buckets:
+ aws(endpoint, root, "get-object", "--bucket", bucket, "--key", "object.bin", str(directory / "oidc-root-read.bin"))
+ if (directory / "oidc-root-read.bin").read_bytes() != body:
+ raise RuntimeError("rejected session requests changed the root fixture")
+ observations["checks"]["fixtures_preserved"] = True
+ except Exception as exc:
+ primary = exc
+ observations["failure"] = {"type": type(exc).__name__, "message": str(exc)}
+ finally:
+ cleanup_errors = []
+ for bucket in created:
+ try:
+ aws(endpoint, root, "delete-object", "--bucket", bucket, "--key", "object.bin")
+ aws(endpoint, root, "delete-bucket", "--bucket", bucket)
+ except Exception:
+ cleanup_errors.append(bucket)
+ observations["cleanup"] = "FAIL" if cleanup_errors else "PASS"
+ save(directory / "validation-oidc-observations.json", observations)
+ if primary is not None:
+ raise primary
+ if cleanup_errors:
+ raise RuntimeError("OIDC fixture cleanup failed")
+
+
+def teardown(directory: Path) -> None:
+ state_path = directory / "oidc-state.json"
+ if not state_path.exists():
+ return
+ state = json.loads(state_path.read_text())
+ containers = docker("ps", "--all", "--format", "{{.Names}}").splitlines()
+ if state["container"] in containers:
+ docker("rm", "--force", state["container"])
+ if state["container"] in docker("ps", "--all", "--format", "{{.Names}}").splitlines():
+ raise RuntimeError("run-owned Keycloak container remains after cleanup")
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("mode", choices=("setup", "test", "teardown"))
+ parser.add_argument("--directory", type=Path, required=True)
+ parser.add_argument("--endpoint")
+ args = parser.parse_args()
+ {"setup": lambda: setup(args.directory), "test": lambda: test(args.directory, args.endpoint),
+ "teardown": lambda: teardown(args.directory)}[args.mode]()
+
+
+if __name__ == "__main__":
+ try:
+ main()
+ except Exception as exc:
+ print(f"OIDC validation failed: {exc}", file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_oidc/run_case.sh b/scripts/validation_oidc/run_case.sh
new file mode 100644
index 00000000..be92f33a
--- /dev/null
+++ b/scripts/validation_oidc/run_case.sh
@@ -0,0 +1,122 @@
+#!/usr/bin/env bash
+set -Eeuo pipefail
+
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+RUSTFS_SECURITY_SCRIPT_SOURCE_ONLY=1 source "${ROOT}/rustfs-security-test.sh"
+
+candidate="" testing_commit="" artifacts=""
+environment_active=0 provider_active=0 finalized=0
+binary_sha="" evidence="" cleanup_status=PASS
+
+finalize_environment() {
+ local cleanup_rc=0
+ if [ "${environment_active}" -eq 1 ]; then
+ environment_active=0
+ (env_teardown single-single) >> "${evidence}/cleanup.log" 2>&1 || cleanup_rc=1
+ fi
+ if [ "${provider_active}" -eq 1 ]; then
+ provider_active=0
+ python3 -B "${ROOT}/scripts/validation_oidc.py" teardown --directory "${TEST_TMP}" \
+ >> "${evidence}/cleanup.log" 2>&1 || cleanup_rc=1
+ fi
+ [ "${cleanup_rc}" -eq 0 ] || cleanup_status=FAIL
+ return "${cleanup_rc}"
+}
+
+on_exit() {
+ local status=$?
+ trap - EXIT
+ if [ "${DRY_RUN}" -eq 0 ] && [ "${finalized}" -eq 0 ] && [ -n "${evidence}" ]; then
+ finalize_environment || true
+ fi
+ cleanup_tmp
+ exit "${status}"
+}
+trap on_exit EXIT
+trap 'exit 130' INT
+trap 'exit 143' TERM
+
+while [ "$#" -gt 0 ]; do
+ case "$1" in
+ --validation-case) VALIDATION_CASE="$2"; shift 2 ;;
+ --candidate-manifest) candidate="$2"; shift 2 ;;
+ --testing-commit) testing_commit="$2"; shift 2 ;;
+ --validation-artifacts-dir) artifacts="$2"; shift 2 ;;
+ --dry-run) DRY_RUN=1; shift ;;
+ -y|--yes) ASSUME_YES=1; shift ;;
+ *) die "unknown OIDC validation option: $1" ;;
+ esac
+done
+[ "${VALIDATION_CASE}" = oidc.login-and-session ] || die "OIDC validation case required"
+if [ "${DRY_RUN}" -eq 1 ]; then
+ VALIDATION_OIDC_ORIGIN="http://${RUSTFS_HEAL_IP:-10.0.0.4}:18090"
+ VALIDATION_OIDC_ISSUER="${VALIDATION_OIDC_ORIGIN}/realms/rustfs-validation"
+ VALIDATION_OIDC_CONFIG_URL="${VALIDATION_OIDC_ISSUER}/.well-known/openid-configuration"
+ VALIDATION_OIDC_CLIENT_SECRET=dry-run
+ env_setup single-single
+ env_teardown single-single
+ exit 0
+fi
+for command in docker ssh curl openssl aws jq python3 rc; do need_cmd "${command}" "OIDC validation"; done
+[ -f "${candidate}" ] || die "candidate manifest missing"
+[ -n "${artifacts}" ] && [ ! -e "${artifacts}" ] || die "fresh artifact directory required"
+[ "${testing_commit}" = "$(git -C "${ROOT}" rev-parse HEAD)" ] || die "testing commit differs from checkout"
+PACKAGE_URL="$(jq -er '.package_url' "${candidate}")"
+PACKAGE_SHA256="$(jq -er '.package_sha256' "${candidate}")"
+[[ "${PACKAGE_SHA256}" =~ ^[0-9a-f]{64}$ ]] || die "invalid candidate SHA-256"
+export RUSTFS_ACCESS_KEY="${ACCESS_KEY}" RUSTFS_SECRET_KEY="${SECRET_KEY}"
+evidence="${artifacts}/evidence/${VALIDATION_CASE}"
+mkdir -p "${artifacts}/cases" "${evidence}"
+python3 "${ROOT}/scripts/validation_result.py" --case-id "${VALIDATION_CASE}" \
+ --profile single-1disk --result-file "${artifacts}/cases/${VALIDATION_CASE}.json" \
+ --verdict RUNNING --cleanup-status PENDING
+
+provider_active=1
+python3 -B "${ROOT}/scripts/validation_oidc.py" setup --directory "${TEST_TMP}" \
+ > "${evidence}/provider-setup.log" 2>&1 || {
+ cat "${evidence}/provider-setup.log"; exit 1;
+ }
+VALIDATION_OIDC_ISSUER="$(jq -er '.issuer' "${TEST_TMP}/oidc-state.json")"
+VALIDATION_OIDC_ORIGIN="${VALIDATION_OIDC_ISSUER%/realms/*}"
+VALIDATION_OIDC_CONFIG_URL="$(jq -er '.config_url' "${TEST_TMP}/oidc-state.json")"
+VALIDATION_OIDC_CLIENT_SECRET="$(jq -er '.client_secret' "${TEST_TMP}/oidc-state.json")"
+environment_active=1
+ENDPOINT="${RUSTFS_API_ENDPOINT:-${ENDPOINT_SINGLE_SINGLE}}"
+(env_setup single-single) > "${evidence}/setup.log" 2>&1 || {
+ cat "${evidence}/setup.log"; exit 1;
+}
+binary_sha="$(ssh "${SSH_OPTS[@]}" "${SSH_USER}@${NODES[0]}" sha256sum /usr/bin/rustfs | awk '{print $1}')"
+[[ "${binary_sha}" =~ ^[0-9a-f]{64}$ ]] || die "installed RustFS binary SHA-256 unavailable"
+case_rc=0
+python3 -B "${ROOT}/scripts/validation_oidc.py" test --directory "${TEST_TMP}" --endpoint "${ENDPOINT}" \
+ > "${evidence}/test.log" 2>&1 || case_rc=$?
+cat "${evidence}/test.log"
+if ! jq -e '.cleanup == "PASS"' "${TEST_TMP}/validation-oidc-observations.json" >/dev/null 2>&1; then
+ cleanup_status=FAIL
+fi
+finalize_environment || true
+finalized=1
+for item in validation-oidc-provider.json validation-oidc-observations.json; do
+ if [ -f "${TEST_TMP}/${item}" ]; then cp "${TEST_TMP}/${item}" "${evidence}/${item}"; fi
+done
+verdict=FAIL reason="OIDC identity or session assertions failed; inspect evidence"
+if [ "${case_rc}" -eq 0 ] && [ "${cleanup_status}" = PASS ]; then
+ verdict=PASS; reason=""
+elif [ "${case_rc}" -eq 0 ]; then
+ verdict=INFRA_ERROR; reason="OIDC environment finalization failed"
+fi
+args=("${ROOT}/scripts/validation_result.py" --case-id "${VALIDATION_CASE}"
+ --profile single-1disk --result-file "${artifacts}/cases/${VALIDATION_CASE}.json"
+ --verdict "${verdict}" --cleanup-status "${cleanup_status}"
+ --candidate-manifest "${candidate}" --rc-bin "$(command -v rc)"
+ --testing-commit "${testing_commit}" --binary-sha256 "${binary_sha}"
+ --evidence "evidence/${VALIDATION_CASE}/test.log"
+ --evidence "evidence/${VALIDATION_CASE}/setup.log"
+ --evidence "evidence/${VALIDATION_CASE}/cleanup.log")
+if [ -n "${reason}" ]; then args+=(--reason "${reason}"); fi
+if [ "${verdict}" = PASS ]; then
+ args+=(--evidence "evidence/${VALIDATION_CASE}/validation-oidc-provider.json"
+ --evidence "evidence/${VALIDATION_CASE}/validation-oidc-observations.json")
+fi
+python3 "${args[@]}"
+[ "${verdict}" = PASS ]
diff --git a/scripts/validation_pool/case_plan.py b/scripts/validation_pool/case_plan.py
new file mode 100644
index 00000000..ec793cf9
--- /dev/null
+++ b/scripts/validation_pool/case_plan.py
@@ -0,0 +1,30 @@
+"""Select pool lifecycle cases from the canonical auto-testing catalog."""
+
+from __future__ import annotations
+
+import json
+from pathlib import Path
+from typing import Iterable
+
+
+CATALOG = Path(__file__).resolve().parents[2] / "validation-cases.json"
+
+
+def pool_case_ids() -> tuple[str, ...]:
+ catalog = json.loads(CATALOG.read_text(encoding="utf-8"))
+ modules = [module for module in catalog["modules"] if module["id"] == "pool-lifecycle"]
+ if len(modules) != 1:
+ raise ValueError("canonical pool module is missing or duplicated")
+ return tuple(case["id"] for case in modules[0]["cases"])
+
+
+POOL_CASE_IDS = pool_case_ids()
+
+
+def pool_harness_scored_cases(requested_cases: Iterable[str] | None) -> tuple[str, ...]:
+ if requested_cases is None:
+ return POOL_CASE_IDS
+ selected = tuple(requested_cases)
+ if len(selected) != len(set(selected)) or set(selected) - set(POOL_CASE_IDS):
+ raise ValueError("duplicate or unknown canonical pool case")
+ return tuple(case_id for case_id in POOL_CASE_IDS if case_id in selected)
diff --git a/scripts/validation_pool/pool_behavior.py b/scripts/validation_pool/pool_behavior.py
new file mode 100644
index 00000000..d47e3480
--- /dev/null
+++ b/scripts/validation_pool/pool_behavior.py
@@ -0,0 +1,2812 @@
+#!/usr/bin/env python3
+"""Run destructive fixed-environment RustFS pool lifecycle validation."""
+
+from __future__ import annotations
+
+import argparse
+import concurrent.futures
+import hashlib
+import json
+import os
+import secrets
+import shutil
+import subprocess
+import sys
+import tempfile
+import threading
+import time
+import urllib.error
+import urllib.parse
+import urllib.request
+import xml.etree.ElementTree as ET
+from dataclasses import asdict, dataclass, field
+from datetime import UTC, datetime
+from email.utils import format_datetime
+from pathlib import Path
+from typing import Any, Callable
+
+from case_plan import POOL_CASE_IDS as CASE_IDS, pool_harness_scored_cases
+from pool_helpers import active_endpoint_indices, active_fixture_bytes
+from pool_status import (
+ PoolStatusError,
+ assert_pool_parity,
+ canonical_pool,
+ decommission_active,
+ decommission_active_or_completed,
+ decommission_canceled,
+ decommission_cleared,
+ decommission_complete,
+ decommission_entry,
+ decommission_multipart_blocked,
+ multipart_drain_failure_records,
+ pool_entries,
+ rc_failure_is_retryable,
+ rebalance_active,
+ rebalance_active_or_completed,
+ rebalance_complete,
+ rebalance_entries,
+ rebalance_stopped,
+ validate_pool_inventory,
+)
+from rc_binary import resolve_rc_binary
+
+try:
+ import boto3
+ import botocore
+ from botocore.auth import SigV4Auth
+ from botocore.awsrequest import AWSRequest
+ from botocore.config import Config
+ from botocore.credentials import Credentials
+ from botocore.exceptions import (
+ ClientError,
+ ConnectTimeoutError,
+ EndpointConnectionError,
+ ReadTimeoutError,
+ )
+except ImportError as exc: # pragma: no cover - dependency guidance
+ raise SystemExit(
+ "boto3 is required for pool validation"
+ ) from exc
+
+
+ADMIN_PREFIX = "/rustfs/admin/v3"
+INFRA_EXCEPTIONS = (ConnectTimeoutError, EndpointConnectionError, ReadTimeoutError)
+INFRA_ERROR_CODES = {
+ "AccessDenied",
+ "InvalidAccessKeyId",
+ "InvalidToken",
+ "RequestTimeout",
+ "ServiceUnavailable",
+ "SignatureDoesNotMatch",
+ "SlowDown",
+}
+TRANSIENT_S3_CODES = {"InternalError", "RequestTimeout", "ServiceUnavailable", "SlowDown"}
+
+
+def utc_now() -> str:
+ return datetime.now(UTC).isoformat().replace("+00:00", "Z")
+
+
+def json_default(value: Any) -> Any:
+ if isinstance(value, datetime):
+ return value.astimezone(UTC).isoformat().replace("+00:00", "Z")
+ if isinstance(value, bytes):
+ return {"bytes": len(value), "sha256": hashlib.sha256(value).hexdigest()}
+ return str(value)
+
+
+def safe_name(value: str) -> str:
+ return "".join(character if character.isalnum() or character in "-." else "-" for character in value)
+
+
+def deterministic_payload(seed: str, size: int) -> bytes:
+ block = hashlib.sha256(seed.encode("utf-8")).digest()
+ return (block * ((size + len(block) - 1) // len(block)))[:size]
+
+
+@dataclass
+class CaseResult:
+ id: str
+ state: str
+ message: str
+ started_at: str
+ finished_at: str
+ duration_seconds: float
+ evidence: list[str] = field(default_factory=list)
+
+
+class EventRecorder:
+ def __init__(self, path: Path) -> None:
+ self.path = path
+ self.lock = threading.Lock()
+ self.path.parent.mkdir(parents=True, exist_ok=True)
+
+ def record(self, event: str, **details: Any) -> None:
+ item = {"timestamp": utc_now(), "event": event, "details": details}
+ with self.lock:
+ with self.path.open("a", encoding="utf-8") as handle:
+ handle.write(json.dumps(item, default=json_default, sort_keys=True) + "\n")
+
+
+class PoolHarness:
+ def __init__(self, args: argparse.Namespace) -> None:
+ args.endpoint = args.endpoint.rstrip("/")
+ args.node_endpoint = [endpoint.rstrip("/") for endpoint in args.node_endpoint]
+ self.args = args
+ self.evidence_dir = args.evidence_dir.resolve()
+ self.case_dir = self.evidence_dir / "cases"
+ self.case_dir.mkdir(parents=True, exist_ok=True)
+ self.events = EventRecorder(self.evidence_dir / "events.jsonl")
+ self.results: list[CaseResult] = []
+ self.created_buckets: set[str] = set()
+ self.current_layout = "initial"
+ self.isolation_failure: dict[str, str] | None = None
+ self.cleanup_outcome_value: dict[str, Any] = {
+ "attempted": False,
+ "state": "preserved",
+ "message": "physical cleanup has not run",
+ }
+ self.rc_home = Path(tempfile.mkdtemp(prefix="rustfs-pool-rc-"))
+ self.rc_env = os.environ.copy()
+ self.rc_env["RC_CONFIG_DIR"] = str(self.rc_home)
+ self.rc_bin = args.rc_bin
+ self.rc_version = "unavailable"
+ self.rc_binary_sha256 = "unavailable"
+ self.expected_rc_binary_sha256 = os.environ.get(
+ "RUSTFS_EXPECTED_RC_SHA256", ""
+ ).lower()
+ self.rc_alias = f"pool-{safe_name(args.run_id)[-16:]}"
+ self.initial_endpoint = args.endpoint
+ self.rc_configured = False
+ self.secrets = {args.access_key, args.secret_key}
+ self.client = self.s3_client(args.endpoint)
+ self.node_clients = {
+ endpoint: self.s3_client(endpoint) for endpoint in args.node_endpoint
+ }
+
+ def close(self) -> None:
+ shutil.rmtree(self.rc_home, ignore_errors=True)
+
+ def redact(self, value: str) -> str:
+ result = value
+ for secret in sorted((item for item in self.secrets if item), key=len, reverse=True):
+ result = result.replace(secret, "[REDACTED]")
+ return result
+
+ def redact_document(self, value: Any) -> Any:
+ serialized = json.dumps(value, default=json_default, sort_keys=True)
+ return json.loads(self.redact(serialized))
+
+ def evidence_path(self, case_id: str, name: str) -> Path:
+ directory = self.case_dir / safe_name(case_id)
+ directory.mkdir(parents=True, exist_ok=True)
+ return directory / name
+
+ def save_json(self, case_id: str, name: str, value: Any) -> str:
+ path = self.evidence_path(case_id, name)
+ text = json.dumps(value, default=json_default, indent=2, sort_keys=True) + "\n"
+ path.write_text(self.redact(text), encoding="utf-8")
+ return str(path.relative_to(self.evidence_dir))
+
+ def save_text(self, case_id: str, name: str, value: str) -> str:
+ path = self.evidence_path(case_id, name)
+ path.write_text(self.redact(value), encoding="utf-8")
+ return str(path.relative_to(self.evidence_dir))
+
+ def s3_client(self, endpoint: str) -> Any:
+ return boto3.client(
+ "s3",
+ endpoint_url=endpoint,
+ aws_access_key_id=self.args.access_key,
+ aws_secret_access_key=self.args.secret_key,
+ region_name=self.args.region,
+ config=Config(
+ signature_version="s3v4",
+ s3={"addressing_style": "path"},
+ retries={"max_attempts": 3, "mode": "standard"},
+ connect_timeout=5,
+ read_timeout=60,
+ ),
+ )
+
+ def configure_rc(self) -> None:
+ requested = self.rc_bin
+ try:
+ self.rc_bin, self.rc_binary_sha256 = resolve_rc_binary(requested)
+ except FileNotFoundError as exc:
+ raise RuntimeError(f"required rc binary not found: {requested}") from exc
+ version = subprocess.run(
+ [self.rc_bin, "--version"],
+ env=self.rc_env,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True,
+ check=False,
+ )
+ if version.returncode != 0:
+ raise RuntimeError(
+ f"rc --version failed with exit {version.returncode}: {version.stderr.strip()}"
+ )
+ self.rc_version = self.redact((version.stdout or version.stderr).strip())
+ if (
+ self.expected_rc_binary_sha256
+ and self.rc_binary_sha256.lower() != self.expected_rc_binary_sha256
+ ):
+ raise RuntimeError(
+ "resolved rc binary SHA-256 differs from RUSTFS_EXPECTED_RC_SHA256"
+ )
+ self.set_rc_alias(self.args.endpoint)
+ self.rc_configured = True
+ self.events.record(
+ "rc.client.identified",
+ binaryPath=self.rc_bin,
+ version=self.rc_version,
+ binarySha256=self.rc_binary_sha256,
+ )
+
+ def set_rc_alias(self, endpoint: str) -> None:
+ command = [
+ self.rc_bin,
+ "--json",
+ "--no-color",
+ "alias",
+ "set",
+ self.rc_alias,
+ endpoint,
+ self.args.access_key,
+ self.args.secret_key,
+ "--bucket-lookup",
+ "path",
+ ]
+ result = subprocess.run(
+ command,
+ env=self.rc_env,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.PIPE,
+ text=True,
+ check=False,
+ )
+ if result.returncode != 0:
+ raise RuntimeError(f"rc alias setup failed: {self.redact(result.stderr.strip())}")
+ self.events.record(
+ "rc.alias.configured",
+ alias=self.rc_alias,
+ endpoint=endpoint,
+ )
+
+ def control_endpoint(self, layout: str) -> str:
+ if self.initial_endpoint not in self.args.node_endpoint:
+ return self.initial_endpoint
+ active = active_endpoint_indices(len(self.args.node_endpoint), layout)
+ active_endpoints = tuple(self.args.node_endpoint[index] for index in active)
+ if self.initial_endpoint in active_endpoints:
+ return self.initial_endpoint
+ return active_endpoints[0]
+
+ def retained_pool_endpoints(self) -> tuple[str, ...]:
+ indices = active_endpoint_indices(len(self.args.node_endpoint), "retired")
+ return tuple(self.args.node_endpoint[index] for index in indices)
+
+ def retained_pool_client(self) -> Any:
+ return self.node_clients[self.retained_pool_endpoints()[0]]
+
+ def retire_decommissioned_pool(self, case_id: str) -> Any:
+ if self.current_layout != "expanded":
+ raise AssertionError("pool retirement requires the expanded topology")
+ self.wait_decommission(case_id, 0, "complete", label="before-retirement")
+ expanded, _ = self.pool_snapshot(
+ case_id, "before-retired-config-removal", 2, require_active=False
+ )
+ pools = {item["id"]: item for item in validate_pool_inventory(expanded, 2, require_active=False)}
+ if set(pools) != {0, 1}:
+ raise AssertionError("pool retirement requires the original pool identities")
+ if (pools[0]["status"].lower() != "decommissioned"
+ or pools[0]["decommissionStatus"].lower() != "complete"
+ or pools[1]["status"].lower() != "active"):
+ raise AssertionError("pool retirement requires a completed source and an active retained pool")
+ retained_cmdline = pools[1]["cmdline"]
+ self.census(case_id, "before-retired-config-removal")
+ self.apply_topology(case_id, "retired")
+ remaining, _ = self.pool_snapshot(case_id, "after-retired-config-removal", 1)
+ if canonical_pool(pool_entries(remaining)[0])["cmdline"] != retained_cmdline:
+ raise AssertionError("retired topology does not contain exactly the retained pool")
+ self.census(case_id, "after-retired-config-removal")
+ return self.retained_pool_client()
+
+ def select_control_endpoint(self, layout: str) -> None:
+ endpoint = self.control_endpoint(layout)
+ self.args.endpoint = endpoint
+ self.client = self.s3_client(endpoint)
+ if self.rc_configured:
+ self.set_rc_alias(endpoint)
+
+ def run_rc(
+ self,
+ case_id: str,
+ name: str,
+ arguments: list[str],
+ *,
+ expect_success: bool = True,
+ expected_error_code: str | None = None,
+ ) -> subprocess.CompletedProcess[str]:
+ if not expect_success and expected_error_code not in {
+ "InvalidArgument", "InvalidRequest", "OperationAborted", "NotImplemented"
+ }:
+ raise AssertionError(f"rc {name} requires an expected server error for a negative test")
+ if expect_success and expected_error_code is not None:
+ raise AssertionError(f"rc {name} cannot expect both success and a server error")
+ command = [self.rc_bin, "--json", "--no-color", *arguments]
+ result = subprocess.run(
+ command,
+ env=self.rc_env,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True,
+ check=False,
+ )
+ self.save_json(
+ case_id,
+ f"{name}.json",
+ {
+ "command": [
+ "[REDACTED]" if token in self.secrets else token
+ for token in command
+ ],
+ "exitCode": result.returncode,
+ "stdout": self.redact(result.stdout),
+ "stderr": self.redact(result.stderr),
+ },
+ )
+ if expect_success and result.returncode != 0:
+ raise AssertionError(
+ f"rc {name} exited {result.returncode}: {self.redact(result.stderr.strip())}"
+ )
+ if not expect_success and result.returncode == 0:
+ raise AssertionError(f"rc {name} unexpectedly succeeded")
+ if not expect_success:
+ try:
+ document = json.loads(result.stderr)
+ except json.JSONDecodeError as exc:
+ raise AssertionError(f"rc {name} did not return a structured server rejection") from exc
+ details = document.get("details") if isinstance(document, dict) else None
+ message = document.get("error") if isinstance(document, dict) else None
+ if not isinstance(details, dict) or details.get("retryable") is not False or not isinstance(message, str):
+ raise AssertionError(f"rc {name} did not return a non-retryable server rejection")
+ xml_start = message.find("")
+ if xml_start >= 0:
+ error_xml = message[xml_start:]
+ elif (
+ expected_error_code == "NotImplemented"
+ and arguments == ["admin", "rebalance", "start", self.rc_alias]
+ and details.get("type") == "unsupported_feature"
+ and "Unsupported feature:" in message
+ ):
+ # rc strips the XML code from NotImplemented. Confirm the
+ # single-pool server guard directly, not merely CLI capability.
+ status, headers, body = self.signed_request(
+ self.args.endpoint, "POST", f"{ADMIN_PREFIX}/rebalance/start"
+ )
+ error_xml = body.decode("utf-8", errors="replace")
+ self.save_json(case_id, f"{name}-server-rejection.json", {
+ "status": status, "requestId": headers.get("x-amz-request-id"),
+ "body": self.redact(error_xml),
+ })
+ if status != 501:
+ raise AssertionError(f"rc {name} server proof returned HTTP {status}, expected 501")
+ else:
+ raise AssertionError(f"rc {name} did not retain its expected server error code")
+ try:
+ root = ET.fromstring(error_xml)
+ except ET.ParseError as exc:
+ raise AssertionError(f"rc {name} returned malformed server error XML") from exc
+ codes = root.findall("Code") if root.tag == "Error" else []
+ if len(codes) != 1 or codes[0].text != expected_error_code:
+ observed = [element.text for element in codes]
+ raise AssertionError(f"rc {name} returned server errors {observed!r}, expected {expected_error_code}")
+ return result
+
+ @staticmethod
+ def parse_json_output(result: subprocess.CompletedProcess[str], operation: str) -> Any:
+ try:
+ return json.loads(result.stdout)
+ except json.JSONDecodeError as exc:
+ raise AssertionError(f"{operation} did not return JSON") from exc
+
+ def assert_no_secrets(self, document: Any, label: str) -> None:
+ serialized = json.dumps(document, default=json_default, sort_keys=True)
+ exposed = [secret for secret in self.secrets if secret and secret in serialized]
+ if exposed:
+ raise AssertionError(f"{label} exposed configured credentials")
+
+ def signed_request(
+ self,
+ endpoint: str,
+ method: str,
+ path: str,
+ *,
+ query: str = "",
+ body: bytes | None = None,
+ timeout: float = 60,
+ ) -> tuple[int, dict[str, str], bytes]:
+ url = endpoint.rstrip("/") + urllib.parse.quote(path, safe="/-._~")
+ if query:
+ url += f"?{query}"
+ payload = body or b""
+ headers = {"x-amz-content-sha256": hashlib.sha256(payload).hexdigest()}
+ request = AWSRequest(method=method, url=url, data=body, headers=headers)
+ SigV4Auth(
+ Credentials(self.args.access_key, self.args.secret_key),
+ "s3",
+ self.args.region,
+ ).add_auth(request)
+ outgoing = urllib.request.Request(
+ url,
+ data=body,
+ headers=dict(request.headers.items()),
+ method=method,
+ )
+ opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
+ try:
+ with opener.open(outgoing, timeout=timeout) as response:
+ return response.status, dict(response.headers.items()), response.read()
+ except urllib.error.HTTPError as exc:
+ return exc.code, dict(exc.headers.items()), exc.read()
+
+ def admin_json(
+ self,
+ case_id: str,
+ name: str,
+ subpath: str,
+ *,
+ endpoint: str | None = None,
+ query: str = "",
+ expected_status: int = 200,
+ ) -> Any:
+ status, headers, body = self.signed_request(
+ endpoint or self.args.endpoint,
+ "GET",
+ f"{ADMIN_PREFIX}/{subpath}",
+ query=query,
+ )
+ try:
+ parsed = json.loads(body) if body.strip() else {}
+ except json.JSONDecodeError:
+ parsed = {"raw": body.decode("utf-8", errors="replace")}
+ self.save_json(
+ case_id,
+ f"{name}.json",
+ {
+ "status": status,
+ "requestId": headers.get("x-amz-request-id"),
+ "body": parsed,
+ },
+ )
+ if status != expected_status:
+ raise AssertionError(
+ f"GET {subpath} returned HTTP {status}, expected {expected_status}: {parsed!r}"
+ )
+ return parsed
+
+ def hook(
+ self,
+ case_id: str,
+ name: str,
+ raw: str | None,
+ extra: tuple[str, ...] = (),
+ *,
+ timeout: float | None = None,
+ ) -> str:
+ if not raw:
+ raise RuntimeError(f"{name} requires a driver hook")
+ try:
+ command = json.loads(raw)
+ except json.JSONDecodeError as exc:
+ raise RuntimeError(f"{name} hook is not valid JSON") from exc
+ if not isinstance(command, list) or not command or not all(
+ isinstance(token, str) and token for token in command
+ ):
+ raise RuntimeError(f"{name} hook must be a JSON command array")
+ result = subprocess.run(
+ [*command, *extra],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ check=False,
+ timeout=timeout,
+ )
+ reference = self.save_text(case_id, f"{name}.log", result.stdout)
+ self.events.record(
+ "driver.hook",
+ case=case_id,
+ hook=name,
+ exitCode=result.returncode,
+ arguments=list(extra),
+ )
+ if result.returncode != 0:
+ raise RuntimeError(f"{name} hook exited {result.returncode}")
+ return reference
+
+ def reset_case_environment(self, case_id: str) -> None:
+ self.hook(case_id, "physical-reset-initial", self.args.reset_hook)
+ self.current_layout = "initial"
+ self.select_control_endpoint("initial")
+ self.wait_s3(case_id, self.client, "stable initial endpoint")
+
+ def apply_topology(self, case_id: str, layout: str) -> None:
+ self.hook(
+ case_id,
+ f"topology-{layout}",
+ self.args.topology_hook,
+ ("--layout", layout),
+ )
+ self.current_layout = layout
+ self.select_control_endpoint(layout)
+ self.wait_s3(case_id, self.client, f"stable {layout} endpoint")
+ indices = active_endpoint_indices(len(self.args.node_endpoint), layout)
+ for index in indices:
+ endpoint = self.args.node_endpoint[index]
+ client = self.node_clients[endpoint]
+ self.wait_s3(case_id, client, f"node {index} {layout} endpoint")
+
+ def restart_topology(self, case_id: str, layout: str) -> None:
+ self.hook(
+ case_id,
+ f"restart-{layout}",
+ self.args.restart_hook,
+ ("--layout", layout),
+ )
+ self.current_layout = layout
+ self.select_control_endpoint(layout)
+ self.wait_s3(case_id, self.client, f"stable endpoint after {layout} restart")
+
+ def census(self, case_id: str, phase: str) -> str:
+ return self.hook(
+ case_id,
+ f"physical-census-{safe_name(phase)}",
+ self.args.census_hook,
+ ("--phase", phase),
+ )
+
+ def capture_diagnostics(self, case_id: str, phase: str) -> None:
+ raw = getattr(self.args, "diagnostic_hook", None)
+ if not raw:
+ return
+ errors = []
+ for index, endpoint in enumerate(self.args.node_endpoint):
+ try:
+ self.admin_json(
+ case_id,
+ f"diagnostic-{safe_name(phase)}-node-{index}-decommission",
+ "decommission/status",
+ endpoint=endpoint,
+ )
+ except Exception as exc:
+ errors.append(self.redact(f"node {index}: {type(exc).__name__}: {exc}"))
+ try:
+ self.hook(
+ case_id,
+ f"diagnostic-{safe_name(phase)}-metadata",
+ raw,
+ ("--phase", phase),
+ timeout=60,
+ )
+ except Exception as exc:
+ errors.append(self.redact(f"metadata: {type(exc).__name__}: {exc}"))
+ self.save_json(
+ case_id,
+ f"diagnostic-{safe_name(phase)}-collection.json",
+ {"phase": phase, "errors": errors, "complete": not errors},
+ )
+
+ def wait_canceled_quiescence(self, case_id: str, pool_id: int, *, label: str) -> None:
+ stable_since: float | None = None
+ prior: tuple[int, int] | None = None
+ history: list[dict[str, Any]] = []
+
+ def probe() -> tuple[bool, Any]:
+ nonlocal stable_since, prior
+ counters = []
+ for endpoint in self.args.node_endpoint:
+ try:
+ status, _, body = self.signed_request(
+ endpoint, "GET", f"{ADMIN_PREFIX}/decommission/status"
+ )
+ except INFRA_EXCEPTIONS as exc:
+ # A gap in observations is not evidence of quiescence.
+ # Preserve wait_until's infrastructure classification.
+ stable_since = None
+ history.append({
+ "capturedAt": utc_now(), "endpoint": endpoint,
+ "error": self.redact(f"{type(exc).__name__}: {exc}"),
+ })
+ raise
+ if status != 200:
+ stable_since = None
+ return False, {"endpoint": endpoint, "httpStatus": status}
+ document = json.loads(body)
+ if not decommission_canceled(document, pool_id):
+ stable_since = None
+ return False, document
+ counters.append(self.decommission_counters(document, pool_id))
+ now = time.monotonic()
+ sample = {"capturedAt": utc_now(), "counters": counters}
+ history.append(sample)
+ if len(set(counters)) != 1:
+ stable_since = None
+ return False, sample
+ current = counters[0]
+ if current != prior or stable_since is None:
+ prior, stable_since = current, now
+ return now - stable_since >= 3.0, sample
+
+ try:
+ self.wait_until(
+ case_id,
+ f"decommission pool {pool_id} canceled and quiescent on all nodes",
+ probe,
+ timeout=min(self.args.timeout, 30),
+ )
+ finally:
+ self.save_json(case_id, f"{safe_name(label)}-quiescence-samples.json", history)
+
+ def wait_until(
+ self,
+ case_id: str,
+ description: str,
+ probe: Callable[[], tuple[bool, Any]],
+ *,
+ timeout: float | None = None,
+ ) -> Any:
+ deadline = time.monotonic() + (timeout or self.args.timeout)
+ attempts = 0
+ last: Any = None
+ last_was_infrastructure = False
+ while time.monotonic() < deadline:
+ attempts += 1
+ try:
+ complete, last = probe()
+ last_was_infrastructure = False
+ except INFRA_EXCEPTIONS as exc:
+ complete, last = False, {"error": f"{type(exc).__name__}: {exc}"}
+ last_was_infrastructure = True
+ if complete:
+ self.events.record(
+ "poll.complete",
+ case=case_id,
+ description=description,
+ attempts=attempts,
+ detail=self.redact_document(last),
+ )
+ return last
+ time.sleep(self.args.poll_interval)
+ self.save_json(
+ case_id,
+ f"poll-timeout-{safe_name(description)}.json",
+ {"description": description, "attempts": attempts, "last": last},
+ )
+ error_type = RuntimeError if last_was_infrastructure else AssertionError
+ raise error_type(f"timed out waiting for {description}; last={last!r}")
+
+ def wait_s3(self, case_id: str, client: Any, description: str) -> None:
+ def probe() -> tuple[bool, Any]:
+ try:
+ response = client.list_buckets()
+ return True, {
+ "requestId": response.get("ResponseMetadata", {}).get("RequestId")
+ }
+ except INFRA_EXCEPTIONS:
+ raise
+ except ClientError as exc:
+ code = exc.response.get("Error", {}).get("Code", "")
+ if code not in TRANSIENT_S3_CODES:
+ raise
+ return False, {"error": f"ClientError[{code}]: {exc}"[:500]}
+
+ self.wait_until(case_id, description, probe, timeout=min(self.args.timeout, 180))
+
+ def bucket_name(self, case_id: str) -> str:
+ digest = hashlib.sha256(f"{self.args.run_id}:{case_id}".encode()).hexdigest()[:14]
+ return f"rv-pool-{digest}"
+
+ def create_bucket(self, case_id: str) -> str:
+ bucket = self.bucket_name(case_id)
+ self.client.create_bucket(Bucket=bucket)
+ self.created_buckets.add(bucket)
+ self.events.record("bucket.created", case=case_id, bucket=bucket)
+ return bucket
+
+ @staticmethod
+ def delete_batches(client: Any, bucket: str, items: list[dict[str, str]]) -> None:
+ for offset in range(0, len(items), 1000):
+ batch = items[offset : offset + 1000]
+ try:
+ response = client.delete_objects(
+ Bucket=bucket,
+ Delete={"Objects": batch, "Quiet": True},
+ )
+ except ClientError as exc:
+ code = exc.response.get("Error", {}).get("Code", "")
+ if code not in {"MethodNotAllowed", "NotImplemented", "XNotImplemented"}:
+ raise
+ # Some S3-compatible candidates do not implement multi-delete
+ # for explicit version IDs. Individual deletes are the portable
+ # cleanup fallback and retain the same isolation guarantee.
+ for item in batch:
+ client.delete_object(Bucket=bucket, **item)
+ continue
+ if response.get("Errors"):
+ raise RuntimeError(f"delete_objects failed: {response['Errors'][:5]!r}")
+
+ def cleanup_bucket(self, bucket: str) -> None:
+ deadline = time.monotonic() + min(self.args.timeout, 120)
+ while True:
+ try:
+ uploads: list[dict[str, Any]] = []
+ for page in self.client.get_paginator("list_multipart_uploads").paginate(Bucket=bucket):
+ uploads.extend(page.get("Uploads", []))
+ for upload in uploads:
+ self.client.abort_multipart_upload(
+ Bucket=bucket,
+ Key=upload["Key"],
+ UploadId=upload["UploadId"],
+ )
+ versions: list[dict[str, str]] = []
+ for page in self.client.get_paginator("list_object_versions").paginate(Bucket=bucket):
+ versions.extend(
+ {"Key": item["Key"], "VersionId": item["VersionId"]}
+ for group in ("Versions", "DeleteMarkers")
+ for item in page.get(group, [])
+ )
+ if versions:
+ self.delete_batches(self.client, bucket, versions)
+ objects: list[dict[str, str]] = []
+ for page in self.client.get_paginator("list_objects_v2").paginate(Bucket=bucket):
+ objects.extend({"Key": item["Key"]} for item in page.get("Contents", []))
+ if objects:
+ self.delete_batches(self.client, bucket, objects)
+ self.client.delete_bucket(Bucket=bucket)
+ self.created_buckets.discard(bucket)
+ self.events.record("bucket.cleanup.complete", bucket=bucket)
+ return
+ except INFRA_EXCEPTIONS as exc:
+ if time.monotonic() >= deadline:
+ raise
+ self.events.record(
+ "bucket.cleanup.retry",
+ bucket=bucket,
+ errorType=type(exc).__name__,
+ )
+ time.sleep(self.args.poll_interval)
+ except ClientError as exc:
+ code = exc.response.get("Error", {}).get("Code", "")
+ if code == "NoSuchBucket":
+ self.created_buckets.discard(bucket)
+ return
+ retryable = code == "BucketNotEmpty" or code in TRANSIENT_S3_CODES
+ if not retryable or time.monotonic() >= deadline:
+ raise
+ self.events.record(
+ "bucket.cleanup.retry",
+ bucket=bucket,
+ errorCode=code,
+ )
+ time.sleep(self.args.poll_interval)
+
+ def s3_retrying(
+ self,
+ operation: str,
+ function: Callable[[], Any],
+ *,
+ timeout: float | None = None,
+ ) -> Any:
+ deadline = time.monotonic() + min(timeout or self.args.timeout, 120)
+ attempt = 0
+ while True:
+ attempt += 1
+ try:
+ return function()
+ except (*INFRA_EXCEPTIONS, ClientError) as exc:
+ retryable = isinstance(exc, INFRA_EXCEPTIONS)
+ error_code: str | None = None
+ if isinstance(exc, ClientError):
+ error_code = exc.response.get("Error", {}).get("Code")
+ retryable = error_code in TRANSIENT_S3_CODES
+ if not retryable or time.monotonic() >= deadline:
+ response_metadata = (
+ exc.response.get("ResponseMetadata", {})
+ if isinstance(exc, ClientError) else {}
+ )
+ self.events.record(
+ "s3.operation.exhausted" if retryable else "s3.operation.failed",
+ operation=operation,
+ attempt=attempt,
+ errorType=type(exc).__name__,
+ errorCode=error_code,
+ httpStatus=response_metadata.get("HTTPStatusCode"),
+ requestId=response_metadata.get("RequestId"),
+ sdkRetryAttempts=response_metadata.get("RetryAttempts"),
+ )
+ raise
+ self.events.record(
+ "s3.operation.retry",
+ operation=operation,
+ attempt=attempt,
+ errorType=type(exc).__name__,
+ errorCode=error_code,
+ )
+ time.sleep(self.args.poll_interval)
+
+ def put_retrying(
+ self,
+ client: Any,
+ bucket: str,
+ key: str,
+ body: bytes,
+ metadata: dict[str, str],
+ ) -> dict[str, Any]:
+ return self.s3_retrying(
+ f"PUT s3://{bucket}/{key}",
+ lambda: client.put_object(
+ Bucket=bucket,
+ Key=key,
+ Body=body,
+ Metadata=metadata,
+ ),
+ )
+
+ def put_inventory(
+ self,
+ case_id: str,
+ bucket: str,
+ *,
+ count: int | None = None,
+ size: int | None = None,
+ prefix: str = "baseline",
+ ) -> list[dict[str, Any]]:
+ count = count or self.args.fixture_objects
+ size = size or self.args.fixture_bytes
+
+ def put(index: int) -> dict[str, Any]:
+ key = f"{prefix}/{index:05d}.bin"
+ actual_size = size if index >= 3 else (0, 1, 4097)[index]
+ body = deterministic_payload(f"{case_id}:{key}", actual_size)
+ metadata = {"case": safe_name(case_id), "ordinal": str(index)}
+ response = self.put_retrying(self.client, bucket, key, body, metadata)
+ return {
+ "key": key,
+ "size": actual_size,
+ "sha256": hashlib.sha256(body).hexdigest(),
+ "etag": str(response.get("ETag", "")).strip('"'),
+ "metadata": metadata,
+ "versionId": response.get("VersionId"),
+ }
+
+ with concurrent.futures.ThreadPoolExecutor(max_workers=self.args.workers) as pool:
+ inventory = list(pool.map(put, range(count)))
+ inventory.sort(key=lambda item: item["key"])
+ self.save_json(case_id, f"{prefix}-manifest.json", inventory)
+ self.events.record(
+ "fixture.inventory.created",
+ case=case_id,
+ bucket=bucket,
+ objects=len(inventory),
+ bytes=sum(item["size"] for item in inventory),
+ )
+ return inventory
+
+ def assert_inventory(
+ self,
+ case_id: str,
+ client: Any,
+ bucket: str,
+ inventory: list[dict[str, Any]],
+ label: str,
+ ) -> None:
+ def check(item: dict[str, Any]) -> dict[str, Any]:
+ def fetch() -> tuple[dict[str, Any], bytes]:
+ response = client.get_object(Bucket=bucket, Key=item["key"])
+ return response, response["Body"].read()
+
+ response, body = self.s3_retrying(
+ f"GET integrity object for {label}", fetch
+ )
+ observed = {
+ "key": item["key"],
+ "size": len(body),
+ "sha256": hashlib.sha256(body).hexdigest(),
+ "etag": str(response.get("ETag", "")).strip('"'),
+ "metadata": response.get("Metadata", {}),
+ "versionId": response.get("VersionId"),
+ }
+ for field in ("size", "sha256", "etag", "metadata"):
+ if observed[field] != item[field]:
+ raise AssertionError(
+ f"{label} {item['key']} {field} mismatch: expected "
+ f"{item[field]!r}, got {observed[field]!r}"
+ )
+ if item.get("versionId") is not None and observed["versionId"] != item["versionId"]:
+ raise AssertionError(
+ f"{label} {item['key']} version ID changed: "
+ f"{item['versionId']!r} -> {observed['versionId']!r}"
+ )
+ return observed
+
+ with concurrent.futures.ThreadPoolExecutor(max_workers=self.args.workers) as pool:
+ observed = list(pool.map(check, inventory))
+
+ def list_keys() -> set[str]:
+ return {
+ item["Key"]
+ for page in client.get_paginator("list_objects_v2").paginate(Bucket=bucket)
+ for item in page.get("Contents", [])
+ }
+
+ listed = self.s3_retrying(f"LIST integrity objects for {label}", list_keys)
+ missing = sorted(item["key"] for item in inventory if item["key"] not in listed)
+ if missing:
+ raise AssertionError(f"{label} listing omitted keys: {missing[:10]!r}")
+ self.save_json(case_id, f"integrity-{safe_name(label)}.json", observed)
+
+ def pool_snapshot(
+ self,
+ case_id: str,
+ label: str,
+ expected_count: int,
+ *,
+ require_active: bool = True,
+ ) -> tuple[Any, Any]:
+ deadline = time.monotonic() + min(self.args.active_timeout, 10)
+ attempt = 0
+ while True:
+ attempt += 1
+ suffix = "" if require_active else f"-{attempt}"
+ api = self.admin_json(
+ case_id, f"{label}{suffix}-admin-pool-list", "pools/list"
+ )
+ rc_result = self.run_rc(
+ case_id,
+ f"{label}{suffix}-rc-pool-list",
+ ["admin", "pool", "list", self.rc_alias],
+ )
+ rc = self.parse_json_output(rc_result, "rc admin pool list")
+ self.assert_no_secrets(api, f"{label} Admin pool response")
+ self.assert_no_secrets(rc, f"{label} rc pool response")
+ try:
+ assert_pool_parity(
+ rc, api, expected_count, require_active=require_active
+ )
+ except PoolStatusError:
+ if time.monotonic() >= deadline:
+ raise
+ time.sleep(self.args.poll_interval)
+ continue
+ return api, rc
+
+ def assert_pool_specific_status(self, case_id: str, expected_count: int) -> None:
+ api_list, _ = self.pool_snapshot(case_id, "specific-source", expected_count)
+ initial = validate_pool_inventory(api_list, expected_count)
+ stable_fields = (
+ "id", "cmdline", "status", "decommissionStatus", "rebalanceStatus", "totalSize",
+ )
+
+ def stable_values(entry: dict[str, Any]) -> tuple[Any, ...]:
+ return tuple(entry[field] for field in stable_fields)
+
+ for expected in initial:
+ pool_id = expected["id"]
+ deadline = time.monotonic() + min(self.args.active_timeout, 10)
+ attempt = 0
+ while True:
+ attempt += 1
+ suffix = "" if attempt == 1 else f"-{attempt}"
+ observed = []
+ for selector, by_id, label in (
+ (str(pool_id), True, f"pool-{pool_id}-status"),
+ (expected["cmdline"], False, f"pool-{pool_id}-expression-status"),
+ ):
+ query = urllib.parse.urlencode({"pool": selector, "by-id": str(by_id).lower()})
+ api = self.admin_json(
+ case_id, f"admin-{label}{suffix}", "pools/status", query=query,
+ )
+ self.assert_no_secrets(api, f"{label} Admin status")
+ observed.extend(validate_pool_inventory(api, 1))
+ arguments = ["admin", "pool", "status", self.rc_alias, selector]
+ if by_id:
+ arguments.append("--by-id")
+ rc_result = self.run_rc(case_id, f"rc-{label}{suffix}", arguments)
+ rc = self.parse_json_output(rc_result, "rc admin pool status")
+ self.assert_no_secrets(rc, f"{label} rc status")
+ observed.extend(validate_pool_inventory(rc, 1))
+
+ if any(stable_values(item) != stable_values(expected) for item in observed):
+ raise AssertionError(
+ f"pool {pool_id} selector identity/state/total capacity disagrees with pool list: "
+ f"list={expected!r} observed={observed!r}"
+ )
+ if all(item == expected for item in observed):
+ break
+
+ # Disk usage can change between the sequential requests even
+ # without client writes. Reacquire the list and all selectors;
+ # never ignore capacity fields or accept a lasting mismatch.
+ remaining = deadline - time.monotonic()
+ if remaining <= 0:
+ raise AssertionError(
+ f"pool {pool_id} selector capacity snapshots did not converge: "
+ f"list={expected!r} observed={observed!r}"
+ )
+ self.events.record(
+ "pool.selector.capacity.resample", case=case_id, poolId=pool_id,
+ attempt=attempt, expected=expected, observed=observed,
+ )
+ time.sleep(min(self.args.poll_interval, remaining))
+ if time.monotonic() >= deadline:
+ raise AssertionError(f"pool {pool_id} selector capacity snapshots did not converge before deadline")
+ refreshed_api = self.admin_json(
+ case_id, f"specific-source-pool-{pool_id}-resample-{attempt + 1}-admin-pool-list",
+ "pools/list",
+ )
+ self.assert_no_secrets(refreshed_api, f"pool {pool_id} refreshed Admin pool list")
+ refreshed = validate_pool_inventory(refreshed_api, expected_count)
+ if tuple(map(stable_values, initial)) != tuple(map(stable_values, refreshed)):
+ raise AssertionError("pool inventory identity/state/total capacity changed during selector sampling")
+ expected = next(item for item in refreshed if item["id"] == pool_id)
+
+ def start_rebalance(self, case_id: str, *, command: str = "rebalance") -> str:
+ deadline = time.monotonic() + min(self.args.active_timeout, 60)
+ attempt = 0
+ while True:
+ attempt += 1
+ arguments = ["admin", command, "start", self.rc_alias]
+ result = subprocess.run(
+ [self.rc_bin, "--json", "--no-color", *arguments],
+ env=self.rc_env,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True,
+ check=False,
+ )
+ self.save_json(
+ case_id,
+ f"{command}-start-{attempt}.json",
+ {
+ "command": arguments,
+ "exitCode": result.returncode,
+ "stdout": result.stdout,
+ "stderr": result.stderr,
+ },
+ )
+ if result.returncode == 0:
+ document = self.parse_json_output(
+ result, f"rc admin {command} start"
+ )
+ operation_id = (
+ document.get("id") if isinstance(document, dict) else None
+ )
+ if not isinstance(operation_id, str) or not operation_id:
+ raise AssertionError(
+ f"{command} start omitted operation ID: {document!r}"
+ )
+ return operation_id
+ error_text = f"{result.stdout}\n{result.stderr}"
+ if (
+ rc_failure_is_retryable(result.stdout, result.stderr)
+ and time.monotonic() < deadline
+ ):
+ time.sleep(self.args.poll_interval)
+ continue
+ raise AssertionError(
+ f"{command} start failed: {self.redact(error_text.strip())}"
+ )
+
+ def rebalance_status(
+ self,
+ case_id: str,
+ operation_id: str,
+ label: str,
+ *,
+ allow_stopped: bool = False,
+ ) -> Any:
+ deadline = time.monotonic() + min(self.args.active_timeout, 10)
+ attempt = 0
+ last_states: tuple[Any, Any] | None = None
+ while True:
+ attempt += 1
+ api = self.admin_json(
+ case_id, f"{label}-{attempt}-admin", "rebalance/status"
+ )
+ rc_result = self.run_rc(
+ case_id,
+ f"{label}-{attempt}-rc",
+ ["admin", "rebalance", "status", self.rc_alias],
+ )
+ rc = self.parse_json_output(rc_result, "rc admin rebalance status")
+ self.assert_no_secrets(api, f"{label} Admin rebalance response")
+ self.assert_no_secrets(rc, f"{label} rc rebalance response")
+ api_pools = rebalance_entries(
+ api,
+ operation_id,
+ allow_stopped=allow_stopped,
+ )
+ rc_pools = rebalance_entries(
+ rc,
+ operation_id,
+ allow_stopped=allow_stopped,
+ )
+ api_states = tuple(
+ (item.get("id"), item.get("status")) for item in api_pools
+ )
+ rc_states = tuple(
+ (item.get("id"), item.get("status")) for item in rc_pools
+ )
+ if api_states == rc_states:
+ return api
+ last_states = (rc_states, api_states)
+ if time.monotonic() >= deadline:
+ raise AssertionError(
+ f"rc/Admin rebalance state did not converge: {last_states!r}"
+ )
+ time.sleep(self.args.poll_interval)
+
+ def wait_rebalance(
+ self,
+ case_id: str,
+ operation_id: str,
+ state: str,
+ *,
+ label: str | None = None,
+ allow_fast_completion: bool = False,
+ ) -> Any:
+ predicates: dict[str, Callable[[Any, str], bool]] = {
+ "active": (
+ rebalance_active_or_completed
+ if allow_fast_completion
+ else lambda document, expected: rebalance_active(
+ document, expected, require_progress=True
+ )
+ ),
+ "complete": rebalance_complete,
+ "stopped": rebalance_stopped,
+ }
+ artifact_label = label or state
+
+ def probe() -> tuple[bool, Any]:
+ status, _, body = self.signed_request(
+ self.args.endpoint, "GET", f"{ADMIN_PREFIX}/rebalance/status"
+ )
+ if status != 200:
+ return False, {"httpStatus": status, "body": body.decode(errors="replace")[:500]}
+ document = json.loads(body)
+ try:
+ matched = predicates[state](document, operation_id)
+ except PoolStatusError:
+ self.save_json(
+ case_id,
+ f"rebalance-{artifact_label}-failed-status.json",
+ document,
+ )
+ raise
+ return matched, document
+
+ result = self.wait_until(
+ case_id,
+ f"rebalance {operation_id} {state}",
+ probe,
+ timeout=self.args.active_timeout if state == "active" else self.args.timeout,
+ )
+ self.save_json(case_id, f"rebalance-{artifact_label}-final.json", result)
+ if (
+ state == "active"
+ and allow_fast_completion
+ and rebalance_complete(result, operation_id)
+ ):
+ self.events.record(
+ "rebalance.fast-completion",
+ case=case_id,
+ operationId=operation_id,
+ evidence="terminal status carried nonzero movement progress",
+ )
+ self.rebalance_status(
+ case_id,
+ operation_id,
+ f"rebalance-{artifact_label}-parity",
+ allow_stopped=state == "stopped",
+ )
+ if state in {"complete", "stopped"}:
+ inventory, _ = self.pool_snapshot(
+ case_id,
+ f"rebalance-{artifact_label}-pool-inventory",
+ 2,
+ )
+ observed = {
+ str(item.get("rebalanceStatus", "")).lower()
+ for item in pool_entries(inventory)
+ }
+ allowed = (
+ {"completed", "none"}
+ if state == "complete"
+ else {"stopped", "completed", "none"}
+ )
+ required = "completed" if state == "complete" else "stopped"
+ if not observed.issubset(allowed) or required not in observed:
+ raise AssertionError(
+ f"pool inventory disagrees with rebalance {state}: {observed}"
+ )
+ return result
+
+ def stop_rebalance(self, case_id: str, *, command: str = "rebalance") -> None:
+ self.run_rc(
+ case_id,
+ f"{command}-stop",
+ ["admin", command, "stop", self.rc_alias],
+ )
+
+ def start_decommission(self, case_id: str, pool_id: int = 0, *, label: str = "decommission-start") -> None:
+ self.capture_diagnostics(case_id, f"before-{label}")
+ deadline = time.monotonic() + min(self.args.active_timeout, 60)
+ attempt = 0
+ while True:
+ attempt += 1
+ command = [
+ "admin",
+ "decommission",
+ "start",
+ self.rc_alias,
+ str(pool_id),
+ "--by-id",
+ ]
+ result = subprocess.run(
+ [self.rc_bin, "--json", "--no-color", *command],
+ env=self.rc_env,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True,
+ check=False,
+ )
+ self.save_json(
+ case_id,
+ f"{label}-{attempt}.json",
+ {
+ "command": command,
+ "exitCode": result.returncode,
+ "stdout": result.stdout,
+ "stderr": result.stderr,
+ },
+ )
+ if result.returncode == 0:
+ return
+ error_text = f"{result.stdout}\n{result.stderr}"
+ if (
+ rc_failure_is_retryable(result.stdout, result.stderr)
+ and time.monotonic() < deadline
+ ):
+ time.sleep(self.args.poll_interval)
+ continue
+ raise AssertionError(
+ f"decommission start failed: {self.redact(error_text.strip())}"
+ )
+
+ def decommission_status(
+ self, case_id: str, pool_id: int, label: str, *,
+ predicate: Callable[[Any, int], bool] | None = None,
+ ) -> Any:
+ deadline = time.monotonic() + min(self.args.active_timeout, 10)
+ attempt = 0
+ fields = ("id", "cmdline", "status", "poolStatus")
+ last_states: tuple[Any, Any] | None = None
+ while True:
+ attempt += 1
+ api = self.admin_json(
+ case_id, f"{label}-{attempt}-admin", "decommission/status"
+ )
+ rc_result = self.run_rc(
+ case_id,
+ f"{label}-{attempt}-rc",
+ ["admin", "decommission", "status", self.rc_alias],
+ )
+ rc = self.parse_json_output(rc_result, "rc admin decommission status")
+ self.assert_no_secrets(api, f"{label} Admin decommission response")
+ self.assert_no_secrets(rc, f"{label} rc decommission response")
+ api_entry = decommission_entry(api, pool_id)
+ rc_entry = decommission_entry(rc, pool_id)
+ api_state = tuple(api_entry.get(field) for field in fields)
+ rc_state = tuple(rc_entry.get(field) for field in fields)
+ if api_state == rc_state:
+ if predicate is not None and not all(predicate(document, pool_id) for document in (api, rc)):
+ raise PoolStatusError("rc/Admin decommission details violate the expected state")
+ return api
+ last_states = (rc_state, api_state)
+ if time.monotonic() >= deadline:
+ raise AssertionError(
+ f"rc/Admin decommission state did not converge: {last_states!r}"
+ )
+ time.sleep(self.args.poll_interval)
+
+ def wait_decommission(
+ self,
+ case_id: str,
+ pool_id: int,
+ state: str,
+ *,
+ label: str | None = None,
+ allow_fast_completion: bool = False,
+ ) -> Any:
+ predicates: dict[str, Callable[[Any, int], bool]] = {
+ "active": (
+ decommission_active_or_completed
+ if allow_fast_completion
+ else lambda document, expected: decommission_active(
+ document, expected, require_progress=True
+ )
+ ),
+ "complete": decommission_complete,
+ "canceled": decommission_canceled,
+ "cleared": decommission_cleared,
+ }
+ artifact_label = label or state
+
+ def probe() -> tuple[bool, Any]:
+ status, _, body = self.signed_request(
+ self.args.endpoint, "GET", f"{ADMIN_PREFIX}/decommission/status"
+ )
+ if status != 200:
+ return False, {"httpStatus": status, "body": body.decode(errors="replace")[:500]}
+ document = json.loads(body)
+ try:
+ matched = predicates[state](document, pool_id)
+ except PoolStatusError:
+ self.save_json(
+ case_id,
+ f"decommission-{artifact_label}-failed-status.json",
+ document,
+ )
+ raise
+ return matched, document
+
+ result = self.wait_until(
+ case_id,
+ f"decommission pool {pool_id} {state}",
+ probe,
+ timeout=self.args.active_timeout if state == "active" else self.args.timeout,
+ )
+ self.save_json(
+ case_id, f"decommission-{artifact_label}-final.json", result
+ )
+ if (
+ state == "active"
+ and allow_fast_completion
+ and decommission_complete(result, pool_id)
+ ):
+ self.events.record(
+ "decommission.fast-completion",
+ case=case_id,
+ poolId=pool_id,
+ evidence="terminal status carried nonzero movement progress",
+ )
+ self.decommission_status(
+ case_id, pool_id, f"decommission-{artifact_label}-parity"
+ )
+ if state == "canceled":
+ self.wait_canceled_quiescence(case_id, pool_id, label=artifact_label)
+ result = self.decommission_status(
+ case_id, pool_id, f"decommission-{artifact_label}-quiescent"
+ )
+ if not decommission_canceled(result, pool_id):
+ raise AssertionError("decommission left canceled state after quiescence")
+ self.save_json(case_id, f"decommission-{artifact_label}-final.json", result)
+ if state == "active":
+ self.capture_diagnostics(case_id, f"decommission-{artifact_label}")
+ if state in {"complete", "canceled", "cleared"}:
+ expected_count = len(pool_entries(result))
+ require_active = state == "cleared"
+ inventory, _ = self.pool_snapshot(
+ case_id,
+ f"decommission-{artifact_label}-pool-inventory",
+ expected_count,
+ require_active=require_active,
+ )
+ matches = [
+ item for item in pool_entries(inventory) if item.get("id") == pool_id
+ ]
+ if len(matches) != 1:
+ raise AssertionError(
+ f"pool inventory omitted decommission pool {pool_id}"
+ )
+ entry = matches[0]
+ expected_states = {
+ "complete": ("decommissioned", "complete", True),
+ "canceled": ("blocked", "canceled", True),
+ "cleared": ("active", "none", False),
+ }
+ pool_state, operation_state, has_info = expected_states[state]
+ if (
+ str(entry.get("status", "")).lower() != pool_state
+ or str(entry.get("decommissionStatus", "")).lower()
+ != operation_state
+ or (entry.get("decommissionInfo") is not None) is not has_info
+ ):
+ raise AssertionError(
+ f"pool inventory disagrees with decommission {state}: {entry!r}"
+ )
+ return result
+
+ def cancel_decommission(self, case_id: str, pool_id: int = 0) -> None:
+ self.run_rc(
+ case_id,
+ "decommission-cancel",
+ ["admin", "decommission", "cancel", self.rc_alias, str(pool_id), "--by-id"],
+ )
+
+ def clear_decommission(self, case_id: str, pool_id: int = 0) -> None:
+ self.run_rc(
+ case_id,
+ "decommission-clear",
+ ["admin", "decommission", "clear", self.rc_alias, str(pool_id), "--by-id"],
+ )
+
+ def seed_and_expand(
+ self,
+ case_id: str,
+ *,
+ count: int | None = None,
+ size: int | None = None,
+ ) -> tuple[str, list[dict[str, Any]]]:
+ bucket = self.create_bucket(case_id)
+ inventory = self.put_inventory(case_id, bucket, count=count, size=size)
+ self.assert_inventory(case_id, self.client, bucket, inventory, "before-expansion")
+ self.census(case_id, "before-expansion")
+ self.apply_topology(case_id, "expanded")
+ self.pool_snapshot(case_id, "after-expansion", 2)
+ self.assert_inventory(
+ case_id,
+ self.retained_pool_client(),
+ bucket,
+ inventory,
+ "after-expansion-pool1-node0",
+ )
+ return bucket, inventory
+
+ def case_status(self, case_id: str) -> None:
+ bucket = self.create_bucket(case_id)
+ body = deterministic_payload(case_id, 8193)
+ self.client.put_object(Bucket=bucket, Key="status-probe.bin", Body=body)
+ initial, _ = self.pool_snapshot(case_id, "initial", 1)
+ self.assert_pool_specific_status(case_id, 1)
+ self.census(case_id, "initial")
+ self.apply_topology(case_id, "expanded")
+ expanded, _ = self.pool_snapshot(case_id, "expanded", 2)
+ self.assert_pool_specific_status(case_id, 2)
+ self.census(case_id, "expanded")
+ if pool_entries(initial)[0]["cmdline"] == pool_entries(expanded)[1]["cmdline"]:
+ raise AssertionError("expansion pool reused the seed pool command line")
+ initial_identity = canonical_pool(pool_entries(initial)[0])
+ expanded_seed = canonical_pool(pool_entries(expanded)[0])
+ if (
+ initial_identity["id"],
+ initial_identity["cmdline"],
+ ) != (
+ expanded_seed["id"],
+ expanded_seed["cmdline"],
+ ):
+ raise AssertionError(
+ "seed pool identity or ordering changed during expansion: "
+ f"{initial_identity!r} -> {expanded_seed!r}"
+ )
+ for endpoint in self.retained_pool_endpoints():
+ def read_probe() -> bytes:
+ response = self.node_clients[endpoint].get_object(
+ Bucket=bucket, Key="status-probe.bin"
+ )
+ return response["Body"].read()
+
+ observed = self.s3_retrying(
+ f"GET status probe through {endpoint}", read_probe
+ )
+ if observed != body:
+ raise AssertionError(f"status probe changed through {endpoint}")
+
+ def case_expand_and_rebalance(self, case_id: str) -> None:
+ bucket, inventory = self.seed_and_expand(case_id)
+ operation_id = self.start_rebalance(case_id, command="expand")
+ self.wait_rebalance(
+ case_id,
+ operation_id,
+ "active",
+ allow_fast_completion=True,
+ )
+ self.wait_rebalance(case_id, operation_id, "complete")
+ self.census(case_id, "after-rebalance")
+ self.assert_inventory(case_id, self.client, bucket, inventory, "after-rebalance")
+
+ def case_expansion_restart_persistence(self, case_id: str) -> None:
+ bucket, inventory = self.seed_and_expand(
+ case_id, size=active_fixture_bytes(self.args.fixture_bytes)
+ )
+ before, _ = self.pool_snapshot(case_id, "before-restart", 2)
+ operation_id = self.start_rebalance(case_id)
+ active = self.wait_rebalance(case_id, operation_id, "active")
+ active_counters = self.rebalance_counters(active)
+ self.restart_topology(case_id, "expanded")
+ after, _ = self.pool_snapshot(
+ case_id, "after-restart", 2, require_active=False
+ )
+ unexpected_states = {
+ str(item.get("status", "")).lower()
+ for item in pool_entries(after)
+ }.difference({"active", "rebalancing"})
+ if unexpected_states:
+ raise AssertionError(
+ f"unexpected pool states after rebalance restart: {unexpected_states}"
+ )
+ before_identity = tuple((item["id"], item["cmdline"]) for item in pool_entries(before))
+ after_identity = tuple((item["id"], item["cmdline"]) for item in pool_entries(after))
+ if before_identity != after_identity:
+ raise AssertionError(
+ f"pool identity changed across restart: {before_identity!r} -> {after_identity!r}"
+ )
+ completed = self.wait_rebalance(case_id, operation_id, "complete")
+ self.pool_snapshot(case_id, "after-rebalance-restart-complete", 2)
+ completed_counters = self.rebalance_counters(completed)
+ if any(
+ final < initial
+ for initial, final in zip(
+ active_counters, completed_counters, strict=True
+ )
+ ):
+ raise AssertionError(
+ "rebalance counters regressed across restart: "
+ f"{active_counters} -> {completed_counters}"
+ )
+ for index in range(len(self.args.node_endpoint)):
+ self.assert_inventory(
+ case_id,
+ self.node_clients[self.args.node_endpoint[index]],
+ bucket,
+ inventory,
+ f"restart-node-{index}",
+ )
+
+ @staticmethod
+ def rebalance_counters(document: Any) -> tuple[int, int, int]:
+ counters = [0, 0, 0]
+ for pool in document.get("pools", []):
+ progress = pool.get("progress") or {}
+ for index, field in enumerate(("objects", "versions", "bytes")):
+ counters[index] += int(progress.get(field, 0))
+ return tuple(counters)
+
+ def case_rebalance_stop_resume(self, case_id: str) -> None:
+ bucket, inventory = self.seed_and_expand(
+ case_id, size=active_fixture_bytes(self.args.fixture_bytes)
+ )
+ first_id = self.start_rebalance(case_id)
+ self.wait_rebalance(case_id, first_id, "active")
+ self.stop_rebalance(case_id)
+ self.wait_rebalance(
+ case_id,
+ first_id,
+ "stopped",
+ label="stopped-before-restart",
+ )
+ self.assert_inventory(case_id, self.client, bucket, inventory, "after-rebalance-stop")
+ self.restart_topology(case_id, "expanded")
+ self.wait_rebalance(
+ case_id,
+ first_id,
+ "stopped",
+ label="stopped-after-restart",
+ )
+ second_id = self.start_rebalance(case_id)
+ if second_id == first_id:
+ raise AssertionError("rebalance resume reused the stopped operation ID")
+ self.wait_rebalance(case_id, second_id, "complete")
+ self.assert_inventory(case_id, self.client, bucket, inventory, "after-rebalance-resume")
+
+ def exercise_live_io(
+ self,
+ case_id: str,
+ bucket: str,
+ *,
+ prefix: str,
+ client: Any | None = None,
+ ) -> list[dict[str, Any]]:
+ client = client or self.client
+
+ def live_put(index: int) -> dict[str, Any]:
+ key = f"{prefix}/put-{index:02d}.bin"
+ body = deterministic_payload(f"{case_id}:{key}", 64 * 1024)
+ response = self.put_retrying(client, bucket, key, body, {"phase": prefix})
+ return {
+ "key": key,
+ "size": len(body),
+ "sha256": hashlib.sha256(body).hexdigest(),
+ "etag": str(response.get("ETag", "")).strip('"'),
+ "metadata": {"phase": prefix},
+ "versionId": response.get("VersionId"),
+ }
+
+ with concurrent.futures.ThreadPoolExecutor(
+ max_workers=self.args.workers
+ ) as pool:
+ committed = list(pool.map(live_put, range(self.args.live_objects)))
+ committed.sort(key=lambda item: item["key"])
+ first = committed[0]
+ head = self.s3_retrying(
+ f"HEAD live object for {prefix}",
+ lambda: client.head_object(Bucket=bucket, Key=first["key"]),
+ )
+ if head.get("ContentLength") != first["size"]:
+ raise AssertionError("HEAD returned the wrong live-object length")
+
+ def read_live_object() -> bytes:
+ response = client.get_object(Bucket=bucket, Key=first["key"])
+ return response["Body"].read()
+
+ got = self.s3_retrying(f"GET live object for {prefix}", read_live_object)
+ if hashlib.sha256(got).hexdigest() != first["sha256"]:
+ raise AssertionError("GET returned corrupt live-object bytes")
+ listed = self.s3_retrying(
+ f"LIST live objects for {prefix}",
+ lambda: client.list_objects_v2(Bucket=bucket, Prefix=f"{prefix}/"),
+ ).get("Contents", [])
+ if not any(item.get("Key") == first["key"] for item in listed):
+ raise AssertionError("LIST omitted a committed live object")
+ copy_key = f"{prefix}/copy.bin"
+ copy_response = self.s3_retrying(
+ f"COPY live object for {prefix}",
+ lambda: client.copy_object(
+ Bucket=bucket,
+ Key=copy_key,
+ CopySource={"Bucket": bucket, "Key": first["key"]},
+ Metadata={"phase": prefix},
+ MetadataDirective="REPLACE",
+ ),
+ )
+ committed.append(
+ {
+ "key": copy_key,
+ "size": first["size"],
+ "sha256": first["sha256"],
+ "etag": str(copy_response.get("CopyObjectResult", {}).get("ETag", "")).strip('"'),
+ "metadata": {"phase": prefix},
+ "versionId": copy_response.get("VersionId"),
+ }
+ )
+ delete_key = f"{prefix}/delete-control.bin"
+ self.s3_retrying(
+ f"PUT delete control for {prefix}",
+ lambda: client.put_object(Bucket=bucket, Key=delete_key, Body=b"delete-me"),
+ )
+ self.s3_retrying(
+ f"DELETE control for {prefix}",
+ lambda: client.delete_object(Bucket=bucket, Key=delete_key),
+ )
+ try:
+ self.s3_retrying(
+ f"HEAD deleted control for {prefix}",
+ lambda: client.head_object(Bucket=bucket, Key=delete_key),
+ )
+ except ClientError as exc:
+ if exc.response.get("Error", {}).get("Code") not in {
+ "404",
+ "NoSuchKey",
+ "NotFound",
+ }:
+ raise
+ else:
+ raise AssertionError("DELETE control object remains visible")
+ self.save_json(case_id, f"{prefix}-live-manifest.json", committed)
+ return committed
+
+ def case_rebalance_live_io(self, case_id: str) -> None:
+ bucket, inventory = self.seed_and_expand(
+ case_id, size=active_fixture_bytes(self.args.fixture_bytes)
+ )
+ operation_id = self.start_rebalance(case_id)
+ self.wait_rebalance(case_id, operation_id, "active")
+ live = self.exercise_live_io(case_id, bucket, prefix="during-rebalance")
+ status, _, body = self.signed_request(
+ self.args.endpoint, "GET", f"{ADMIN_PREFIX}/rebalance/status"
+ )
+ if status != 200 or not rebalance_active(json.loads(body), operation_id, require_progress=True):
+ raise AssertionError("rebalance did not remain active across the live-I/O window")
+ self.wait_rebalance(case_id, operation_id, "complete")
+ self.assert_inventory(case_id, self.client, bucket, inventory + live, "rebalance-live-final")
+
+ def case_decommission(self, case_id: str) -> None:
+ bucket, inventory = self.seed_and_expand(case_id)
+ self.start_decommission(case_id)
+ self.wait_decommission(
+ case_id, 0, "active", allow_fast_completion=True
+ )
+ self.wait_decommission(case_id, 0, "complete")
+ self.census(case_id, "after-decommission")
+ self.assert_inventory(
+ case_id,
+ self.retire_decommissioned_pool(case_id),
+ bucket,
+ inventory,
+ "decommission-survivor",
+ )
+
+ def case_decommission_cancel_clear_resume(self, case_id: str) -> None:
+ bucket, inventory = self.seed_and_expand(
+ case_id, size=active_fixture_bytes(self.args.fixture_bytes)
+ )
+ self.start_decommission(case_id)
+ self.wait_decommission(case_id, 0, "active")
+ self.cancel_decommission(case_id)
+ canceled_before = self.wait_decommission(
+ case_id,
+ 0,
+ "canceled",
+ label="canceled-before-restart",
+ )
+ self.assert_inventory(case_id, self.client, bucket, inventory, "after-decommission-cancel")
+ self.restart_topology(case_id, "expanded")
+ canceled_after = self.wait_decommission(
+ case_id,
+ 0,
+ "canceled",
+ label="canceled-after-restart",
+ )
+ if self.decommission_counters(canceled_before, 0) != self.decommission_counters(canceled_after, 0):
+ raise AssertionError("canceled decommission progress changed across restart")
+ self.clear_decommission(case_id)
+ self.wait_decommission(
+ case_id,
+ 0,
+ "cleared",
+ label="cleared-before-restart",
+ )
+ self.restart_topology(case_id, "expanded")
+ self.wait_decommission(
+ case_id,
+ 0,
+ "cleared",
+ label="cleared-after-restart",
+ )
+ self.start_decommission(case_id)
+ self.wait_decommission(case_id, 0, "complete")
+ self.assert_inventory(
+ case_id, self.retire_decommissioned_pool(case_id), bucket, inventory, "after-decommission-resume"
+ )
+
+ @staticmethod
+ def decommission_counters(document: Any, pool_id: int) -> tuple[int, int]:
+ info = decommission_entry(document, pool_id).get("decommissionInfo", {})
+ return int(info.get("objectsDecommissioned", 0)), int(info.get("bytesDecommissioned", 0))
+
+ def case_decommission_restart_recovery(self, case_id: str) -> None:
+ bucket, inventory = self.seed_and_expand(
+ case_id, size=active_fixture_bytes(self.args.fixture_bytes)
+ )
+ self.start_decommission(case_id)
+ before = self.wait_decommission(case_id, 0, "active")
+ before_counters = self.decommission_counters(before, 0)
+ self.restart_topology(case_id, "expanded")
+ final = self.wait_decommission(case_id, 0, "complete")
+ final_counters = self.decommission_counters(final, 0)
+ if any(after < prior for prior, after in zip(before_counters, final_counters, strict=True)):
+ raise AssertionError(
+ f"decommission counters regressed across restart: {before_counters} -> {final_counters}"
+ )
+ self.assert_inventory(
+ case_id,
+ self.retire_decommissioned_pool(case_id),
+ bucket,
+ inventory,
+ "decommission-restart-final",
+ )
+
+ def assert_expanded_delete_marker_removal(self, case_id: str, bucket: str) -> None:
+ if self.current_layout != "expanded":
+ raise AssertionError("delete-marker API probes require the expanded topology")
+ for operation in ("single", "batch"):
+ key = f"delete-api-probe/{operation}.bin"
+ payload = b"historical data behind a delete marker"
+ original = self.client.put_object(Bucket=bucket, Key=key, Body=payload)
+ marker = self.client.delete_object(Bucket=bucket, Key=key)
+ marker_id = marker.get("VersionId")
+ if not original.get("VersionId") or not marker_id or marker.get("DeleteMarker") is not True:
+ raise AssertionError("delete-marker API probe did not create versioned history")
+ self.save_json(case_id, f"delete-api-{operation}-fixture.json", {"original": original, "marker": marker})
+ for method in (self.client.get_object, self.client.head_object):
+ try:
+ method(Bucket=bucket, Key=key, VersionId=marker_id)
+ except ClientError as exc:
+ if (exc.response.get("Error", {}).get("Code") not in {"405", "MethodNotAllowed"}
+ or exc.response.get("ResponseMetadata", {}).get("HTTPStatusCode") != 405):
+ raise
+ else:
+ raise AssertionError("GET/HEAD of an explicit delete marker must remain 405")
+ if operation == "single":
+ response = self.client.delete_object(Bucket=bucket, Key=key, VersionId=marker_id)
+ self.save_json(case_id, "delete-api-single-response.json", response)
+ if response.get("VersionId") != marker_id or response.get("DeleteMarker") is not True:
+ raise AssertionError("single delete did not acknowledge the exact marker version")
+ else:
+ response = self.client.delete_objects(
+ Bucket=bucket, Delete={"Objects": [{"Key": key, "VersionId": marker_id}], "Quiet": False}
+ )
+ self.save_json(case_id, "delete-api-batch-response.json", response)
+ deleted = response.get("Deleted", [])
+ if (response.get("Errors") or len(deleted) != 1 or deleted[0].get("Key") != key
+ or deleted[0].get("VersionId") != marker_id or deleted[0].get("DeleteMarker") is not True):
+ raise AssertionError("batch delete did not acknowledge the exact marker version")
+ versions = self.client.list_object_versions(Bucket=bucket, Prefix=key)
+ self.save_json(case_id, f"delete-api-{operation}-remaining.json", versions)
+ if any(item.get("Key") == key for item in versions.get("DeleteMarkers", [])):
+ raise AssertionError("marker version remains after successful delete acknowledgement")
+ remaining = [item for item in versions.get("Versions", []) if item.get("Key") == key]
+ if (len(remaining) != 1 or remaining[0].get("VersionId") != original["VersionId"]
+ or remaining[0].get("IsLatest") is not True):
+ raise AssertionError("marker removal changed the historical version identity")
+ if self.client.get_object(Bucket=bucket, Key=key)["Body"].read() != payload:
+ raise AssertionError("marker removal damaged the historical payload")
+
+ def case_decommission_versioned_integrity(self, case_id: str) -> None:
+ bucket = self.create_bucket(case_id)
+ self.client.put_bucket_versioning(
+ Bucket=bucket, VersioningConfiguration={"Status": "Enabled"}
+ )
+ key = "history/versioned.bin"
+ payloads = [
+ deterministic_payload(f"{case_id}:v1", 37 * 1024),
+ deterministic_payload(f"{case_id}:v2", 71 * 1024),
+ ]
+ version_ids: list[str] = []
+ for payload in payloads:
+ response = self.client.put_object(Bucket=bucket, Key=key, Body=payload)
+ version_id = response.get("VersionId")
+ if not isinstance(version_id, str) or not version_id:
+ raise AssertionError("versioned PUT omitted VersionId")
+ version_ids.append(version_id)
+ deleted = self.client.delete_object(Bucket=bucket, Key=key)
+ marker_id = deleted.get("VersionId")
+ if deleted.get("DeleteMarker") is not True or not marker_id:
+ raise AssertionError("versioned DELETE did not create a delete marker")
+ self.save_json(case_id, "version-fixture.json", {
+ "versionIds": version_ids, "deleteMarkerVersionId": marker_id,
+ "payloads": [{"size": len(payload), "sha256": hashlib.sha256(payload).hexdigest()} for payload in payloads],
+ })
+ inventory = self.put_inventory(case_id, bucket, count=max(32, self.args.fixture_objects // 2))
+ self.apply_topology(case_id, "expanded")
+ self.assert_expanded_delete_marker_removal(case_id, bucket)
+ self.start_decommission(case_id)
+ self.wait_decommission(case_id, 0, "complete")
+ checker = self.retire_decommissioned_pool(case_id)
+ versions = checker.list_object_versions(Bucket=bucket, Prefix=key)
+ self.save_json(
+ case_id, "version-timeline.json",
+ {"versionIds": version_ids, "deleteMarkerVersionId": marker_id, "observed": versions},
+ )
+ observed_versions = {item["VersionId"] for item in versions.get("Versions", []) if item.get("Key") == key}
+ observed_markers = {
+ item["VersionId"]
+ for item in versions.get("DeleteMarkers", [])
+ if item.get("Key") == key
+ }
+ latest_markers = {
+ item["VersionId"]
+ for item in versions.get("DeleteMarkers", [])
+ if item.get("Key") == key and item.get("IsLatest") is True
+ }
+ if (
+ set(version_ids) != observed_versions
+ or observed_markers != {marker_id}
+ or latest_markers != {marker_id}
+ ):
+ raise AssertionError(
+ "version timeline changed: "
+ f"versions={observed_versions}, markers={observed_markers}, "
+ f"latestMarkers={latest_markers}"
+ )
+ for version_id, expected in zip(version_ids, payloads, strict=True):
+ body = checker.get_object(
+ Bucket=bucket, Key=key, VersionId=version_id
+ )["Body"].read()
+ if body != expected:
+ raise AssertionError(f"historical version {version_id} changed")
+ marker_modified = next(
+ item.get("LastModified") for item in versions["DeleteMarkers"]
+ if item.get("Key") == key and item.get("VersionId") == marker_id
+ )
+ self.assert_delete_marker_reads(case_id, checker, bucket, key, marker_id, marker_modified)
+ self.assert_inventory(case_id, checker, bucket, inventory, "versioned-decommission-final")
+
+ def assert_delete_marker_reads(
+ self, case_id: str, checker: Any, bucket: str, key: str,
+ marker_id: str, marker_modified: datetime | None,
+ ) -> None:
+ for operation, explicit, filename in (
+ ("get_object", False, "latest-delete-marker-response.json"),
+ ("head_object", False, "latest-delete-marker-head-response.json"),
+ ("get_object", True, "explicit-delete-marker-get-response.json"),
+ ("head_object", True, "explicit-delete-marker-head-response.json"),
+ ):
+ request = {"Bucket": bucket, "Key": key}
+ if explicit:
+ request["VersionId"] = marker_id
+ try:
+ response = getattr(checker, operation)(**request)
+ except ClientError as exc:
+ self.save_json(case_id, filename, exc.response)
+ expected_status = 405 if explicit else 404
+ expected_codes = {"405", "MethodNotAllowed"} if explicit else {"404", "NoSuchKey", "NotFound"}
+ if exc.response.get("Error", {}).get("Code") not in expected_codes:
+ raise
+ metadata = exc.response.get("ResponseMetadata", {})
+ if metadata.get("HTTPStatusCode") != expected_status:
+ raise AssertionError(f"{operation} delete marker returned the wrong HTTP status")
+ headers = metadata.get("HTTPHeaders", {})
+ if str(headers.get("x-amz-delete-marker", "")).lower() != "true":
+ raise AssertionError(
+ f"{'explicit' if explicit else 'latest'} version returned {expected_status} "
+ "without delete-marker identity"
+ )
+ if headers.get("x-amz-version-id") != marker_id:
+ raise AssertionError("delete-marker response returned the wrong VersionId")
+ if explicit:
+ if not isinstance(marker_modified, datetime) or marker_modified.utcoffset() is None:
+ raise AssertionError("version listing omitted the marker LastModified timestamp")
+ expected_date = format_datetime(marker_modified.astimezone(UTC).replace(microsecond=0), usegmt=True)
+ if headers.get("last-modified") != expected_date:
+ raise AssertionError("explicit delete-marker response returned the wrong Last-Modified")
+ else:
+ body = response.get("Body")
+ if body is not None:
+ body.close()
+ raise AssertionError("latest or explicit delete marker no longer hides the versioned object")
+
+ def multipart_request(
+ self, case_id: str, label: str, client: Any, operation: str, **params: Any,
+ ) -> Any:
+ request = dict(params)
+ if "Body" in request:
+ body = request.pop("Body")
+ request.update(bodyBytes=len(body), bodySha256=hashlib.sha256(body).hexdigest())
+ evidence = {"operation": operation, "request": request, "startedAt": utc_now()}
+ try:
+ response = getattr(client, operation)(**params)
+ except Exception as exc:
+ evidence.update(error=f"{type(exc).__name__}: {exc}", response=getattr(exc, "response", None))
+ self.save_json(case_id, f"mpu-{label}.json", evidence)
+ raise
+ evidence["response"] = {key: value for key, value in response.items() if key != "Body"}
+ try:
+ self.save_json(case_id, f"mpu-{label}.json", evidence)
+ except Exception:
+ if "Body" in response:
+ response["Body"].close()
+ raise
+ return response
+
+ def multipart_entries(
+ self, case_id: str, label: str, client: Any, operation: str, **params: Any,
+ ) -> list[dict[str, Any]]:
+ field = "Uploads" if operation == "list_multipart_uploads" else "Parts"
+ entries: list[dict[str, Any]] = []
+ identities: set[Any] = set()
+ deadline = time.monotonic() + self.args.active_timeout
+ last_truncated: bool | None = None
+ try:
+ pages = client.get_paginator(operation).paginate(**params, PaginationConfig={"PageSize": 1})
+ for index, page in enumerate(pages, 1):
+ self.save_json(case_id, f"mpu-{label}-page-{index}.json", {"request": params, "response": page})
+ if index > 64 or time.monotonic() >= deadline:
+ raise AssertionError("MPU fixture pagination exceeded its bounded budget")
+ if (not isinstance(page, dict) or type(page.get("IsTruncated")) is not bool
+ or not isinstance(page.get(field, []), list)):
+ raise AssertionError("malformed MPU listing page")
+ last_truncated = page["IsTruncated"]
+ for name in ("Bucket", "Key", "UploadId"):
+ if name in params and name in page and page[name] != params[name]:
+ raise AssertionError("MPU listing response has the wrong identity")
+ for item in page.get(field, []):
+ if not isinstance(item, dict):
+ raise AssertionError("malformed MPU listing entry")
+ if field == "Uploads":
+ identity = (item.get("Key"), item.get("UploadId"))
+ valid = all(isinstance(value, str) and value for value in identity)
+ else:
+ identity = item.get("PartNumber")
+ valid = (
+ type(identity) is int and 1 <= identity <= 10000
+ and type(item.get("Size")) is int and item["Size"] >= 0
+ and isinstance(item.get("ETag"), str) and bool(item["ETag"])
+ )
+ if not valid or identity in identities:
+ raise AssertionError("invalid or duplicate MPU listing identity")
+ identities.add(identity)
+ entries.append(item)
+ except Exception as exc:
+ self.save_json(case_id, f"mpu-{label}-error.json", {
+ "operation": operation, "request": params, "error": f"{type(exc).__name__}: {exc}",
+ "response": getattr(exc, "response", None),
+ })
+ raise
+ if last_truncated is None:
+ raise AssertionError("MPU paginator returned no response")
+ if last_truncated:
+ raise AssertionError("MPU paginator stopped before the final page")
+ return entries
+
+ def assert_multipart_uploads(
+ self, case_id: str, bucket: str, uploads: list[dict[str, Any]], label: str,
+ ) -> None:
+ expected = {(item["key"], item["uploadId"]) for item in uploads}
+ for index, endpoint in enumerate(self.args.node_endpoint):
+ client = self.node_clients[endpoint]
+ prefix = f"{label}-node-{index}"
+ listed = self.multipart_entries(case_id, prefix, client, "list_multipart_uploads", Bucket=bucket)
+ if {(item["Key"], item["UploadId"]) for item in listed} != expected:
+ raise AssertionError("pending MPU inventory changed or contains an unexpected upload")
+ for upload in uploads:
+ parts = self.multipart_entries(
+ case_id, f"{prefix}-{safe_name(upload['key'])}-parts", client, "list_parts",
+ Bucket=bucket, Key=upload["key"], UploadId=upload["uploadId"],
+ )
+ fields = ("PartNumber", "ETag", "Size")
+ observed_parts = [tuple(item[name] for name in fields) for item in parts]
+ expected_parts = [tuple(item[name] for name in fields) for item in upload["parts"]]
+ if observed_parts != expected_parts:
+ raise AssertionError("pending MPU part number, ETag or length changed")
+
+ def wait_multipart_drain_blocked(self, case_id: str, bucket: str, since: str) -> None:
+ attempt = 0
+
+ def probe() -> tuple[bool, Any]:
+ nonlocal attempt
+ attempt += 1
+ status, _, body = self.signed_request(self.args.endpoint, "GET", f"{ADMIN_PREFIX}/decommission/status")
+ if status != 200:
+ raise PoolStatusError(f"MPU drain status returned HTTP {status}")
+ document = json.loads(body)
+ try:
+ return decommission_multipart_blocked(document, 0), document
+ finally:
+ self.save_json(case_id, f"mpu-drain-status-{attempt}.json", document)
+ result = self.wait_until(case_id, "owned MPU to block final decommission", probe)
+ self.save_json(case_id, "mpu-drain-blocked.json", result)
+ node_attempt = 0
+
+ def nodes_probe() -> tuple[bool, Any]:
+ nonlocal node_attempt
+ node_attempt += 1
+ documents = [
+ self.admin_json(case_id, f"mpu-drain-blocked-{node_attempt}-node-{index}", "decommission/status", endpoint=endpoint)
+ for index, endpoint in enumerate(self.args.node_endpoint)
+ ]
+ matched = [decommission_multipart_blocked(document, 0) for document in documents]
+ return all(matched), documents
+
+ self.wait_until(case_id, "MPU drain failure on every node", nodes_probe, timeout=min(self.args.active_timeout, 10))
+ parity = self.decommission_status(case_id, 0, "mpu-drain-blocked-parity", predicate=decommission_multipart_blocked)
+ if not decommission_multipart_blocked(parity, 0):
+ raise PoolStatusError("MPU drain failure disagrees with rc/Admin parity")
+ reference = self.hook(
+ case_id, "mpu-drain-journal", self.args.diagnostic_hook,
+ ("--phase", "mpu-drain-failure", "--since", since), timeout=60,
+ )
+ journal = json.loads((self.evidence_dir / reference).read_text(encoding="utf-8"))
+ records = multipart_drain_failure_records(journal, bucket=bucket, since=since, node_count=len(self.args.node_endpoint))
+ self.save_json(case_id, "mpu-drain-reason.json", records)
+
+ def case_decommission_multipart_integrity(self, case_id: str) -> None:
+ if not self.args.diagnostic_hook:
+ raise RuntimeError("MPU drain verification requires the read-only journal hook")
+ bucket = self.create_bucket(case_id)
+ inventory = self.put_inventory(
+ case_id, bucket, count=max(32, self.args.fixture_objects // 2),
+ size=active_fixture_bytes(self.args.fixture_bytes),
+ )
+ first = deterministic_payload(f"{case_id}:multipart:first", 5 * 1024 * 1024)
+ second = deterministic_payload(f"{case_id}:multipart:second", 1024 * 1024)
+
+ def part(upload: dict[str, Any], number: int, data: bytes, client: Any) -> None:
+ response = self.multipart_request(
+ case_id, f"{upload['kind']}-part-{number}", client, "upload_part",
+ Bucket=bucket, Key=upload["key"], UploadId=upload["uploadId"], PartNumber=number, Body=data,
+ )
+ if not isinstance(response.get("ETag"), str) or not response["ETag"]:
+ raise AssertionError("MPU UploadPart omitted its ETag")
+ upload["parts"].append({
+ "PartNumber": number, "ETag": response["ETag"], "Size": len(data),
+ "sha256": hashlib.sha256(data).hexdigest(),
+ })
+
+ def complete(upload: dict[str, Any], client: Any) -> None:
+ parts = [{name: item[name] for name in ("ETag", "PartNumber")} for item in upload["parts"]]
+ response = self.multipart_request(
+ case_id, f"{upload['kind']}-complete", client, "complete_multipart_upload",
+ Bucket=bucket, Key=upload["key"], UploadId=upload["uploadId"], MultipartUpload={"Parts": parts},
+ )
+ if not isinstance(response.get("ETag"), str) or not response["ETag"]:
+ raise AssertionError("MPU Complete omitted its ETag")
+ upload["etag"] = response["ETag"]
+
+ uploads = []
+ for kind in ("completed", "pending", "abort-active", "abort-failed", "resume-cleared"):
+ key = f"multipart/{kind}.bin"
+ created = self.multipart_request(
+ case_id, f"{kind}-create", self.client, "create_multipart_upload",
+ Bucket=bucket, Key=key, Metadata={"kind": kind},
+ )
+ if not isinstance(created.get("UploadId"), str) or not created["UploadId"]:
+ raise AssertionError("MPU creation omitted its UploadId")
+ upload = {"kind": kind, "key": key, "uploadId": created["UploadId"], "parts": []}
+ uploads.append(upload)
+ part(upload, 1, first, self.client)
+ if kind == "completed":
+ part(upload, 2, second, self.client)
+ complete(upload, self.client)
+ completed, pending, abort_active, abort_failed, resume_cleared = uploads
+ self.save_json(case_id, "multipart-fixture.json", {"bucket": bucket, "uploads": uploads})
+ self.apply_topology(case_id, "expanded")
+ self.assert_multipart_uploads(case_id, bucket, uploads[1:], "before-start")
+ since = utc_now()
+ self.start_decommission(case_id)
+ self.wait_decommission(case_id, 0, "active", label="mpu-active")
+ # Both ends of the S3 window must be observed active. A fast terminal
+ # transition is a coverage failure, never permission to resume afterward.
+ self.assert_multipart_uploads(case_id, bucket, uploads[1:], "active")
+ client = self.retained_pool_client()
+ part(pending, 2, second, client)
+ self.assert_multipart_uploads(case_id, bucket, uploads[1:], "active-two-parts")
+ complete(pending, client)
+ self.multipart_request(
+ case_id, "abort-active", client, "abort_multipart_upload",
+ Bucket=bucket, Key=abort_active["key"], UploadId=abort_active["uploadId"],
+ )
+ active = self.decommission_status(case_id, 0, "mpu-after-active-io")
+ if not decommission_active(active, 0, require_progress=True):
+ raise AssertionError("decommission did not remain active across MPU continuation")
+ self.save_json(case_id, "multipart-after-active-io.json", {"bucket": bucket, "uploads": uploads})
+
+ # Retain owned uploads to exercise the gate and both recovery paths.
+ self.wait_multipart_drain_blocked(case_id, bucket, since)
+ self.assert_multipart_uploads(case_id, bucket, [abort_failed, resume_cleared], "blocked")
+ self.multipart_request(
+ case_id, "abort-failed", client, "abort_multipart_upload",
+ Bucket=bucket, Key=abort_failed["key"], UploadId=abort_failed["uploadId"],
+ )
+ self.assert_multipart_uploads(case_id, bucket, [resume_cleared], "before-clear")
+ self.clear_decommission(case_id)
+ self.wait_decommission(case_id, 0, "cleared", label="mpu-drained-clear")
+ self.assert_multipart_uploads(case_id, bucket, [resume_cleared], "after-clear")
+ # Clear does not abort MPU data. Completing this source-owned UploadId
+ # before retry also supplies real movement work for its strict counters.
+ part(resume_cleared, 2, second, client)
+ self.assert_multipart_uploads(case_id, bucket, [resume_cleared], "cleared-two-parts")
+ complete(resume_cleared, client)
+ self.assert_multipart_uploads(case_id, bucket, [], "drained-before-retry")
+ self.save_json(case_id, "multipart-after-clear.json", {"bucket": bucket, "uploads": uploads})
+ self.start_decommission(case_id, label="mpu-drained-retry")
+ self.wait_decommission(case_id, 0, "complete", label="mpu-drained-complete")
+ checker = self.retire_decommissioned_pool(case_id)
+ expected_body = first + second
+ for upload in (completed, pending, resume_cleared):
+ head = self.multipart_request(
+ case_id, f"{upload['kind']}-final-head", checker, "head_object", Bucket=bucket, Key=upload["key"],
+ )
+ if (
+ head.get("ContentLength") != len(expected_body)
+ or str(head.get("ETag", "")).strip('"') != upload["etag"].strip('"')
+ or head.get("Metadata") != {"kind": upload["kind"]}
+ ):
+ raise AssertionError("multipart length, ETag or metadata changed after decommission")
+ response = self.multipart_request(
+ case_id, f"{upload['kind']}-final-get", checker, "get_object", Bucket=bucket, Key=upload["key"],
+ )
+ stream = response["Body"]
+ try:
+ body = stream.read()
+ finally:
+ stream.close()
+ self.save_json(case_id, f"mpu-{upload['kind']}-final-content.json", {
+ "bytes": len(body), "sha256": hashlib.sha256(body).hexdigest(),
+ })
+ if body != expected_body:
+ raise AssertionError("multipart payload changed after decommission")
+ if self.multipart_entries(case_id, "retired-pending", checker, "list_multipart_uploads", Bucket=bucket):
+ raise AssertionError("drained multipart uploads reappeared after retirement")
+ self.assert_inventory(case_id, checker, bucket, inventory, "multipart-decommission-final")
+ self.save_json(case_id, "multipart-summary.json", {
+ "bucket": bucket, "uploads": uploads, "sha256": hashlib.sha256(expected_body).hexdigest(),
+ "drainRecoveryVerified": True,
+ })
+
+ def case_decommission_live_io(self, case_id: str) -> None:
+ bucket, inventory = self.seed_and_expand(
+ case_id, size=active_fixture_bytes(self.args.fixture_bytes)
+ )
+ self.start_decommission(case_id)
+ self.wait_decommission(case_id, 0, "active")
+ clients = [self.node_clients[endpoint] for endpoint in self.args.node_endpoint]
+
+ def live_put(index: int) -> dict[str, Any]:
+ client = clients[index % len(clients)]
+ key = f"during-decommission/concurrent-{index:02d}.bin"
+ body = deterministic_payload(f"{case_id}:{key}", 64 * 1024)
+ response = self.put_retrying(client, bucket, key, body, {"phase": "decommission"})
+ return {
+ "key": key,
+ "size": len(body),
+ "sha256": hashlib.sha256(body).hexdigest(),
+ "etag": str(response.get("ETag", "")).strip('"'),
+ "metadata": {"phase": "decommission"},
+ "versionId": response.get("VersionId"),
+ }
+
+ with concurrent.futures.ThreadPoolExecutor(max_workers=self.args.workers) as pool:
+ live = list(pool.map(live_put, range(self.args.live_objects)))
+ live.extend(
+ self.exercise_live_io(
+ case_id,
+ bucket,
+ prefix="during-decommission-serial",
+ client=self.retained_pool_client(),
+ )
+ )
+ status, _, body = self.signed_request(
+ self.args.endpoint, "GET", f"{ADMIN_PREFIX}/decommission/status"
+ )
+ if status != 200 or not decommission_active(json.loads(body), 0, require_progress=True):
+ raise AssertionError("decommission did not remain active across concurrent I/O")
+ self.wait_decommission(case_id, 0, "complete")
+ self.assert_inventory(
+ case_id,
+ self.retire_decommissioned_pool(case_id),
+ bucket,
+ inventory + live,
+ "decommission-live-final",
+ )
+
+ def case_retired_config_removal(self, case_id: str) -> None:
+ bucket, inventory = self.seed_and_expand(case_id)
+ self.start_decommission(case_id)
+ checker = self.retire_decommissioned_pool(case_id)
+ self.assert_inventory(case_id, checker, bucket, inventory, "retired-config-final")
+ payload = b"written after retired pool removal"
+ self.client.put_object(Bucket=bucket, Key="after-removal.bin", Body=payload)
+ observed = self.client.get_object(Bucket=bucket, Key="after-removal.bin")["Body"].read()
+ if observed != payload:
+ raise AssertionError("post-removal object round-trip failed")
+ self.census(case_id, "after-retired-config-removal")
+
+ def case_operation_guards(self, case_id: str) -> None:
+ bucket = self.create_bucket(case_id)
+ inventory = self.put_inventory(
+ case_id,
+ bucket,
+ size=active_fixture_bytes(self.args.fixture_bytes),
+ )
+ self.run_rc(
+ case_id,
+ "unknown-pool-status",
+ ["admin", "pool", "status", self.rc_alias, "999999", "--by-id"],
+ expect_success=False,
+ expected_error_code="InvalidArgument",
+ )
+ self.run_rc(
+ case_id,
+ "unknown-pool-decommission",
+ ["admin", "decommission", "start", self.rc_alias, "999999", "--by-id"],
+ expect_success=False,
+ expected_error_code="InvalidArgument",
+ )
+ self.run_rc(
+ case_id,
+ "unknown-pool-decommission-status",
+ ["admin", "decommission", "status", self.rc_alias, "999999", "--by-id"],
+ expect_success=False,
+ expected_error_code="InvalidArgument",
+ )
+ for operation in ("cancel", "clear"):
+ self.run_rc(
+ case_id,
+ f"unknown-pool-decommission-{operation}",
+ [
+ "admin",
+ "decommission",
+ operation,
+ self.rc_alias,
+ "999999",
+ "--by-id",
+ ],
+ expect_success=False,
+ expected_error_code="InvalidArgument",
+ )
+ self.run_rc(
+ case_id,
+ "only-pool-decommission",
+ ["admin", "decommission", "start", self.rc_alias, "0", "--by-id"],
+ expect_success=False,
+ expected_error_code="InvalidArgument",
+ )
+ self.run_rc(
+ case_id,
+ "single-pool-rebalance",
+ ["admin", "rebalance", "start", self.rc_alias],
+ expect_success=False,
+ expected_error_code="NotImplemented",
+ )
+ for operation in ("cancel", "clear"):
+ self.run_rc(
+ case_id,
+ f"idle-pool-decommission-{operation}",
+ [
+ "admin",
+ "decommission",
+ operation,
+ self.rc_alias,
+ "0",
+ "--by-id",
+ ],
+ expect_success=False,
+ expected_error_code="InvalidArgument",
+ )
+ guarded, _ = self.pool_snapshot(case_id, "after-single-pool-guards", 1)
+ guarded_pool = pool_entries(guarded)[0]
+ if str(guarded_pool.get("decommissionStatus", "")).lower() not in {
+ "",
+ "none",
+ } or guarded_pool.get("decommissionInfo") is not None:
+ raise AssertionError(
+ "rejected single-pool operations left partial decommission metadata"
+ )
+ if str(guarded_pool.get("rebalanceStatus", "")).lower() not in {
+ "",
+ "none",
+ }:
+ raise AssertionError(
+ "rejected single-pool rebalance left partial operation metadata"
+ )
+ self.apply_topology(case_id, "expanded")
+ rebalance_id = self.start_rebalance(case_id)
+ self.wait_rebalance(case_id, rebalance_id, "active")
+ self.run_rc(
+ case_id,
+ "duplicate-rebalance-start",
+ ["admin", "rebalance", "start", self.rc_alias],
+ expect_success=False,
+ expected_error_code="OperationAborted",
+ )
+ self.run_rc(
+ case_id,
+ "decommission-during-rebalance",
+ ["admin", "decommission", "start", self.rc_alias, "0", "--by-id"],
+ expect_success=False,
+ expected_error_code="OperationAborted",
+ )
+ self.wait_rebalance(case_id, rebalance_id, "active")
+ self.stop_rebalance(case_id)
+ self.wait_rebalance(case_id, rebalance_id, "stopped")
+ self.start_decommission(case_id)
+ self.wait_decommission(case_id, 0, "active")
+ self.run_rc(
+ case_id,
+ "duplicate-decommission-start",
+ ["admin", "decommission", "start", self.rc_alias, "0", "--by-id"],
+ expect_success=False,
+ expected_error_code="InvalidRequest",
+ )
+ self.run_rc(
+ case_id,
+ "rebalance-during-decommission",
+ ["admin", "rebalance", "start", self.rc_alias],
+ expect_success=False,
+ expected_error_code="InvalidRequest",
+ )
+ self.wait_decommission(case_id, 0, "active")
+ self.cancel_decommission(case_id)
+ self.wait_decommission(case_id, 0, "canceled")
+ self.run_rc(
+ case_id,
+ "restart-canceled-decommission-before-clear",
+ ["admin", "decommission", "start", self.rc_alias, "0", "--by-id"],
+ expect_success=False,
+ expected_error_code="InvalidArgument",
+ )
+ self.clear_decommission(case_id)
+ self.wait_decommission(case_id, 0, "cleared")
+ self.assert_inventory(case_id, self.client, bucket, inventory, "operation-guards-final")
+
+ def classify_exception(self, exc: BaseException, *, case_id: str | None = None) -> str:
+ if isinstance(exc, (AssertionError, PoolStatusError)):
+ return "fail"
+ if isinstance(exc, ClientError):
+ code = exc.response.get("Error", {}).get("Code", "")
+ if code == "SlowDown" and case_id in {
+ "pool.decommission-live-io", "pool.rebalance-live-io"
+ }:
+ return "fail"
+ return "infra_error" if code in INFRA_ERROR_CODES else "fail"
+ return "infra_error"
+
+ def run_case(self, case_id: str, function: Callable[[str], None]) -> None:
+ started_at = utc_now()
+ started = time.monotonic()
+ state = "pass"
+ message = "all assertions passed"
+ print(f"[RUN ] {case_id}", flush=True)
+ self.events.record("case.start", case=case_id)
+ try:
+ self.reset_case_environment(case_id)
+ function(case_id)
+ except Exception as exc:
+ state = self.classify_exception(exc, case_id=case_id)
+ message = self.redact(f"{type(exc).__name__}: {exc}")
+ self.events.record(
+ "case.error",
+ case=case_id,
+ state=state,
+ errorType=type(exc).__name__,
+ message=self.redact(str(exc)),
+ )
+ try:
+ self.capture_diagnostics(case_id, "case-failure-before-cleanup")
+ except Exception as diagnostic_exc:
+ diagnostic_message = self.redact(
+ f"{type(diagnostic_exc).__name__}: {diagnostic_exc}"
+ )
+ message += f"; diagnostic collection failed: {diagnostic_message}"
+ self.events.record(
+ "diagnostic.collection.error", case=case_id, message=diagnostic_message
+ )
+ if not self.args.cleanup_on_failure:
+ self.isolation_failure = {"caseId": case_id, "message": message}
+ finally:
+ bucket = self.bucket_name(case_id)
+ if bucket in self.created_buckets and (state == "pass" or self.args.cleanup_on_failure):
+ try:
+ self.cleanup_bucket(bucket)
+ except Exception as exc:
+ cleanup_state = self.classify_exception(exc)
+ cleanup_message = self.redact(f"{type(exc).__name__}: {exc}")
+ self.save_json(
+ case_id, "cleanup-error.json", {"state": cleanup_state, "message": cleanup_message}
+ )
+ self.events.record(
+ "case.cleanup.error", case=case_id, state=cleanup_state, message=cleanup_message
+ )
+ if state == "pass":
+ state = cleanup_state
+ message = self.redact(
+ f"{message}; cleanup failed: {cleanup_message}"
+ )
+ if not self.args.cleanup_on_failure:
+ self.isolation_failure = {"caseId": case_id, "message": message}
+ elif bucket in self.created_buckets:
+ message += f"; preserved failed bucket {bucket} in {self.current_layout} topology"
+ directory = self.evidence_path(case_id, "evidence-index").parent
+ evidence = sorted(
+ str(path.relative_to(self.evidence_dir))
+ for path in directory.iterdir()
+ if path.is_file() and path.name != "evidence-index"
+ )
+ if not evidence:
+ evidence = [self.save_json(case_id, "case-summary.json", {"state": state, "message": message})]
+ result = CaseResult(
+ id=case_id,
+ state=state,
+ message=message,
+ started_at=started_at,
+ finished_at=utc_now(),
+ duration_seconds=round(time.monotonic() - started, 3),
+ evidence=evidence,
+ )
+ self.results.append(result)
+ self.events.record("case.complete", **asdict(result))
+ label = "PASS" if state == "pass" else state.upper()
+ print(f"[{label:5}] {case_id}: {message}", flush=True)
+
+ def block_case(self, case_id: str) -> None:
+ assert self.isolation_failure is not None
+ message = (
+ "blocked to preserve the failed pool topology from "
+ f"{self.isolation_failure['caseId']}: {self.isolation_failure['message']}"
+ )
+ evidence = [
+ self.save_json(
+ case_id,
+ "blocked-by-isolation.json",
+ {"state": "infra_error", "reason": self.isolation_failure},
+ )
+ ]
+ now = utc_now()
+ self.results.append(
+ CaseResult(
+ id=case_id,
+ state="infra_error",
+ message=message,
+ started_at=now,
+ finished_at=now,
+ duration_seconds=0.0,
+ evidence=evidence,
+ )
+ )
+ self.events.record("case.blocked", case=case_id, reason=self.isolation_failure)
+ print(f"[INFRA] {case_id}: {message}", flush=True)
+
+ def finalize_physical_cleanup(self) -> None:
+ states = {item.state for item in self.results}
+ should_cleanup = not states.difference({"pass"}) or self.args.cleanup_on_failure
+ if not should_cleanup:
+ self.cleanup_outcome_value = {
+ "attempted": False,
+ "state": "preserved",
+ "message": "non-pass pool topology and data were preserved for investigation",
+ "preservedBuckets": sorted(self.created_buckets),
+ "layout": self.current_layout,
+ }
+ return
+ try:
+ reference = self.hook("run-cleanup", "physical-cleanup", self.args.cleanup_hook)
+ except Exception as exc:
+ self.cleanup_outcome_value = {
+ "attempted": True,
+ "state": "infra_error",
+ "message": self.redact(
+ f"physical cleanup failed: {type(exc).__name__}: {exc}"
+ ),
+ }
+ for result in self.results:
+ if result.state == "pass":
+ result.state = "infra_error"
+ result.message = self.cleanup_outcome_value["message"]
+ result.evidence.append(reference if "reference" in locals() else self.save_json(
+ result.id, "physical-cleanup-failure.json", self.cleanup_outcome_value
+ ))
+ break
+ return
+ self.created_buckets.clear()
+ self.cleanup_outcome_value = {
+ "attempted": True,
+ "state": "pass",
+ "message": "marker-guarded physical pool data cleanup completed",
+ "evidence": reference,
+ }
+
+ def write_junit(self, overall: str, duration: float) -> None:
+ suite = ET.Element(
+ "testsuite",
+ {
+ "name": "rustfs-fixed-environment-pool-lifecycle",
+ "tests": str(len(self.results)),
+ "failures": str(sum(item.state == "fail" for item in self.results)),
+ "errors": str(sum(item.state == "infra_error" for item in self.results)),
+ "skipped": "0",
+ "time": f"{duration:.3f}",
+ },
+ )
+ for item in self.results:
+ case = ET.SubElement(
+ suite,
+ "testcase",
+ {
+ "classname": "rustfs.pool_lifecycle",
+ "name": item.id,
+ "time": f"{item.duration_seconds:.3f}",
+ },
+ )
+ if item.state == "fail":
+ ET.SubElement(case, "failure", {"message": item.message}).text = item.message
+ elif item.state == "infra_error":
+ ET.SubElement(case, "error", {"message": item.message}).text = item.message
+ ET.SubElement(case, "system-out").text = "\n".join(item.evidence)
+ tree = ET.ElementTree(suite)
+ ET.indent(tree, space=" ")
+ tree.write(self.evidence_dir / "junit.xml", encoding="utf-8", xml_declaration=True)
+ self.events.record("run.complete", state=overall)
+
+ def write_results(self, started_at: str, started: float) -> str:
+ selected_cases = pool_harness_scored_cases(self.args.case)
+ order = {case_id: index for index, case_id in enumerate(CASE_IDS)}
+ self.results.sort(key=lambda item: order[item.id])
+ states = {item.state for item in self.results}
+ overall = "fail" if "fail" in states else "infra_error" if "infra_error" in states else "pass"
+ finished_at = utc_now()
+ manifest = {
+ "runId": self.args.run_id,
+ "environmentId": self.args.environment_id,
+ "endpoint": self.initial_endpoint,
+ "finalControlEndpoint": self.args.endpoint,
+ "nodeEndpoints": self.args.node_endpoint,
+ "startedAt": started_at,
+ "finishedAt": finished_at,
+ "durationSeconds": round(time.monotonic() - started, 3),
+ "fixtureObjects": self.args.fixture_objects,
+ "fixtureBytes": self.args.fixture_bytes,
+ "activeFixtureBytes": active_fixture_bytes(self.args.fixture_bytes),
+ "liveObjects": self.args.live_objects,
+ "boto3Version": boto3.__version__,
+ "botocoreVersion": botocore.__version__,
+ "rcBinaryPath": self.rc_bin,
+ "rcVersion": self.rc_version,
+ "rcBinarySha256": self.rc_binary_sha256,
+ "expectedRcBinarySha256": self.expected_rc_binary_sha256 or None,
+ "executionPlan": {
+ "selectionMode": "filtered" if self.args.case is not None else "full",
+ "module": "pool",
+ "scoredCases": list(selected_cases),
+ "scoredCaseCount": len(selected_cases),
+ "fixtures": [],
+ },
+ "fixtureOutcomes": [],
+ "cleanupOnFailure": self.args.cleanup_on_failure,
+ "cleanupOutcome": self.cleanup_outcome_value,
+ "preservedBuckets": sorted(self.created_buckets),
+ "finalLayout": self.current_layout,
+ }
+ (self.evidence_dir / "run-manifest.json").write_text(
+ json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8"
+ )
+ result = {
+ "apiVersion": "validation.rustfs.io/v1alpha1",
+ "kind": "TestRunResult",
+ "metadata": {
+ "runId": self.args.run_id,
+ "environmentId": self.args.environment_id,
+ },
+ "status": {
+ "state": overall,
+ "startedAt": started_at,
+ "finishedAt": finished_at,
+ "cases": [
+ {
+ "id": item.id,
+ "state": item.state,
+ "message": item.message,
+ "evidence": item.evidence,
+ }
+ for item in self.results
+ ],
+ "evidence": ["events.jsonl", "run-manifest.json", "junit.xml"],
+ },
+ }
+ (self.evidence_dir / "result.json").write_text(
+ json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8"
+ )
+ self.write_junit(overall, time.monotonic() - started)
+ return overall
+
+ def preflight_failure(self, selected_cases: tuple[str, ...], exc: BaseException) -> None:
+ message = self.redact(
+ f"pool harness preflight failed: {type(exc).__name__}: {exc}"
+ )
+ for case_id in selected_cases:
+ evidence = [self.save_json(case_id, "preflight-failure.json", {"message": message})]
+ self.results.append(
+ CaseResult(
+ id=case_id,
+ state="infra_error",
+ message=message,
+ started_at=utc_now(),
+ finished_at=utc_now(),
+ duration_seconds=0.0,
+ evidence=evidence,
+ )
+ )
+
+ def run(self) -> int:
+ started_at = utc_now()
+ started = time.monotonic()
+ selected_cases = pool_harness_scored_cases(self.args.case)
+ self.events.record(
+ "run.start",
+ runId=self.args.run_id,
+ environmentId=self.args.environment_id,
+ selectionMode="filtered" if self.args.case is not None else "full",
+ scoredCases=list(selected_cases),
+ )
+ try:
+ self.wait_s3("run-preflight", self.client, "initial S3 connectivity")
+ self.configure_rc()
+ except Exception as exc:
+ self.preflight_failure(selected_cases, exc)
+ self.finalize_physical_cleanup()
+ overall = self.write_results(started_at, started)
+ return 2 if overall == "infra_error" else 1
+
+ cases: dict[str, Callable[[str], None]] = {
+ "pool.status": self.case_status,
+ "pool.expand-and-rebalance": self.case_expand_and_rebalance,
+ "pool.expansion-restart-persistence": self.case_expansion_restart_persistence,
+ "pool.rebalance-stop-resume": self.case_rebalance_stop_resume,
+ "pool.rebalance-live-io": self.case_rebalance_live_io,
+ "pool.decommission": self.case_decommission,
+ "pool.decommission-cancel-clear-resume": self.case_decommission_cancel_clear_resume,
+ "pool.decommission-restart-recovery": self.case_decommission_restart_recovery,
+ "pool.decommission-versioned-integrity": self.case_decommission_versioned_integrity,
+ "pool.decommission-multipart-integrity": self.case_decommission_multipart_integrity,
+ "pool.decommission-live-io": self.case_decommission_live_io,
+ "pool.retired-config-removal": self.case_retired_config_removal,
+ "pool.operation-guards": self.case_operation_guards,
+ }
+ for case_id in selected_cases:
+ if self.isolation_failure is not None:
+ self.block_case(case_id)
+ else:
+ self.run_case(case_id, cases[case_id])
+ self.finalize_physical_cleanup()
+ overall = self.write_results(started_at, started)
+ print(
+ f"Pool lifecycle validation {overall}: result={self.evidence_dir / 'result.json'}",
+ flush=True,
+ )
+ return {"pass": 0, "fail": 1, "infra_error": 2}[overall]
+
+
+def parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--endpoint", default=os.environ.get("RUSTFS_POOL_ENDPOINT"))
+ parser.add_argument("--node-endpoint", action="append", required=True)
+ parser.add_argument("--access-key", default=os.environ.get("RUSTFS_ACCESS_KEY"))
+ parser.add_argument("--secret-key", default=os.environ.get("RUSTFS_SECRET_KEY"))
+ parser.add_argument("--region", default=os.environ.get("RUSTFS_REGION", "us-east-1"))
+ parser.add_argument("--environment-id", default="lifecycle-a")
+ parser.add_argument("--run-id", required=True)
+ parser.add_argument("--evidence-dir", required=True, type=Path)
+ parser.add_argument("--timeout", type=float, default=600)
+ parser.add_argument("--active-timeout", type=float, default=90)
+ parser.add_argument("--poll-interval", type=float, default=0.5)
+ parser.add_argument("--fixture-objects", type=int, default=128)
+ parser.add_argument("--fixture-bytes", type=int, default=1024 * 1024)
+ parser.add_argument("--live-objects", type=int, default=16)
+ parser.add_argument("--workers", type=int, default=8)
+ parser.add_argument("--rc-bin", default="rc")
+ parser.add_argument("--reset-hook", required=True)
+ parser.add_argument("--topology-hook", required=True)
+ parser.add_argument("--restart-hook", required=True)
+ parser.add_argument("--census-hook", required=True)
+ parser.add_argument("--diagnostic-hook")
+ parser.add_argument("--cleanup-hook", required=True)
+ parser.add_argument("--case", action="append", choices=CASE_IDS)
+ parser.add_argument("--cleanup-on-failure", action="store_true")
+ args = parser.parse_args()
+ if not args.endpoint or not args.access_key or not args.secret_key:
+ parser.error("endpoint, access key, and secret key are required")
+ if len(args.node_endpoint) not in {2, 4}:
+ parser.error("--node-endpoint must be supplied exactly two or four times")
+ for value in (args.endpoint, *args.node_endpoint):
+ parsed = urllib.parse.urlparse(value)
+ if (
+ parsed.scheme not in {"http", "https"}
+ or not parsed.hostname
+ or parsed.username is not None
+ or parsed.password is not None
+ ):
+ parser.error(f"invalid absolute HTTP(S) endpoint: {value!r}")
+ if any(character in args.access_key + args.secret_key for character in "\r\n"):
+ parser.error("access key and secret key must not contain line breaks")
+ if (
+ args.timeout <= 0
+ or args.active_timeout <= 0
+ or args.poll_interval <= 0
+ or args.fixture_objects < 4
+ or args.fixture_bytes < 1
+ or args.live_objects < 1
+ or args.workers < 1
+ ):
+ parser.error("timeouts and fixture/worker sizes must be positive")
+ return args
+
+
+def main() -> int:
+ if hasattr(sys.stdout, "reconfigure"):
+ sys.stdout.reconfigure(line_buffering=True)
+ args = parse_args()
+ harness = PoolHarness(args)
+ try:
+ return harness.run()
+ finally:
+ harness.close()
+
+
+if __name__ == "__main__":
+ try:
+ raise SystemExit(main())
+ except KeyboardInterrupt:
+ raise SystemExit(130) from None
diff --git a/scripts/validation_pool/pool_disk_probe.py b/scripts/validation_pool/pool_disk_probe.py
new file mode 100644
index 00000000..6c242186
--- /dev/null
+++ b/scripts/validation_pool/pool_disk_probe.py
@@ -0,0 +1,197 @@
+#!/usr/bin/env python3
+"""Read-only, descriptor-scoped census and allowlisted pool metadata evidence."""
+
+from __future__ import annotations
+
+import argparse
+import base64
+import bisect
+import errno
+import hashlib
+import json
+import os
+import stat
+import time
+from pathlib import Path, PurePosixPath
+from typing import Any
+
+
+DIRECTORY_FLAGS = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW
+CACHE_OBJECTS = {".usage-cache.bin", ".usage-cache.bin.bkp"}
+DIAGNOSTIC_OBJECTS = (
+ ".rustfs.sys/buckets/.scanner-pause-backlog.json",
+ ".rustfs.sys/pool.bin",
+)
+MAX_FILE_BYTES = 4 * 1024 * 1024
+MAX_OBJECT_BYTES = 16 * 1024 * 1024
+
+
+class CensusEntryMissing(FileNotFoundError):
+ """A specific child disappeared while its anchored parent was being read."""
+
+
+def _cache_path(relative: str) -> bool:
+ parts = PurePosixPath(relative).parts
+ return (
+ len(parts) >= 3
+ and parts[:2] == (".rustfs.sys", "buckets")
+ and parts[2] in CACHE_OBJECTS
+ )
+
+
+def _check_root(root: Path, descriptor: int) -> os.stat_result:
+ expected = os.fstat(descriptor)
+ observed = root.lstat()
+ if not stat.S_ISDIR(observed.st_mode) or (
+ observed.st_dev, observed.st_ino
+ ) != (expected.st_dev, expected.st_ino):
+ raise RuntimeError("census root identity changed")
+ return expected
+
+
+def _census_once(descriptor: int) -> dict[str, Any]:
+ device = os.fstat(descriptor).st_dev
+ result: dict[str, Any] = {
+ "files": 0, "directories": 1, "xlMeta": 0, "bytes": 0, "samples": [],
+ }
+
+ def walk(parent: int, prefix: str, depth: int) -> None:
+ if depth > 128:
+ raise RuntimeError("census directory depth exceeds 128")
+ with os.scandir(parent) as entries:
+ for entry in entries:
+ relative = f"{prefix}/{entry.name}" if prefix else entry.name
+ try:
+ info = os.stat(entry.name, dir_fd=parent, follow_symlinks=False)
+ if stat.S_ISDIR(info.st_mode):
+ if info.st_dev != device:
+ raise RuntimeError(f"unexpected nested mount: {relative}")
+ child = os.open(entry.name, DIRECTORY_FLAGS, dir_fd=parent)
+ try:
+ opened = os.fstat(child)
+ if (opened.st_dev, opened.st_ino) != (info.st_dev, info.st_ino):
+ raise RuntimeError(f"directory identity changed: {relative}")
+ result["directories"] += 1
+ walk(child, relative, depth + 1)
+ finally:
+ os.close(child)
+ elif stat.S_ISREG(info.st_mode):
+ result["files"] += 1
+ result["bytes"] += info.st_size
+ result["xlMeta"] += int(entry.name == "xl.meta")
+ bisect.insort(result["samples"], (relative, info.st_size))
+ del result["samples"][20:]
+ except CensusEntryMissing:
+ raise
+ except FileNotFoundError as exc:
+ raise CensusEntryMissing(errno.ENOENT, "entry disappeared", relative) from exc
+
+ walk(descriptor, "", 0)
+ return result
+
+
+def census(root: Path, *, attempts: int = 3) -> dict[str, Any]:
+ if attempts < 1:
+ raise ValueError("census attempts must be positive")
+ descriptor = os.open(root, DIRECTORY_FLAGS)
+ races: list[str] = []
+ try:
+ for attempt in range(1, attempts + 1):
+ _check_root(root, descriptor)
+ try:
+ result = _census_once(descriptor)
+ except CensusEntryMissing as exc:
+ _check_root(root, descriptor)
+ if not _cache_path(str(exc.filename)) or attempt == attempts:
+ raise
+ races.append(str(exc.filename))
+ time.sleep(0.05)
+ continue
+ _check_root(root, descriptor)
+ result.update({
+ "sampling": "non-atomic-online",
+ "attempts": attempt,
+ "retriedCachePaths": races,
+ })
+ return result
+ raise AssertionError("unreachable census retry state")
+ finally:
+ os.close(descriptor)
+
+
+def metadata_snapshot(root: Path) -> dict[str, Any]:
+ """Read only the two non-credential objects needed for migration diagnosis."""
+ descriptor = os.open(root, DIRECTORY_FLAGS)
+ records: dict[str, Any] = {}
+ try:
+ _check_root(root, descriptor)
+ for object_name in DIAGNOSTIC_OBJECTS:
+ parent = os.dup(descriptor)
+ record: dict[str, Any] = {"files": []}
+ records[object_name] = record
+ try:
+ for component in PurePosixPath(object_name).parts:
+ child = os.open(component, DIRECTORY_FLAGS, dir_fd=parent)
+ os.close(parent)
+ parent = child
+ total = 0
+ for name in sorted(os.listdir(parent)):
+ # Object metadata and inline/single-part payloads only.
+ file_descriptor = os.open(
+ name, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=parent
+ )
+ try:
+ before = os.fstat(file_descriptor)
+ if not stat.S_ISREG(before.st_mode):
+ continue
+ if before.st_size > MAX_FILE_BYTES or total + before.st_size > MAX_OBJECT_BYTES:
+ record["files"].append({"name": name, "state": "size-limit"})
+ continue
+ chunks = []
+ remaining = MAX_FILE_BYTES + 1
+ while remaining:
+ block = os.read(file_descriptor, min(65536, remaining))
+ if not block:
+ break
+ chunks.append(block)
+ remaining -= len(block)
+ data = b"".join(chunks)
+ if len(data) > MAX_FILE_BYTES or total + len(data) > MAX_OBJECT_BYTES:
+ raise RuntimeError("diagnostic file grew past size limit")
+ after = os.fstat(file_descriptor)
+ total += len(data)
+ record["files"].append({
+ "name": name,
+ "bytes": len(data),
+ "sha256": hashlib.sha256(data).hexdigest(),
+ "mtimeNs": before.st_mtime_ns,
+ "stableDuringRead": (
+ before.st_size, before.st_mtime_ns, before.st_ctime_ns
+ ) == (after.st_size, after.st_mtime_ns, after.st_ctime_ns),
+ "base64": base64.b64encode(data).decode("ascii"),
+ })
+ finally:
+ os.close(file_descriptor)
+ record["state"] = "captured"
+ except OSError as exc:
+ record.update({"state": "unavailable", "errno": exc.errno})
+ finally:
+ os.close(parent)
+ _check_root(root, descriptor)
+ return {"sampling": "non-atomic-online", "objects": records}
+ finally:
+ os.close(descriptor)
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("mode", choices=("census", "metadata"))
+ parser.add_argument("root", type=Path)
+ args = parser.parse_args()
+ result = census(args.root) if args.mode == "census" else metadata_snapshot(args.root)
+ print(json.dumps(result, sort_keys=True))
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/scripts/validation_pool/pool_helpers.py b/scripts/validation_pool/pool_helpers.py
new file mode 100644
index 00000000..cd96be15
--- /dev/null
+++ b/scripts/validation_pool/pool_helpers.py
@@ -0,0 +1,25 @@
+"""Pool topology and fixture rules used by the source behavior checks."""
+
+MIN_ACTIVE_FIXTURE_BYTES = 32 * 1024 * 1024
+
+
+def active_endpoint_indices(endpoint_count: int, layout: str) -> tuple[int, ...]:
+ if layout not in {"initial", "expanded", "retired"}:
+ raise ValueError(f"unknown pool topology layout: {layout}")
+ if endpoint_count == 2:
+ return {
+ "initial": (0,),
+ "expanded": (0, 1),
+ "retired": (1,),
+ }[layout]
+ if endpoint_count == 4:
+ return {
+ "initial": (0, 1),
+ "expanded": (0, 1, 2, 3),
+ "retired": (2, 3),
+ }[layout]
+ raise ValueError("pool lifecycle requires exactly two or four node endpoints")
+
+
+def active_fixture_bytes(configured_bytes: int) -> int:
+ return max(configured_bytes, MIN_ACTIVE_FIXTURE_BYTES)
diff --git a/scripts/validation_pool/pool_status.py b/scripts/validation_pool/pool_status.py
new file mode 100644
index 00000000..02963247
--- /dev/null
+++ b/scripts/validation_pool/pool_status.py
@@ -0,0 +1,588 @@
+"""Strict, side-effect-free validators for pool lifecycle Admin API output."""
+
+from __future__ import annotations
+
+import json
+from datetime import datetime
+from typing import Any
+
+
+class PoolStatusError(ValueError):
+ """A pool lifecycle response does not satisfy the release-gate contract."""
+
+
+def _integer(value: Any, field: str, *, minimum: int = 0) -> int:
+ if type(value) is not int or value < minimum:
+ raise PoolStatusError(f"{field} must be an integer >= {minimum}, got {value!r}")
+ return value
+
+
+def _number(value: Any, field: str) -> float:
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
+ raise PoolStatusError(f"{field} must be numeric, got {value!r}")
+ result = float(value)
+ if not 0.0 <= result <= 1.0:
+ raise PoolStatusError(f"{field} must be in 0.0..=1.0, got {value!r}")
+ return result
+
+
+def _text(value: Any, field: str, *, allow_empty: bool = False) -> str:
+ if not isinstance(value, str) or (not allow_empty and not value):
+ qualifier = "a string" if allow_empty else "a non-empty string"
+ raise PoolStatusError(f"{field} must be {qualifier}, got {value!r}")
+ return value
+
+
+def _timestamp(value: Any, field: str, *, allow_none: bool = False) -> str | None:
+ if value is None and allow_none:
+ return None
+ text = _text(value, field)
+ try:
+ parsed = datetime.fromisoformat(text[:-1] + "+00:00" if text.endswith("Z") else text)
+ except ValueError as exc:
+ raise PoolStatusError(f"{field} must be an RFC 3339 timestamp") from exc
+ if parsed.tzinfo is None:
+ raise PoolStatusError(f"{field} must include a timezone")
+ return text
+
+
+def pool_entries(document: Any) -> list[dict[str, Any]]:
+ """Return pool entries from raw API, rc list, or rc/API status shapes."""
+
+ raw: Any
+ if isinstance(document, list):
+ raw = document
+ elif isinstance(document, dict) and isinstance(document.get("pools"), list):
+ raw = document["pools"]
+ elif isinstance(document, dict) and isinstance(document.get("pool"), dict):
+ raw = [document["pool"]]
+ elif isinstance(document, dict) and "id" in document:
+ raw = [document]
+ else:
+ raise PoolStatusError("pool response must be an array or contain pools/pool")
+ if not raw:
+ raise PoolStatusError("pool response must contain at least one pool")
+ if not all(isinstance(item, dict) for item in raw):
+ raise PoolStatusError("every pool response entry must be an object")
+ return list(raw)
+
+
+def canonical_pool(entry: dict[str, Any], *, require_capacity: bool = True) -> dict[str, Any]:
+ """Validate and normalize fields shared by pool list and status output."""
+
+ result: dict[str, Any] = {
+ "id": _integer(entry.get("id"), "pool.id"),
+ "cmdline": _text(entry.get("cmdline"), "pool.cmdline"),
+ "status": _text(entry.get("status"), "pool.status"),
+ "decommissionStatus": _text(
+ entry.get("decommissionStatus"),
+ "pool.decommissionStatus",
+ allow_empty=True,
+ ),
+ "rebalanceStatus": _text(
+ entry.get("rebalanceStatus"),
+ "pool.rebalanceStatus",
+ allow_empty=True,
+ ),
+ }
+ _timestamp(entry.get("lastUpdate"), "pool.lastUpdate")
+ if require_capacity:
+ total = _integer(entry.get("totalSize"), "pool.totalSize")
+ current = _integer(entry.get("currentSize"), "pool.currentSize")
+ used_size = _integer(entry.get("usedSize"), "pool.usedSize")
+ used = _number(entry.get("used"), "pool.used")
+ if current > total or used_size > total:
+ raise PoolStatusError(
+ "pool currentSize/usedSize cannot exceed totalSize: "
+ f"total={total} current={current} used={used_size}"
+ )
+ expected_used_size = total - current
+ if used_size != expected_used_size:
+ raise PoolStatusError(
+ "pool capacity counters are inconsistent: "
+ f"total={total} current={current} used={used_size}"
+ )
+ expected_ratio = used_size / total if total else 0.0
+ if abs(used - expected_ratio) > 1e-9:
+ raise PoolStatusError(
+ "pool used ratio is inconsistent with byte counters: "
+ f"used={used} expected={expected_ratio}"
+ )
+ result.update(
+ {
+ "totalSize": total,
+ "currentSize": current,
+ "usedSize": used_size,
+ # ``used`` is derived from the integer counters. Independent
+ # JSON encoders may emit adjacent decimal representations, so
+ # normalize it after validating the supplied ratio above.
+ "used": expected_ratio,
+ }
+ )
+ return result
+
+
+def rc_failure_is_retryable(*streams: str) -> bool:
+ """Recognize an rc JSON failure that explicitly permits a bounded retry."""
+
+ if any("requires a live fleet capability proof" in stream for stream in streams):
+ return True
+ for stream in streams:
+ try:
+ document = json.loads(stream)
+ except (json.JSONDecodeError, TypeError):
+ continue
+ if not isinstance(document, dict):
+ continue
+ details = document.get("details")
+ if isinstance(details, dict) and details.get("retryable") is True:
+ return True
+ return False
+
+
+def validate_pool_inventory(
+ document: Any,
+ expected_count: int,
+ *,
+ require_active: bool = True,
+) -> tuple[dict[str, Any], ...]:
+ entries = tuple(canonical_pool(item) for item in pool_entries(document))
+ if len(entries) != expected_count:
+ raise PoolStatusError(
+ f"expected exactly {expected_count} pools, observed {len(entries)}"
+ )
+ ids = tuple(item["id"] for item in entries)
+ if len(ids) != len(set(ids)):
+ raise PoolStatusError(f"pool IDs are not unique: {ids}")
+ if ids != tuple(sorted(ids)):
+ raise PoolStatusError(f"pool IDs are not in stable ascending order: {ids}")
+ cmdlines = tuple(item["cmdline"] for item in entries)
+ if len(cmdlines) != len(set(cmdlines)):
+ raise PoolStatusError("pool command lines are not unique")
+ if require_active:
+ inactive = [
+ item["id"]
+ for item in entries
+ if item["status"].lower() != "active"
+ ]
+ if inactive:
+ raise PoolStatusError(f"expected active pools, inactive IDs: {inactive}")
+ return entries
+
+
+def assert_pool_parity(
+ left: Any,
+ right: Any,
+ expected_count: int,
+ *,
+ require_active: bool = True,
+) -> None:
+ left_entries = validate_pool_inventory(
+ left, expected_count, require_active=require_active
+ )
+ right_entries = validate_pool_inventory(
+ right, expected_count, require_active=require_active
+ )
+ if left_entries != right_entries:
+ raise PoolStatusError(
+ f"rc/Admin pool status mismatch: rc={left_entries!r} admin={right_entries!r}"
+ )
+
+
+def decommission_entry(document: Any, pool_id: int) -> dict[str, Any]:
+ matches = [
+ entry
+ for entry in pool_entries(document)
+ if entry.get("id") == pool_id
+ ]
+ if len(matches) != 1:
+ raise PoolStatusError(
+ f"decommission status must contain pool {pool_id} exactly once"
+ )
+ entry = matches[0]
+ _integer(entry.get("id"), "decommission.id")
+ _text(entry.get("cmdline"), "decommission.cmdline")
+ _text(entry.get("status"), "decommission.status")
+ _text(entry.get("poolStatus"), "decommission.poolStatus")
+ return entry
+
+
+def _decommission_info(entry: dict[str, Any]) -> dict[str, Any]:
+ info = entry.get("decommissionInfo")
+ if not isinstance(info, dict):
+ raise PoolStatusError("decommissionInfo must be an object while an operation exists")
+ for field in ("complete", "failed", "canceled", "queued"):
+ if type(info.get(field)) is not bool:
+ raise PoolStatusError(f"decommissionInfo.{field} must be a boolean")
+ for field in ("startSize", "totalSize", "currentSize"):
+ _optional_counter(info, field)
+ for field in (
+ "objectsDecommissioned",
+ "objectsDecommissionedFailed",
+ "bytesDecommissioned",
+ "bytesDecommissionedFailed",
+ ):
+ _optional_counter(info, field)
+ for field in ("queuedBuckets", "decommissionedBuckets"):
+ value = info.get(field, [])
+ if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
+ raise PoolStatusError(f"decommissionInfo.{field} must be a string array")
+ for field in ("bucket", "prefix", "object", "stage"):
+ _text(info.get(field, ""), f"decommissionInfo.{field}", allow_empty=True)
+ _timestamp(
+ info.get("startTime"), "decommissionInfo.startTime", allow_none=True
+ )
+ waiting_reason = info.get("waitingReason")
+ if waiting_reason is not None and not isinstance(waiting_reason, str):
+ raise PoolStatusError(
+ "decommissionInfo.waitingReason must be a string or null"
+ )
+ unresolved = info.get("unresolvedEntries", [])
+ if not isinstance(unresolved, list):
+ raise PoolStatusError("decommissionInfo.unresolvedEntries must be an array")
+ return info
+
+
+def _decommission_failure(entry: dict[str, Any]) -> str | None:
+ info = entry.get("decommissionInfo")
+ if not isinstance(info, dict):
+ return None
+ _decommission_info(entry)
+ status = str(entry.get("status", "")).lower()
+ if status in {"failed", "canceled", "cancelled"} or info.get("failed") is True:
+ return "decommission reported failed or canceled"
+ if _optional_counter(info, "objectsDecommissionedFailed") > 0:
+ return "decommission reported failed objects"
+ if _optional_counter(info, "bytesDecommissionedFailed") > 0:
+ return "decommission reported failed bytes"
+ unresolved = info.get("unresolvedEntries", [])
+ if unresolved is not None and not isinstance(unresolved, list):
+ return "decommission unresolvedEntries is malformed"
+ if unresolved:
+ return "decommission reported unresolved entries"
+ return None
+
+
+def _optional_counter(document: dict[str, Any], field: str) -> int:
+ value = document.get(field, 0)
+ return _integer(value, field)
+
+
+def decommission_active(document: Any, pool_id: int, *, require_progress: bool) -> bool:
+ entry = decommission_entry(document, pool_id)
+ reason = _decommission_failure(entry)
+ if reason:
+ raise PoolStatusError(reason)
+ info = _decommission_info(entry)
+ running = str(entry["status"]).lower() == "running" and str(
+ entry["poolStatus"]
+ ).lower() == "decommissioning"
+ queued = info.get("queued") is True
+ if not running or queued or info.get("complete") is True or info.get("canceled") is True:
+ return False
+ if not require_progress:
+ return True
+ return (
+ _optional_counter(info, "objectsDecommissioned") > 0
+ and _optional_counter(info, "bytesDecommissioned") > 0
+ )
+
+
+def decommission_active_or_completed(document: Any, pool_id: int) -> bool:
+ """Accept durable movement counters when decommission finishes before polling."""
+
+ return decommission_active(
+ document, pool_id, require_progress=True
+ ) or decommission_complete(document, pool_id)
+
+
+def decommission_complete(document: Any, pool_id: int) -> bool:
+ entry = decommission_entry(document, pool_id)
+ reason = _decommission_failure(entry)
+ if reason:
+ raise PoolStatusError(reason)
+ info = _decommission_info(entry)
+ queued_buckets = info.get("queuedBuckets", [])
+ if not isinstance(queued_buckets, list):
+ raise PoolStatusError("queuedBuckets must be an array")
+ terminal = (
+ info.get("complete") is True
+ and str(entry["status"]).lower() == "complete"
+ and str(entry["poolStatus"]).lower() == "decommissioned"
+ )
+ clean = (
+ info.get("failed") is False
+ and info.get("canceled") is False
+ and info.get("queued") is False
+ and not queued_buckets
+ and _optional_counter(info, "objectsDecommissionedFailed") == 0
+ and _optional_counter(info, "bytesDecommissionedFailed") == 0
+ )
+ moved = (
+ _optional_counter(info, "objectsDecommissioned") > 0
+ and _optional_counter(info, "bytesDecommissioned") > 0
+ )
+ return terminal and clean and moved
+
+
+def decommission_canceled(document: Any, pool_id: int) -> bool:
+ entry = decommission_entry(document, pool_id)
+ info = _decommission_info(entry)
+ status_canceled = str(entry["status"]).lower() in {
+ "canceled",
+ "cancelled",
+ }
+ if info.get("failed") is True:
+ raise PoolStatusError("canceled decommission also reports failed=true")
+ if info.get("complete") is True:
+ raise PoolStatusError("canceled decommission also reports complete=true")
+ if (
+ _optional_counter(info, "objectsDecommissionedFailed") > 0
+ or _optional_counter(info, "bytesDecommissionedFailed") > 0
+ or info.get("unresolvedEntries", [])
+ ):
+ raise PoolStatusError("canceled decommission reports unresolved failures")
+ return (
+ info.get("canceled") is True
+ and status_canceled
+ and info.get("queued") is False
+ # Cancel retains unfinished buckets for diagnostics; queued is the
+ # scheduling flag. Completion, unlike cancellation, drains the list.
+ and info.get("startTime") is None
+ and str(entry["poolStatus"]).lower() == "blocked"
+ )
+
+
+def decommission_cleared(document: Any, pool_id: int) -> bool:
+ entry = decommission_entry(document, pool_id)
+ return (
+ entry.get("decommissionInfo") is None
+ and str(entry["status"]).lower() in {"", "none"}
+ and str(entry["poolStatus"]).lower() == "active"
+ )
+
+
+def decommission_multipart_blocked(document: Any, pool_id: int) -> bool:
+ """Recognize a clean post-check failure; the caller must prove its MPU cause."""
+ entry = decommission_entry(document, pool_id)
+ info = _decommission_info(entry)
+ for field in ("objectsDecommissioned", "bytesDecommissioned", "objectsDecommissionedFailed", "bytesDecommissionedFailed"):
+ _integer(info.get(field), f"MPU drain.{field}")
+ if (info["complete"] or info["canceled"] or entry["status"].lower() in {"complete", "canceled", "cancelled"}
+ or entry["poolStatus"].lower() == "decommissioned"):
+ raise PoolStatusError("MPU drain protection completed or canceled unexpectedly")
+ if any(_optional_counter(info, field) for field in ("objectsDecommissionedFailed", "bytesDecommissionedFailed")) or info.get("unresolvedEntries"):
+ raise PoolStatusError("MPU drain observation contains unrelated migration failures")
+ if info["failed"] or entry["status"].lower() == "failed":
+ if not (
+ info["failed"] and entry["status"].lower() == "failed"
+ and entry["poolStatus"].lower() == "blocked" and not info["queued"]
+ and info.get("startTime") is None and info.get("queuedBuckets") == []
+ and _optional_counter(info, "objectsDecommissioned") > 0
+ and _optional_counter(info, "bytesDecommissioned") > 0
+ ):
+ raise PoolStatusError("MPU drain failure has inconsistent terminal state")
+ return True
+ return False
+
+
+def multipart_drain_failure_records(
+ document: Any, *, bucket: str, since: str, node_count: int,
+) -> list[dict[str, Any]]:
+ """Require fresh, scoped journal evidence before accepting an expected failure."""
+ if not isinstance(document, dict) or document.get("mode") != "decommission-journal" or document.get("since") != since:
+ raise PoolStatusError("MPU drain journal has the wrong collection identity")
+ nodes = document.get("nodes")
+ if not isinstance(nodes, dict) or len(nodes) != node_count or not all(isinstance(node, str) and node for node in nodes):
+ raise PoolStatusError("MPU drain journal omitted a node")
+ _timestamp(since, "MPU journal.since")
+ lower_bound = datetime.fromisoformat(since.replace("Z", "+00:00"))
+ matches = []
+ for node, output in nodes.items():
+ if not isinstance(output, str):
+ raise PoolStatusError("MPU drain journal node output must be text")
+ for line in output.splitlines():
+ if not line.startswith("{"):
+ continue
+ try:
+ record = json.loads(line)
+ except json.JSONDecodeError as exc:
+ raise PoolStatusError("MPU drain journal contains malformed JSON") from exc
+ if not isinstance(record, dict) or record.get("event") != "decommission_state" or record.get("state") != "routine_failed":
+ continue
+ _timestamp(record.get("timestamp"), "MPU failure.timestamp")
+ if datetime.fromisoformat(record["timestamp"].replace("Z", "+00:00")) < lower_bound:
+ continue
+ error = record.get("error")
+ if not (
+ type(record.get("pool_index")) is int and record["pool_index"] == 0
+ and isinstance(error, str) and "post-check failed:" in error
+ and "pool 0 still contains multipart upload `" in error
+ and f"` for bucket `{bucket}`; resolve it before retrying decommission" in error
+ ):
+ raise PoolStatusError("unexpected failure instead of the owned MPU drain protection")
+ matches.append({**record, "node": node})
+ if not matches:
+ raise PoolStatusError("missing fresh MPU drain failure evidence for the owned bucket")
+ return matches
+
+
+def rebalance_entries(
+ document: Any,
+ expected_id: str,
+ *,
+ allow_stopping: bool = False,
+ allow_stopped: bool = False,
+) -> tuple[dict[str, Any], ...]:
+ if not isinstance(document, dict):
+ raise PoolStatusError("rebalance status must be an object")
+ operation_id = _text(document.get("id"), "rebalance.id")
+ if operation_id != expected_id:
+ raise PoolStatusError(
+ f"rebalance operation ID changed: expected {expected_id!r}, got {operation_id!r}"
+ )
+ pools = document.get("pools")
+ if not isinstance(pools, list) or not pools or not all(
+ isinstance(pool, dict) for pool in pools
+ ):
+ raise PoolStatusError("rebalance status must contain non-empty object pools")
+ ids: list[int] = []
+ for pool in pools:
+ ids.append(_integer(pool.get("id"), "rebalance.pool.id"))
+ state = _text(pool.get("status"), "rebalance.pool.status")
+ _number(pool.get("used"), "rebalance.pool.used")
+ stopping = pool.get("stopping")
+ if stopping is not None and type(stopping) is not bool:
+ raise PoolStatusError("rebalance.pool.stopping must be a boolean")
+ if state.lower() in {"failed", "canceled", "cancelled"}:
+ raise PoolStatusError(f"rebalance entered unsuccessful state {state!r}")
+ if state.lower() == "stopped" and not allow_stopped:
+ raise PoolStatusError("rebalance stopped before successful completion")
+ if pool.get("stopping") is True and not allow_stopping:
+ raise PoolStatusError("rebalance still reports stopping=true")
+ last_error = pool.get("lastError")
+ if last_error not in {None, ""}:
+ raise PoolStatusError(f"rebalance reported lastError={last_error!r}")
+ warnings = pool.get("cleanupWarnings")
+ if not isinstance(warnings, dict):
+ raise PoolStatusError("rebalance cleanupWarnings must be an object")
+ warning_count = _optional_counter(warnings, "count")
+ warning_details: list[str] = []
+ for field in ("lastMsg", "lastBucket", "lastObject", "lastAt"):
+ value = warnings.get(field)
+ if value is not None and not isinstance(value, str):
+ raise PoolStatusError(
+ f"rebalance cleanupWarnings.{field} must be a string or null"
+ )
+ if isinstance(value, str) and value:
+ warning_details.append(value)
+ _timestamp(
+ warnings.get("lastAt"),
+ "rebalance cleanupWarnings.lastAt",
+ allow_none=True,
+ )
+ warning_entries = warnings.get("entries", [])
+ if not isinstance(warning_entries, list) or not all(
+ isinstance(item, dict) for item in warning_entries
+ ):
+ raise PoolStatusError(
+ "rebalance cleanupWarnings.entries must be an object array"
+ )
+ if warning_count != 0:
+ raise PoolStatusError("rebalance reported cleanup warnings")
+ if warning_details or warning_entries:
+ raise PoolStatusError(
+ "rebalance reported cleanup warning details with count=0"
+ )
+ if len(ids) != len(set(ids)):
+ raise PoolStatusError(f"rebalance pool IDs are duplicated: {ids}")
+ propagation = document.get("stopPropagation")
+ if propagation is not None:
+ if not isinstance(propagation, dict):
+ raise PoolStatusError("stopPropagation must be an object")
+ for field in ("failedPeers", "terminalReloadFailedPeers"):
+ peers = propagation.get(field, [])
+ if not isinstance(peers, list) or not all(
+ isinstance(peer, str) for peer in peers
+ ):
+ raise PoolStatusError(
+ f"stopPropagation.{field} must be a string array"
+ )
+ if peers:
+ raise PoolStatusError(f"stopPropagation.{field} is not empty")
+ pending_reload = propagation.get("pendingTerminalReload", False)
+ if type(pending_reload) is not bool:
+ raise PoolStatusError(
+ "stopPropagation.pendingTerminalReload must be a boolean"
+ )
+ if pending_reload:
+ raise PoolStatusError("rebalance has a pending terminal reload")
+ for field in ("lastAttemptAt", "terminalReloadAttemptAt"):
+ _timestamp(
+ propagation.get(field),
+ f"stopPropagation.{field}",
+ allow_none=True,
+ )
+ stopped_at = document.get("stoppedAt")
+ _timestamp(stopped_at, "rebalance.stoppedAt", allow_none=True)
+ return tuple(pools)
+
+
+def _rebalance_progress(pool: dict[str, Any]) -> int:
+ progress = pool.get("progress")
+ if progress is None:
+ return 0
+ if not isinstance(progress, dict):
+ raise PoolStatusError("rebalance progress must be an object or null")
+ moved = sum(
+ _optional_counter(progress, field)
+ for field in ("objects", "versions", "bytes")
+ )
+ for field in ("remainingBuckets", "elapsed", "eta"):
+ _optional_counter(progress, field)
+ for field in ("bucket", "object"):
+ _text(progress.get(field, ""), f"rebalance.progress.{field}", allow_empty=True)
+ return moved
+
+
+def rebalance_active(document: Any, expected_id: str, *, require_progress: bool) -> bool:
+ pools = rebalance_entries(document, expected_id)
+ active = [pool for pool in pools if str(pool["status"]).lower() == "started"]
+ if not active:
+ return False
+ return not require_progress or any(_rebalance_progress(pool) > 0 for pool in active)
+
+
+def rebalance_active_or_completed(document: Any, expected_id: str) -> bool:
+ """Accept observable progress even when a small rebalance finishes before polling."""
+
+ return rebalance_active(
+ document, expected_id, require_progress=True
+ ) or rebalance_complete(document, expected_id)
+
+
+def rebalance_complete(document: Any, expected_id: str) -> bool:
+ pools = rebalance_entries(document, expected_id)
+ completed = [pool for pool in pools if str(pool["status"]).lower() == "completed"]
+ all_terminal = all(
+ str(pool["status"]).lower() in {"completed", "none"} for pool in pools
+ )
+ return all_terminal and bool(completed) and any(
+ _rebalance_progress(pool) > 0 for pool in completed
+ )
+
+
+def rebalance_stopped(document: Any, expected_id: str) -> bool:
+ pools = rebalance_entries(
+ document,
+ expected_id,
+ allow_stopping=True,
+ allow_stopped=True,
+ )
+ stopped_at = document.get("stoppedAt")
+ if not isinstance(stopped_at, str) or not stopped_at:
+ return False
+ return not any(pool.get("stopping") is True for pool in pools) and all(
+ str(pool["status"]).lower() in {"stopped", "completed", "none"}
+ for pool in pools
+ ) and any(str(pool["status"]).lower() == "stopped" for pool in pools)
diff --git a/scripts/validation_pool/rc_binary.py b/scripts/validation_pool/rc_binary.py
new file mode 100644
index 00000000..6bc1068e
--- /dev/null
+++ b/scripts/validation_pool/rc_binary.py
@@ -0,0 +1,10 @@
+"""Share the pinned rc executable resolver with ILM validation."""
+
+from __future__ import annotations
+
+import sys
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
+
+from scripts.validation_ilm.rc_binary import resolve_rc_binary # noqa: E402
diff --git a/scripts/validation_pool/run_case.sh b/scripts/validation_pool/run_case.sh
new file mode 100644
index 00000000..89a7d7c7
--- /dev/null
+++ b/scripts/validation_pool/run_case.sh
@@ -0,0 +1,236 @@
+#!/usr/bin/env bash
+set -Eeuo pipefail
+
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+RUSTFS_POOL_SCRIPT_SOURCE_ONLY=1 source "${ROOT}/rustfs_pool_expand.sh"
+export RUSTFS_POOL_VALIDATION_MODE=1
+RUSTFS_OBS_LOGGER_LEVEL=info
+
+[ "${#NODES[@]}" -ge 2 ] || die "pool validation needs two auto-testing nodes"
+NODES=("${NODES[0]}" "${NODES[1]}")
+NODE_ENDPOINTS=(http://rustfs-node1:9000 http://rustfs-node2:9000)
+LEGACY_ENDPOINT="${NODE_ENDPOINTS[0]}"
+ADMIN_ENDPOINT="${NODE_ENDPOINTS[0]}"
+WARP_ENDPOINT="${NODE_ENDPOINTS[0]}"
+ASSUME_YES=1
+resolve_endpoints
+
+POOL1_VOLUMES="http://rustfs-node2:9000/data/rustfs{1...4}/mnmd"
+MARKER=/etc/rustfs-pool-validation-id
+
+check_marker() {
+ local node
+ [ -n "${POOL_VALIDATION_RUN_ID:-}" ] || die "pool run identity missing"
+ for node in "${NODES[@]}"; do
+ ssh "${SSH_OPTS[@]}" "${SSH_USER}@${node}" \
+ "test \"\$(cat ${MARKER})\" = \"${POOL_VALIDATION_RUN_ID}\"" \
+ || die "pool environment marker does not match on ${node}"
+ done
+}
+
+mark_nodes() {
+ local node
+ for node in "${NODES[@]}"; do
+ printf 'printf %%s %q | sudo -n tee %s >/dev/null\n' \
+ "${POOL_VALIDATION_RUN_ID}" "${MARKER}" | run_remote "${node}"
+ done
+}
+
+reset_initial() {
+ local node
+ for node in "${NODES[@]}"; do service_action stop "${node}"; done
+ for node in "${NODES[@]}"; do
+ run_remote "${node}" <<'REMOTE'
+set -euo pipefail
+SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo -n"
+for i in 1 2 3 4; do
+ ${SUDO} rm -rf "/data/rustfs${i}/mnmd"
+ ${SUDO} install -d -o rustfs -g rustfs "/data/rustfs${i}/mnmd"
+done
+REMOTE
+ done
+ write_rustfs_config "${NODES[0]}" "${VOLUMES_1}"
+ start_and_wait_nodes "${NODES[0]}"
+ wait_node_ready 0
+}
+
+retire_pool_config() {
+ service_action stop "${NODES[0]}"
+ service_action stop "${NODES[1]}"
+ write_rustfs_config "${NODES[1]}" "${POOL1_VOLUMES}"
+ ensure_volume_dirs "${NODES[1]}" "${POOL1_VOLUMES}"
+ start_and_wait_nodes "${NODES[1]}"
+ wait_node_ready 1
+}
+
+disk_probe() {
+ local mode="$1" phase="$2" node index path
+ printf 'phase=%s mode=%s\n' "${phase}" "${mode}"
+ for node in "${NODES[@]}"; do
+ for index in 1 2 3 4; do
+ path="/data/rustfs${index}/mnmd"
+ printf 'node=%s path=%s\n' "${node}" "${path}"
+ {
+ printf 'set -euo pipefail\n'
+ printf 'findmnt -rn -T %q -o TARGET,SOURCE,FSTYPE,UUID\n' "${path}"
+ printf "python3 - %q %q <<'POOL_DISK_PROBE'\n" "${mode}" "${path}"
+ cat "${ROOT}/scripts/validation_pool/pool_disk_probe.py"
+ printf '\nPOOL_DISK_PROBE\n'
+ } | run_remote "${node}"
+ done
+ done
+}
+
+pool_hook() {
+ local action="${1:-}" layout="" phase="" since="" node output0 output1 journal_since
+ shift || true
+ while [ "$#" -gt 0 ]; do
+ case "$1" in
+ --layout) layout="$2"; shift 2 ;;
+ --phase) phase="$2"; shift 2 ;;
+ --since) since="$2"; shift 2 ;;
+ *) die "unknown pool hook option: $1" ;;
+ esac
+ done
+ check_marker
+ case "${action}" in
+ reset) reset_initial ;;
+ topology)
+ case "${layout}" in
+ initial) reset_initial ;;
+ expanded) step5_expand_pool2 ;;
+ retired) retire_pool_config ;;
+ *) die "unknown pool layout: ${layout}" ;;
+ esac ;;
+ restart)
+ case "${layout}" in
+ expanded)
+ service_action_all restart
+ wait_node_ready 0
+ wait_node_ready 1 ;;
+ retired)
+ service_action restart "${NODES[1]}"
+ wait_node_ready 1 ;;
+ *) die "unknown pool restart layout: ${layout}" ;;
+ esac ;;
+ census) disk_probe census "${phase}" ;;
+ diagnostic)
+ if [ -n "${since}" ]; then
+ journal_since="$(python3 -c 'from datetime import datetime,timezone; import sys; value=datetime.fromisoformat(sys.argv[1].replace("Z","+00:00")); assert value.tzinfo is not None; print(value.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M:%S.%f UTC"))' "${since}")"
+ output0="$(ssh "${SSH_OPTS[@]}" "${SSH_USER}@${NODES[0]}" \
+ "journalctl -u rustfs --since $(printf '%q' "${journal_since}") --no-pager --output=cat -n 2000")"
+ output1="$(ssh "${SSH_OPTS[@]}" "${SSH_USER}@${NODES[1]}" \
+ "journalctl -u rustfs --since $(printf '%q' "${journal_since}") --no-pager --output=cat -n 2000")"
+ jq -n --arg phase "${phase}" --arg since "${since}" \
+ --arg node0 "${NODES[0]}" --arg node1 "${NODES[1]}" \
+ --arg output0 "${output0}" --arg output1 "${output1}" \
+ '{environmentId:"lifecycle-a",phase:$phase,mode:"decommission-journal",since:$since,nodes:{($node0):$output0,($node1):$output1}}'
+ else
+ disk_probe metadata "${phase}"
+ fi ;;
+ cleanup) step0_reset ;;
+ *) die "unknown pool hook: ${action}" ;;
+ esac
+}
+
+hook_json() {
+ python3 -c 'import json,sys; print(json.dumps(sys.argv[1:]))' \
+ bash "${ROOT}/scripts/validation_pool/run_case.sh" hook "$1"
+}
+
+setup_environment() {
+ step0_reset
+ step1_download
+ step2_install
+ step3_start_first_pool
+ mark_nodes
+}
+
+run_validation() {
+ local case_id="" candidate="" testing_commit="" artifacts="" setup_rc=0 harness_rc=0 cleanup_rc=0
+ while [ "$#" -gt 0 ]; do
+ case "$1" in
+ --validation-case) case_id="$2"; shift 2 ;;
+ --candidate-manifest) candidate="$2"; shift 2 ;;
+ --testing-commit) testing_commit="$2"; shift 2 ;;
+ --validation-artifacts-dir) artifacts="$2"; shift 2 ;;
+ --dry-run) DRY_RUN=1; shift ;;
+ -y|--yes) shift ;;
+ *) die "unknown pool validation option: $1" ;;
+ esac
+ done
+ [[ "${case_id}" == pool.* ]] || die "pool validation case required"
+ python3 -B -c 'import sys; sys.path.insert(0,sys.argv[1]); from scripts.validation_pool.case_plan import POOL_CASE_IDS; sys.exit(0 if sys.argv[2] in POOL_CASE_IDS else 2)' \
+ "${ROOT}" "${case_id}" || die "unknown canonical pool case: ${case_id}"
+ if [ "${DRY_RUN}" -eq 1 ]; then
+ POOL_VALIDATION_RUN_ID="${case_id}-dry-run"
+ setup_environment
+ return 0
+ fi
+ [ -f "${candidate}" ] || die "candidate manifest missing"
+ [ -n "${artifacts}" ] && [ ! -e "${artifacts}" ] || die "fresh artifact directory required"
+ [ "${testing_commit}" = "$(git -C "${ROOT}" rev-parse HEAD)" ] || die "testing commit differs from checkout"
+ PACKAGE_URL="$(jq -er '.package_url' "${candidate}")"
+ PACKAGE_SHA256="$(jq -er '.package_sha256' "${candidate}")"
+ [[ "${PACKAGE_SHA256}" =~ ^[0-9a-f]{64}$ ]] || die "invalid candidate SHA-256"
+ export RUSTFS_NODES="${NODES[*]}" RUSTFS_ADMIN_ENDPOINT="${NODE_ENDPOINTS[0]}"
+ export POOL_VALIDATION_RUN_ID="${case_id}-$(date +%s)-$$"
+ mkdir -p "${artifacts}/cases" "${artifacts}/evidence/${case_id}/pool"
+ local evidence="${artifacts}/evidence/${case_id}" harness="${artifacts}/evidence/${case_id}/pool"
+ python3 "${ROOT}/scripts/validation_result.py" --case-id "${case_id}" \
+ --profile lifecycle-a --result-file "${artifacts}/cases/${case_id}.json" \
+ --verdict RUNNING --cleanup-status PENDING
+ (setup_environment) > "${evidence}/setup.log" 2>&1 || setup_rc=$?
+ if [ "${setup_rc}" -ne 0 ]; then
+ cat "${evidence}/setup.log"
+ (step0_reset) > "${evidence}/cleanup.log" 2>&1 || true
+ return 1
+ fi
+ local binary_sha
+ binary_sha="$(ssh "${SSH_OPTS[@]}" "${SSH_USER}@${NODES[0]}" sha256sum /usr/bin/rustfs | awk '{print $1}')"
+ [[ "${binary_sha}" =~ ^[0-9a-f]{64}$ ]] || die "installed RustFS binary SHA-256 unavailable"
+ python3 -B "${ROOT}/scripts/validation_pool/pool_behavior.py" \
+ --case "${case_id}" --endpoint "${NODE_ENDPOINTS[0]}" \
+ --node-endpoint "${NODE_ENDPOINTS[0]}" --node-endpoint "${NODE_ENDPOINTS[1]}" \
+ --access-key "${ACCESS_KEY}" --secret-key "${SECRET_KEY}" \
+ --run-id "${POOL_VALIDATION_RUN_ID}" --evidence-dir "${harness}" \
+ --rc-bin "$(command -v rc)" --cleanup-on-failure \
+ --reset-hook "$(hook_json reset)" --topology-hook "$(hook_json topology)" \
+ --restart-hook "$(hook_json restart)" --census-hook "$(hook_json census)" \
+ --diagnostic-hook "$(hook_json diagnostic)" --cleanup-hook "$(hook_json cleanup)" \
+ > "${evidence}/test.log" 2>&1 || harness_rc=$?
+ cat "${evidence}/test.log"
+ if ! jq -e '.cleanupOutcome.state == "pass"' "${harness}/run-manifest.json" >/dev/null 2>&1; then
+ (pool_hook cleanup) > "${evidence}/fallback-cleanup.log" 2>&1 || cleanup_rc=$?
+ fi
+ local verdict=FAIL reason="pool behavior assertions failed; inspect evidence" cleanup_status=FAIL
+ if jq -e '.cleanupOutcome.state == "pass"' "${harness}/run-manifest.json" >/dev/null 2>&1; then
+ cleanup_status=PASS
+ fi
+ if [ "${harness_rc}" -eq 0 ] && [ "${cleanup_rc}" -eq 0 ] && \
+ python3 -B "${ROOT}/scripts/validation_ilm/verify_result.py" "${case_id}" "${harness}"; then
+ verdict=PASS; reason=""
+ elif jq -e '.status.state == "infra_error"' "${harness}/result.json" >/dev/null 2>&1; then
+ verdict=INFRA_ERROR; reason="pool environment or execution infrastructure failed; inspect evidence"
+ fi
+ local args=("${ROOT}/scripts/validation_result.py" --case-id "${case_id}"
+ --profile lifecycle-a --result-file "${artifacts}/cases/${case_id}.json"
+ --verdict "${verdict}" --cleanup-status "${cleanup_status}"
+ --candidate-manifest "${candidate}" --rc-bin "$(command -v rc)"
+ --testing-commit "${testing_commit}" --binary-sha256 "${binary_sha}"
+ --evidence "evidence/${case_id}/setup.log" --evidence "evidence/${case_id}/test.log")
+ if [ -n "${reason}" ]; then args+=(--reason "${reason}"); fi
+ if [ "${verdict}" = PASS ]; then
+ local file
+ while IFS= read -r -d '' file; do
+ args+=(--evidence "${file#"${artifacts}/"}")
+ done < <(find "${harness}" -type f -print0 | sort -z)
+ fi
+ python3 "${args[@]}"
+ [ "${verdict}" = PASS ]
+}
+
+case "${1:-}" in
+ hook) shift; pool_hook "$@" ;;
+ *) run_validation "$@" ;;
+esac
diff --git a/scripts/validation_replication/case_plan.py b/scripts/validation_replication/case_plan.py
new file mode 100644
index 00000000..061e5993
--- /dev/null
+++ b/scripts/validation_replication/case_plan.py
@@ -0,0 +1,28 @@
+"""Select replication cases and their source setup fixture."""
+
+from __future__ import annotations
+
+import json
+from pathlib import Path
+
+
+CATALOG = Path(__file__).resolve().parents[2] / "validation-cases.json"
+
+
+def module_cases(module_id: str) -> tuple[str, ...]:
+ catalog = json.loads(CATALOG.read_text(encoding="utf-8"))
+ modules = [module for module in catalog["modules"] if module["id"] == module_id]
+ if len(modules) != 1:
+ raise ValueError(f"canonical replication module missing or duplicated: {module_id}")
+ return tuple(case["id"] for case in modules[0]["cases"])
+
+
+BUCKET_CASE_IDS = module_cases("bucket-replication")
+SITE_CASE_IDS = module_cases("site-replication")
+SITE_GROUP_ACTIVE_FIXTURE = "site-group-active"
+SITE_GROUP_ACTIVE_CASE_IDS = frozenset(SITE_CASE_IDS) - {
+ "site-replication.unconfigured-info",
+ "site-replication.invalid-credentials",
+ "site-replication.add-and-info",
+ "site-replication.service-account-rotation",
+}
diff --git a/scripts/validation_replication/rc_binary.py b/scripts/validation_replication/rc_binary.py
new file mode 100644
index 00000000..6bc1068e
--- /dev/null
+++ b/scripts/validation_replication/rc_binary.py
@@ -0,0 +1,10 @@
+"""Share the pinned rc executable resolver with ILM validation."""
+
+from __future__ import annotations
+
+import sys
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
+
+from scripts.validation_ilm.rc_binary import resolve_rc_binary # noqa: E402
diff --git a/scripts/validation_replication/replication_behavior.py b/scripts/validation_replication/replication_behavior.py
new file mode 100644
index 00000000..58d009f8
--- /dev/null
+++ b/scripts/validation_replication/replication_behavior.py
@@ -0,0 +1,3699 @@
+#!/usr/bin/env python3
+"""Run bucket- and site-replication black-box checks against two RustFS sites."""
+
+from __future__ import annotations
+
+import argparse
+import base64
+import hashlib
+import json
+import os
+import secrets
+import shutil
+import subprocess
+import sys
+import tempfile
+import threading
+import time
+import urllib.error
+import urllib.parse
+import urllib.request
+import xml.etree.ElementTree as ET
+from dataclasses import asdict, dataclass, field
+from datetime import UTC, datetime
+from pathlib import Path
+from typing import Any, Callable
+
+from case_plan import (
+ BUCKET_CASE_IDS,
+ SITE_CASE_IDS,
+ SITE_GROUP_ACTIVE_CASE_IDS,
+ SITE_GROUP_ACTIVE_FIXTURE,
+)
+from rc_binary import resolve_rc_binary
+from replication_metrics import replication_metrics_ready
+
+try:
+ import boto3
+ import botocore
+ from botocore.auth import SigV4Auth
+ from botocore.awsrequest import AWSRequest
+ from botocore.config import Config
+ from botocore.credentials import Credentials
+ from botocore.exceptions import (
+ ClientError,
+ ConnectTimeoutError,
+ EndpointConnectionError,
+ ReadTimeoutError,
+ )
+except ImportError as exc: # pragma: no cover - dependency guidance
+ raise SystemExit(
+ "boto3 is required for replication validation"
+ ) from exc
+
+
+ADMIN_PREFIX = "/rustfs/admin/v3"
+INFRA_EXCEPTIONS = (ConnectTimeoutError, EndpointConnectionError, ReadTimeoutError)
+INFRA_ERROR_CODES = {
+ "InvalidAccessKeyId",
+ "InvalidToken",
+ "RequestTimeout",
+ "ServiceUnavailable",
+ "SignatureDoesNotMatch",
+ "SlowDown",
+}
+
+def utc_now() -> str:
+ return datetime.now(UTC).isoformat().replace("+00:00", "Z")
+
+
+def safe_name(value: str) -> str:
+ return "".join(char if char.isalnum() or char in "-." else "-" for char in value)
+
+
+def json_default(value: Any) -> Any:
+ if isinstance(value, datetime):
+ return value.astimezone(UTC).isoformat().replace("+00:00", "Z")
+ if isinstance(value, bytes):
+ return {"bytes": len(value), "sha256": hashlib.sha256(value).hexdigest()}
+ return str(value)
+
+
+def without_response_metadata(value: dict[str, Any]) -> dict[str, Any]:
+ return {key: item for key, item in value.items() if key != "ResponseMetadata"}
+
+
+@dataclass(frozen=True)
+class Site:
+ id: str
+ endpoint: str
+ access_key: str
+ secret_key: str
+
+
+@dataclass
+class CaseResult:
+ id: str
+ state: str
+ message: str
+ started_at: str
+ finished_at: str
+ duration_seconds: float
+ evidence: list[str] = field(default_factory=list)
+
+
+class BucketCleanupResidual(RuntimeError):
+ """A logical test bucket survived best-effort cleanup before phase reset."""
+
+ def __init__(
+ self,
+ bucket: str,
+ last_error: BaseException | None,
+ inventory: dict[str, Any],
+ ):
+ self.bucket = bucket
+ self.inventory = inventory
+ super().__init__(f"{bucket} remained non-empty after cleanup retries: {last_error}")
+
+
+class LogicalCleanupFailure(RuntimeError):
+ """One case's logical cleanup failed, with isolation evidence persisted."""
+
+ def __init__(
+ self,
+ *,
+ residuals: list[dict[str, Any]],
+ errors: list[dict[str, Any]],
+ evidence: str,
+ tracked_buckets: list[dict[str, str]],
+ isolation_restored: bool,
+ ) -> None:
+ self.residuals = residuals
+ self.errors = errors
+ self.evidence = evidence
+ self.tracked_buckets = tracked_buckets
+ self.isolation_restored = isolation_restored
+ super().__init__(
+ "logical bucket cleanup failed: "
+ f"residuals={len(residuals)} errors={len(errors)} "
+ f"isolation_restored={isolation_restored} evidence={evidence}"
+ )
+
+
+class PreconditionError(RuntimeError):
+ """A diagnostic fixture could not establish a trustworthy product state."""
+
+
+def _site_endpoint_identity(endpoint: str) -> tuple[str, int] | None:
+ """Return RustFS' product identity for one site endpoint.
+
+ RustFS intentionally keys a site by lower-case host plus effective port;
+ transport scheme and URL path are not part of the persisted identity.
+ Keeping the test harness on that exact contract prevents it from trying to
+ add two URLs that the product will collapse into one deployment.
+ """
+
+ if not isinstance(endpoint, str):
+ return None
+ try:
+ parsed = urllib.parse.urlsplit(endpoint)
+ scheme = parsed.scheme.lower()
+ host = (parsed.hostname or "").lower()
+ port = parsed.port
+ except ValueError:
+ return None
+ if scheme not in {"http", "https"} or not host:
+ return None
+ if port is None:
+ port = 80 if scheme == "http" else 443
+ return host, port
+
+
+def classify_site_group_state(
+ info_by_site: dict[str, Any],
+ expected_endpoints: tuple[str, str],
+) -> tuple[str, str]:
+ """Classify the two-site control-plane snapshot without mutating it."""
+
+ expected_identities = tuple(
+ _site_endpoint_identity(endpoint) for endpoint in expected_endpoints
+ )
+ if any(identity is None for identity in expected_identities):
+ return "malformed", "a configured validation endpoint is invalid"
+ if len(set(expected_identities)) != 2:
+ return "malformed", "the configured validation endpoints are not distinct"
+
+ expected_sites = ("site-a", "site-b")
+ if any(not isinstance(info_by_site.get(site_id), dict) for site_id in expected_sites):
+ return "malformed", "site info is missing or is not an object"
+
+ snapshots = [info_by_site[site_id] for site_id in expected_sites]
+ enabled = [snapshot.get("enabled") for snapshot in snapshots]
+ if all(value is False for value in enabled):
+ stale = [
+ expected_sites[index]
+ for index, snapshot in enumerate(snapshots)
+ if snapshot.get("serviceAccountAccessKey") not in (None, "")
+ or snapshot.get("pendingOperation") is not None
+ or snapshot.get("sites") not in (None, [])
+ or _site_retry_state_is_degraded(snapshot.get("retryStats"))
+ ]
+ if stale:
+ return "malformed", f"disabled site(s) retain replication state: {', '.join(stale)}"
+ return "disabled", "both sites are cleanly unconfigured"
+
+ if not all(value is True for value in enabled):
+ return "partial", f"site enabled flags differ or are invalid: {enabled!r}"
+
+ if any(snapshot.get("pendingOperation") is not None for snapshot in snapshots):
+ return "pending", "a site-replication operation is still pending"
+
+ degraded = [
+ expected_sites[index]
+ for index, snapshot in enumerate(snapshots)
+ if _site_retry_state_is_degraded(snapshot.get("retryStats"))
+ ]
+ if degraded:
+ return "degraded", f"site retry state is not clear: {', '.join(degraded)}"
+
+ expected_identity_set = set(expected_identities)
+
+ peer_maps: list[dict[tuple[str, int], str]] = []
+ service_accounts: list[str] = []
+ for site_id, snapshot in zip(expected_sites, snapshots, strict=True):
+ service_account = snapshot.get("serviceAccountAccessKey")
+ if not isinstance(service_account, str) or not service_account:
+ return "malformed", f"{site_id} has no site-replication service account"
+ service_accounts.append(service_account)
+ peers = snapshot.get("sites")
+ if not isinstance(peers, list) or len(peers) != 2:
+ return "malformed", f"{site_id} peer membership does not contain exactly two entries"
+ if not all(isinstance(peer, dict) for peer in peers):
+ return "malformed", f"{site_id} peer membership contains a non-object entry"
+ peer_map: dict[tuple[str, int], str] = {}
+ for peer in peers:
+ deployment_id = peer.get("deploymentID")
+ endpoint = peer.get("endpoint")
+ sync = peer.get("sync")
+ if not isinstance(deployment_id, str) or not deployment_id:
+ return "malformed", f"{site_id} has an invalid deployment ID"
+ identity = _site_endpoint_identity(endpoint)
+ if identity is None:
+ return "malformed", f"{site_id} has an invalid peer endpoint"
+ if not isinstance(sync, str) or sync.lower() != "enable":
+ return "malformed", f"{site_id} has a peer without sync=enable"
+ if identity in peer_map:
+ return "malformed", f"{site_id} reports a duplicate peer endpoint"
+ peer_map[identity] = deployment_id
+ if len(set(peer_map.values())) != 2:
+ return "malformed", f"{site_id} does not report two unique deployment IDs"
+ if set(peer_map) != expected_identity_set:
+ return "malformed", (
+ f"{site_id} site-group endpoints do not match the configured "
+ "validation sites"
+ )
+ peer_maps.append(peer_map)
+
+ if peer_maps[0] != peer_maps[1]:
+ return "malformed", "the two sites report different endpoint/deployment membership"
+ if len(set(service_accounts)) != 1:
+ return "malformed", "the two sites report different service-account access keys"
+ return "active", "both sites report one stable two-deployment group"
+
+
+def site_group_has_ilm_expiry_value(
+ info_by_site: dict[str, Any],
+ expected: bool,
+) -> tuple[bool, str]:
+ """Require every peer view to expose one exact ILM-expiry flag value."""
+
+ for site_id in ("site-a", "site-b"):
+ snapshot = info_by_site.get(site_id)
+ if not isinstance(snapshot, dict):
+ return False, f"{site_id} site info is missing or malformed"
+ peers = snapshot.get("sites")
+ if not isinstance(peers, list) or len(peers) != 2:
+ return False, f"{site_id} does not expose exactly two site peers"
+ for peer in peers:
+ if not isinstance(peer, dict):
+ return False, f"{site_id} contains a non-object site peer"
+ if peer.get("replicate-ilm-expiry") is not expected:
+ return False, (
+ f"{site_id} peer {peer.get('deploymentID')!r} does not expose "
+ f"replicate-ilm-expiry={str(expected).lower()}"
+ )
+ return True, (
+ "all site peers expose "
+ f"replicate-ilm-expiry={str(expected).lower()}"
+ )
+
+
+def _site_retry_state_is_degraded(value: Any) -> bool:
+ if value is None:
+ return False
+ if not isinstance(value, dict):
+ return True
+ pending = value.get("pending", 0)
+ failed = value.get("failed", 0)
+ last_error = value.get("lastError", "")
+ return (
+ not isinstance(pending, int)
+ or isinstance(pending, bool)
+ or pending < 0
+ or not isinstance(failed, int)
+ or isinstance(failed, bool)
+ or failed < 0
+ or pending != 0
+ or failed != 0
+ or not isinstance(last_error, str)
+ or bool(last_error)
+ )
+
+
+class EventRecorder:
+ def __init__(self, path: Path) -> None:
+ self.path = path
+ self.lock = threading.Lock()
+ path.parent.mkdir(parents=True, exist_ok=True)
+
+ def record(self, event: str, **details: Any) -> None:
+ item = {"timestamp": utc_now(), "event": event, "details": details}
+ with self.lock, self.path.open("a", encoding="utf-8") as handle:
+ handle.write(json.dumps(item, default=json_default, sort_keys=True) + "\n")
+
+
+class ReplicationHarness:
+ def __init__(self, args: argparse.Namespace) -> None:
+ self.args = args
+ self.evidence_dir = args.evidence_dir.resolve()
+ self.case_dir = self.evidence_dir / "cases"
+ self.case_dir.mkdir(parents=True, exist_ok=True)
+ self.events = EventRecorder(self.evidence_dir / "events.jsonl")
+ self.results: list[CaseResult] = []
+ self.rc_home = Path(tempfile.mkdtemp(prefix="rustfs-replication-rc-"))
+ self.rc_env = os.environ.copy()
+ self.rc_env["RC_CONFIG_DIR"] = str(self.rc_home)
+ self.rc_bin = args.rc_bin
+ self.rc_version = "unavailable"
+ self.rc_binary_sha256 = "unavailable"
+ self.expected_rc_binary_sha256 = os.environ.get(
+ "RUSTFS_EXPECTED_RC_SHA256", ""
+ ).lower()
+ self.site_a = Site("site-a", args.endpoint_a.rstrip("/"), args.access_key, args.secret_key)
+ self.site_b = Site("site-b", args.endpoint_b.rstrip("/"), args.access_key, args.secret_key)
+ self.alias_a = f"rv-a-{safe_name(args.run_id)[-12:]}"
+ self.alias_b = f"rv-b-{safe_name(args.run_id)[-12:]}"
+ self.client_a = self.s3_client(self.site_a)
+ self.client_b = self.s3_client(self.site_b)
+ self.created_buckets: list[tuple[Any, str]] = []
+ self.secrets: set[str] = {args.access_key, args.secret_key}
+ self.isolation_failure: dict[str, Any] | None = None
+ self.requested_fixtures = frozenset(getattr(args, "fixture", None) or ())
+ self.site_group_tracker_state = "fresh"
+ self.site_group_mutation_attempted = False
+ self.fixture_outcomes: list[dict[str, Any]] = []
+
+ def close(self) -> None:
+ shutil.rmtree(self.rc_home, ignore_errors=True)
+
+ def s3_client(self, site: Site, access_key: str | None = None, secret_key: str | None = None):
+ return boto3.client(
+ "s3",
+ endpoint_url=site.endpoint,
+ aws_access_key_id=access_key or site.access_key,
+ aws_secret_access_key=secret_key or site.secret_key,
+ region_name=self.args.region,
+ config=Config(
+ signature_version="s3v4",
+ s3={"addressing_style": "path"},
+ retries={"max_attempts": 3, "mode": "standard"},
+ connect_timeout=5,
+ read_timeout=30,
+ ),
+ )
+
+ def evidence_path(self, case_id: str, name: str) -> Path:
+ directory = self.case_dir / safe_name(case_id)
+ directory.mkdir(parents=True, exist_ok=True)
+ return directory / name
+
+ def save_json(self, case_id: str, name: str, value: Any) -> str:
+ path = self.evidence_path(case_id, name)
+ text = json.dumps(value, default=json_default, indent=2, sort_keys=True) + "\n"
+ path.write_text(self.redact(text), encoding="utf-8")
+ return str(path.relative_to(self.evidence_dir))
+
+ def save_text(self, case_id: str, name: str, value: str) -> str:
+ path = self.evidence_path(case_id, name)
+ path.write_text(self.redact(value), encoding="utf-8")
+ return str(path.relative_to(self.evidence_dir))
+
+ def redact(self, value: str) -> str:
+ result = value
+ for secret in sorted((item for item in self.secrets if item), key=len, reverse=True):
+ result = result.replace(secret, "[REDACTED]")
+ return result
+
+ def bucket_name(self, case_id: str, role: str = "src") -> str:
+ digest = hashlib.sha256(f"{self.args.run_id}:{case_id}:{role}".encode()).hexdigest()[:12]
+ return f"rv-{role}-{digest}"
+
+ def identity_name(self, case_id: str, role: str) -> str:
+ digest = hashlib.sha256(f"{self.args.run_id}:{case_id}:{role}".encode()).hexdigest()[:10]
+ return f"rv-{role}-{digest}"
+
+ def create_bucket(self, client: Any, bucket: str) -> None:
+ client.create_bucket(Bucket=bucket)
+ self.created_buckets.append((client, bucket))
+ self.events.record("bucket.created", bucket=bucket)
+
+ @staticmethod
+ def enable_versioning(client: Any, bucket: str) -> None:
+ client.put_bucket_versioning(
+ Bucket=bucket, VersioningConfiguration={"Status": "Enabled"}
+ )
+ status = client.get_bucket_versioning(Bucket=bucket).get("Status")
+ if status != "Enabled":
+ raise AssertionError(f"versioning did not become enabled: {status!r}")
+
+ def wait_until(
+ self,
+ case_id: str,
+ description: str,
+ probe: Callable[[], tuple[bool, Any]],
+ timeout: float | None = None,
+ ) -> Any:
+ deadline = time.monotonic() + (timeout or self.args.timeout)
+ attempts = 0
+ last: Any = None
+ while time.monotonic() < deadline:
+ attempts += 1
+ try:
+ complete, last = probe()
+ except INFRA_EXCEPTIONS as exc:
+ complete, last = False, {"error": f"{type(exc).__name__}: {exc}"}
+ if complete:
+ self.events.record(
+ "poll.complete",
+ case=case_id,
+ description=description,
+ attempts=attempts,
+ detail=last,
+ )
+ return last
+ time.sleep(self.args.poll_interval)
+ self.save_json(
+ case_id,
+ f"poll-timeout-{safe_name(description)}.json",
+ {"description": description, "attempts": attempts, "last": last},
+ )
+ raise AssertionError(f"timed out waiting for {description}; last={last}")
+
+ def wait_api(self, case_id: str, client: Any, label: str) -> None:
+ def probe() -> tuple[bool, Any]:
+ try:
+ response = client.list_buckets()
+ return True, {"requestId": response.get("ResponseMetadata", {}).get("RequestId")}
+ except (*INFRA_EXCEPTIONS, ClientError) as exc:
+ return False, {"error": f"{type(exc).__name__}: {exc}"[:400]}
+
+ self.wait_until(case_id, f"{label} S3 API readiness", probe, timeout=120)
+
+ def configure_rc(self) -> None:
+ requested_rc_bin = self.rc_bin
+ try:
+ self.rc_bin, self.rc_binary_sha256 = resolve_rc_binary(requested_rc_bin)
+ except FileNotFoundError as exc:
+ raise RuntimeError(
+ f"required rc binary not found: {requested_rc_bin}"
+ ) from exc
+ version = subprocess.run(
+ [self.rc_bin, "--version"],
+ env=self.rc_env,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True,
+ check=False,
+ )
+ if version.returncode != 0:
+ raise RuntimeError(f"rc --version failed: {version.stderr.strip()}")
+ self.rc_version = (version.stdout or version.stderr).strip()
+ if (
+ self.expected_rc_binary_sha256
+ and self.rc_binary_sha256.lower() != self.expected_rc_binary_sha256
+ ):
+ raise RuntimeError(
+ "resolved rc binary SHA-256 differs from RUSTFS_EXPECTED_RC_SHA256"
+ )
+ for alias, site in ((self.alias_a, self.site_a), (self.alias_b, self.site_b)):
+ result = subprocess.run(
+ [
+ self.rc_bin,
+ "--no-color",
+ "alias",
+ "set",
+ alias,
+ site.endpoint,
+ site.access_key,
+ site.secret_key,
+ "--bucket-lookup",
+ "path",
+ ],
+ env=self.rc_env,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True,
+ check=False,
+ )
+ if result.returncode != 0:
+ raise RuntimeError(f"rc alias set failed for {site.id}: {result.stderr.strip()}")
+ self.events.record(
+ "rc.configured",
+ binaryPath=self.rc_bin,
+ version=self.rc_version,
+ binarySha256=self.rc_binary_sha256,
+ )
+
+ def run_rc(
+ self,
+ case_id: str,
+ name: str,
+ arguments: list[str],
+ *,
+ expect_success: bool = True,
+ ) -> subprocess.CompletedProcess[str]:
+ command = [self.rc_bin, "--json", "--no-color", *arguments]
+ result = subprocess.run(
+ command,
+ env=self.rc_env,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True,
+ check=False,
+ )
+ sanitized = ["[REDACTED]" if token in self.secrets else token for token in command]
+ self.save_json(
+ case_id,
+ f"{name}.json",
+ {
+ "command": sanitized,
+ "exitCode": result.returncode,
+ "stdout": result.stdout,
+ "stderr": result.stderr,
+ },
+ )
+ if expect_success and result.returncode != 0:
+ raise AssertionError(
+ f"rc {name} exited {result.returncode}: {self.redact(result.stderr.strip())}"
+ )
+ if not expect_success and result.returncode == 0:
+ raise AssertionError(f"rc {name} unexpectedly succeeded")
+ return result
+
+ def signed_request(
+ self,
+ site: Site,
+ method: str,
+ path: str,
+ *,
+ query: str = "",
+ body: bytes | None = None,
+ content_type: str | None = None,
+ timeout: float = 30,
+ ) -> tuple[int, dict[str, str], bytes]:
+ url = f"{site.endpoint}{urllib.parse.quote(path, safe='/-._~')}"
+ if query:
+ url += f"?{query}"
+ payload = body or b""
+ headers = {"x-amz-content-sha256": hashlib.sha256(payload).hexdigest()}
+ if content_type:
+ headers["content-type"] = content_type
+ request = AWSRequest(method=method, url=url, data=body, headers=headers)
+ SigV4Auth(Credentials(site.access_key, site.secret_key), "s3", self.args.region).add_auth(request)
+ outgoing = urllib.request.Request(
+ url,
+ data=body,
+ headers=dict(request.headers.items()),
+ method=method,
+ )
+ opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
+ try:
+ with opener.open(outgoing, timeout=timeout) as response:
+ return response.status, dict(response.headers.items()), response.read()
+ except urllib.error.HTTPError as exc:
+ return exc.code, dict(exc.headers.items()), exc.read()
+
+ def admin_request(
+ self,
+ site: Site,
+ method: str,
+ subpath: str,
+ *,
+ query: str = "",
+ payload: Any | None = None,
+ ) -> tuple[int, dict[str, str], bytes]:
+ body = None if payload is None else json.dumps(payload).encode("utf-8")
+ return self.signed_request(
+ site,
+ method,
+ f"{ADMIN_PREFIX}/{subpath}",
+ query=query,
+ body=body,
+ content_type="application/json" if body is not None else None,
+ )
+
+ @staticmethod
+ def json_body(body: bytes) -> Any:
+ if not body.strip():
+ return {}
+ return json.loads(body)
+
+ @staticmethod
+ def required_json_object(body: bytes, operation: str) -> dict[str, Any]:
+ try:
+ parsed = ReplicationHarness.json_body(body)
+ except (UnicodeDecodeError, json.JSONDecodeError) as exc:
+ raise AssertionError(f"{operation} returned malformed JSON") from exc
+ if not isinstance(parsed, dict):
+ raise AssertionError(
+ f"{operation} returned a non-object JSON value: {parsed!r}"
+ )
+ return parsed
+
+ def hook(self, case_id: str, name: str, raw: str | None) -> str:
+ if not raw:
+ raise RuntimeError(f"{name} requires its driver hook")
+ command = json.loads(raw)
+ if not isinstance(command, list) or not command or not all(isinstance(x, str) and x for x in command):
+ raise RuntimeError(f"{name} hook must be a JSON command array")
+ result = subprocess.run(
+ command,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ check=False,
+ )
+ path = self.save_text(case_id, f"{name}.log", result.stdout)
+ if result.returncode != 0:
+ raise RuntimeError(f"{name} hook exited {result.returncode}")
+ return path
+
+ def wait_object(
+ self,
+ case_id: str,
+ client: Any,
+ bucket: str,
+ key: str,
+ expected: bytes,
+ *,
+ expected_version: str | None = None,
+ timeout: float | None = None,
+ ) -> dict[str, Any]:
+ expected_sha = hashlib.sha256(expected).hexdigest()
+
+ def probe() -> tuple[bool, Any]:
+ try:
+ response = client.get_object(Bucket=bucket, Key=key)
+ body = response["Body"].read()
+ version = response.get("VersionId")
+ detail = {
+ "bytes": len(body),
+ "sha256": hashlib.sha256(body).hexdigest(),
+ "versionId": version,
+ "metadata": response.get("Metadata", {}),
+ }
+ matches = detail["sha256"] == expected_sha
+ if expected_version is not None:
+ matches = matches and version == expected_version
+ return matches, detail
+ except ClientError as exc:
+ return False, {"error": exc.response.get("Error", {}).get("Code", "unknown")}
+
+ return self.wait_until(case_id, f"{bucket}/{key} replication", probe, timeout=timeout)
+
+ @staticmethod
+ def object_exists(client: Any, bucket: str, key: str) -> tuple[bool, Any]:
+ try:
+ response = client.head_object(Bucket=bucket, Key=key)
+ return True, {"versionId": response.get("VersionId"), "contentLength": response.get("ContentLength")}
+ except ClientError as exc:
+ code = exc.response.get("Error", {}).get("Code", "unknown")
+ if code in {"404", "NoSuchKey", "NoSuchBucket", "NotFound"}:
+ return False, {"error": code}
+ raise
+
+ def assert_absent_for(
+ self, case_id: str, client: Any, bucket: str, key: str, duration: float
+ ) -> dict[str, Any]:
+ deadline = time.monotonic() + duration
+ observations = 0
+ last: Any = None
+ while time.monotonic() < deadline:
+ observations += 1
+ exists, last = self.object_exists(client, bucket, key)
+ if exists:
+ raise AssertionError(f"unexpected object appeared at {bucket}/{key}: {last}")
+ time.sleep(self.args.poll_interval)
+ detail = {"durationSeconds": duration, "observations": observations, "last": last}
+ self.events.record("negative-window.complete", case=case_id, **detail)
+ return detail
+
+ def setup_bucket_replication(
+ self,
+ case_id: str,
+ *,
+ flags: str | None = None,
+ prefix: str | None = None,
+ synchronous: bool = False,
+ preexisting: tuple[str, bytes] | None = None,
+ ) -> tuple[str, str, dict[str, Any]]:
+ source = self.bucket_name(case_id, "src")
+ target = self.bucket_name(case_id, "dst")
+ self.create_bucket(self.client_a, source)
+ self.create_bucket(self.client_b, target)
+ self.enable_versioning(self.client_a, source)
+ self.enable_versioning(self.client_b, target)
+ if preexisting:
+ key, body = preexisting
+ self.client_a.put_object(Bucket=source, Key=key, Body=body)
+ arguments = [
+ "bucket",
+ "replication",
+ "add",
+ f"{self.alias_a}/{source}",
+ "--remote-bucket",
+ f"{self.alias_b}/{target}",
+ "--id",
+ f"rule-{hashlib.sha256(case_id.encode()).hexdigest()[:8]}",
+ "--healthcheck-seconds",
+ "1",
+ "--force",
+ ]
+ if flags:
+ arguments.extend(("--replicate", flags))
+ if prefix:
+ arguments.extend(("--prefix", prefix))
+ if synchronous:
+ arguments.append("--sync")
+ self.run_rc(case_id, "replication-add", arguments)
+ config = self.client_a.get_bucket_replication(Bucket=source).get("ReplicationConfiguration", {})
+ self.save_json(case_id, "replication-configuration.json", config)
+ if not config.get("Rules"):
+ raise AssertionError("replication configuration has no rules")
+ return source, target, config
+
+ @staticmethod
+ def delete_replication_configuration(client: Any, bucket: str) -> None:
+ try:
+ client.delete_bucket_replication(Bucket=bucket)
+ except ClientError as exc:
+ if exc.response.get("Error", {}).get("Code") not in {
+ "NoSuchBucket",
+ "ReplicationConfigurationNotFoundError",
+ "NoSuchReplicationConfiguration",
+ }:
+ raise
+
+ @staticmethod
+ def delete_object_batch(client: Any, bucket: str, items: list[dict[str, str]]) -> None:
+ for offset in range(0, len(items), 1000):
+ response = client.delete_objects(
+ Bucket=bucket,
+ Delete={"Objects": items[offset : offset + 1000], "Quiet": True},
+ )
+ errors = response.get("Errors", [])
+ if errors:
+ raise RuntimeError(f"delete_objects reported errors: {errors[:5]}")
+
+ def inspect_bucket_cleanup_inventory(
+ self,
+ client: Any,
+ bucket: str,
+ ) -> dict[str, Any]:
+ inventory: dict[str, Any] = {"bucket": bucket}
+ try:
+ uploads = client.list_multipart_uploads(Bucket=bucket)
+ inventory["multipartUploads"] = [
+ {"key": item.get("Key"), "uploadId": item.get("UploadId")}
+ for item in uploads.get("Uploads", [])[:20]
+ ]
+ inventory["multipartUploadsTruncated"] = bool(
+ uploads.get("IsTruncated")
+ )
+ except Exception as exc:
+ inventory["multipartInspectionError"] = f"{type(exc).__name__}: {exc}"
+ try:
+ version_count = 0
+ marker_count = 0
+ version_samples: list[dict[str, Any]] = []
+ for page in client.get_paginator("list_object_versions").paginate(
+ Bucket=bucket
+ ):
+ versions = page.get("Versions", [])
+ markers = page.get("DeleteMarkers", [])
+ version_count += len(versions)
+ marker_count += len(markers)
+ for kind, items in (("version", versions), ("delete-marker", markers)):
+ for item in items:
+ if len(version_samples) < 20:
+ version_samples.append(
+ {
+ "kind": kind,
+ "key": item.get("Key"),
+ "versionId": item.get("VersionId"),
+ }
+ )
+ inventory["versions"] = version_count
+ inventory["deleteMarkers"] = marker_count
+ inventory["versionSamples"] = version_samples
+ except Exception as exc:
+ inventory["versionInspectionError"] = f"{type(exc).__name__}: {exc}"
+ try:
+ object_count = 0
+ object_samples: list[str] = []
+ for page in client.get_paginator("list_objects_v2").paginate(
+ Bucket=bucket
+ ):
+ objects = page.get("Contents", [])
+ object_count += len(objects)
+ for item in objects:
+ if len(object_samples) < 20:
+ object_samples.append(str(item.get("Key", "")))
+ inventory["objects"] = object_count
+ inventory["objectSamples"] = object_samples
+ except Exception as exc:
+ inventory["objectInspectionError"] = f"{type(exc).__name__}: {exc}"
+ return inventory
+
+ def cleanup_bucket_contents(self, client: Any, bucket: str) -> bool:
+ """Empty one test bucket without deleting its namespace.
+
+ Site-replicated peers with the same bucket name must all cross this
+ barrier before either namespace is removed. Otherwise the first
+ DeleteBucket hook can observe live versions on its peer and persist a
+ destructive retry liability that contaminates later cases.
+ """
+
+ deadline = time.monotonic() + 30
+ last_error: BaseException | None = None
+ changed = False
+ while time.monotonic() < deadline:
+ try:
+ uploads = client.list_multipart_uploads(Bucket=bucket).get(
+ "Uploads",
+ [],
+ )
+ for upload in uploads:
+ changed = True
+ client.abort_multipart_upload(
+ Bucket=bucket,
+ Key=upload["Key"],
+ UploadId=upload["UploadId"],
+ )
+
+ version_items: list[dict[str, str]] = []
+ for page in client.get_paginator("list_object_versions").paginate(
+ Bucket=bucket
+ ):
+ version_items.extend(
+ {"Key": item["Key"], "VersionId": item["VersionId"]}
+ for group in ("Versions", "DeleteMarkers")
+ for item in page.get(group, [])
+ )
+ if version_items:
+ changed = True
+ self.delete_object_batch(client, bucket, version_items)
+
+ object_items: list[dict[str, str]] = []
+ for page in client.get_paginator("list_objects_v2").paginate(
+ Bucket=bucket
+ ):
+ object_items.extend(
+ {"Key": item["Key"]} for item in page.get("Contents", [])
+ )
+ if object_items:
+ changed = True
+ self.delete_object_batch(client, bucket, object_items)
+
+ if not uploads and not version_items and not object_items:
+ return changed
+ last_error = RuntimeError(
+ "bucket contents did not converge to empty"
+ )
+ except ClientError as exc:
+ code = exc.response.get("Error", {}).get("Code", "")
+ if code == "NoSuchBucket":
+ return changed
+ if code != "BucketNotEmpty":
+ raise
+ last_error = exc
+ remaining = deadline - time.monotonic()
+ if remaining > 0:
+ time.sleep(min(self.args.poll_interval, remaining))
+ raise BucketCleanupResidual(
+ bucket,
+ last_error,
+ self.inspect_bucket_cleanup_inventory(client, bucket),
+ )
+
+ def delete_empty_bucket(self, client: Any, bucket: str) -> None:
+ deadline = time.monotonic() + 30
+ last_error: BaseException | None = None
+ while time.monotonic() < deadline:
+ try:
+ client.delete_bucket(Bucket=bucket)
+ return
+ except ClientError as exc:
+ code = exc.response.get("Error", {}).get("Code", "")
+ if code == "NoSuchBucket":
+ return
+ if code != "BucketNotEmpty":
+ raise
+ last_error = exc
+ remaining = deadline - time.monotonic()
+ if remaining > 0:
+ time.sleep(min(self.args.poll_interval, remaining))
+ raise BucketCleanupResidual(
+ bucket,
+ last_error,
+ self.inspect_bucket_cleanup_inventory(client, bucket),
+ )
+
+ def cleanup_bucket(self, client: Any, bucket: str) -> None:
+ """Compatibility wrapper for callers cleaning one independent bucket."""
+
+ self.delete_replication_configuration(client, bucket)
+ self.cleanup_bucket_contents(client, bucket)
+ self.delete_empty_bucket(client, bucket)
+
+ def site_retry_stats_snapshot(self) -> dict[str, Any]:
+ return {
+ site.id: self.site_info(site).get("retryStats")
+ for site in (self.site_a, self.site_b)
+ }
+
+ @staticmethod
+ def normalized_retry_stats(value: Any) -> tuple[int, int, str] | None:
+ if value is None or value == {}:
+ return 0, 0, ""
+ if not isinstance(value, dict):
+ return None
+ pending = value.get("pending", 0)
+ failed = value.get("failed", 0)
+ last_error = value.get("lastError", "")
+ if (
+ not isinstance(pending, int)
+ or isinstance(pending, bool)
+ or pending < 0
+ or not isinstance(failed, int)
+ or isinstance(failed, bool)
+ or failed < 0
+ or not isinstance(last_error, str)
+ ):
+ return None
+ return pending, failed, last_error
+
+ @classmethod
+ def site_retry_state_regressions(
+ cls,
+ before: dict[str, Any],
+ after: dict[str, Any],
+ ) -> list[dict[str, Any]]:
+ regressions: list[dict[str, Any]] = []
+ for site_id in ("site-a", "site-b"):
+ before_raw = before.get(site_id)
+ after_raw = after.get(site_id)
+ before_stats = cls.normalized_retry_stats(before_raw)
+ after_stats = cls.normalized_retry_stats(after_raw)
+ if after_stats is None:
+ regressions.append(
+ {
+ "site": site_id,
+ "reason": "post-cleanup retryStats is malformed",
+ "before": before_raw,
+ "after": after_raw,
+ }
+ )
+ continue
+ if before_raw == after_raw:
+ continue
+ after_pending, after_failed, after_error = after_stats
+ if after_pending == 0 and after_failed == 0 and not after_error:
+ continue
+ if before_stats is None:
+ regressions.append(
+ {
+ "site": site_id,
+ "reason": "retry state changed from an unverifiable baseline",
+ "before": before_raw,
+ "after": after_raw,
+ }
+ )
+ continue
+ before_pending, before_failed, before_error = before_stats
+ improved = (
+ after_pending <= before_pending
+ and after_failed <= before_failed
+ and (after_pending < before_pending or after_failed < before_failed)
+ )
+ unchanged = (
+ after_pending == before_pending
+ and after_failed == before_failed
+ and after_error == before_error
+ )
+ if not improved and not unchanged:
+ regressions.append(
+ {
+ "site": site_id,
+ "reason": "cleanup introduced or worsened retry debt",
+ "before": before_raw,
+ "after": after_raw,
+ }
+ )
+ return regressions
+
+ def cleanup_target(self, client: Any, bucket: str) -> dict[str, str]:
+ if client is self.client_a:
+ site = "site-a"
+ elif client is self.client_b:
+ site = "site-b"
+ else:
+ site = "unknown"
+ return {"site": site, "bucket": bucket}
+
+ def tracked_bucket_summaries(self) -> list[dict[str, str]]:
+ summaries: list[dict[str, str]] = []
+ seen: set[tuple[int, str]] = set()
+ for client, bucket in self.created_buckets:
+ identity = (id(client), bucket)
+ if identity in seen:
+ continue
+ seen.add(identity)
+ summaries.append(self.cleanup_target(client, bucket))
+ return summaries
+
+ def cleanup_created_buckets(self, case_id: str) -> None:
+ errors: list[dict[str, Any]] = []
+ residuals: list[dict[str, Any]] = []
+ unique: list[tuple[Any, str]] = []
+ seen: set[tuple[int, str]] = set()
+ for client, bucket in self.created_buckets:
+ identity = (id(client), bucket)
+ if identity not in seen:
+ seen.add(identity)
+ unique.append((client, bucket))
+ if self.args.module == "site":
+ # A site bucket may reach its peer immediately before a failed
+ # wait returns, so the peer is not necessarily in created_buckets.
+ # Both deterministic test namespaces remain in cleanup scope.
+ bucket_names = list(dict.fromkeys(bucket for _, bucket in unique))
+ unique = [
+ (client, bucket)
+ for bucket in bucket_names
+ for client in (self.client_a, self.client_b)
+ ]
+ remaining = {(id(client), bucket) for client, bucket in unique}
+ retry_check_required = self.args.module == "site" and bool(unique)
+ retry_before: dict[str, Any] | None = None
+ retry_after: dict[str, Any] | None = None
+ retry_regressions: list[dict[str, Any]] = []
+ retry_isolation_unverified = False
+ if retry_check_required:
+ try:
+ retry_before = self.site_retry_stats_snapshot()
+ except Exception as exc:
+ retry_isolation_unverified = True
+ errors.append(
+ {
+ "site": "site-group",
+ "phase": "retry-state-before-cleanup",
+ "errorType": type(exc).__name__,
+ "message": str(exc),
+ }
+ )
+
+ # Remove operator-authored replication configuration while every
+ # tracked peer bucket still exists. RustFS deliberately retains the
+ # derived site-replication rules, so this step alone is not an
+ # all-clear for namespace deletion.
+ for client, bucket in unique:
+ try:
+ self.delete_replication_configuration(client, bucket)
+ except Exception as exc:
+ errors.append(
+ {
+ **self.cleanup_target(client, bucket),
+ "phase": "disable-replication",
+ "errorType": type(exc).__name__,
+ "message": str(exc),
+ }
+ )
+
+ bucket_sites: dict[str, set[int]] = {}
+ for client, bucket in unique:
+ bucket_sites.setdefault(bucket, set()).add(id(client))
+ shared_site_bucket = any(len(clients) > 1 for clients in bucket_sites.values())
+ contents_clean = True
+ stable_empty_rounds = 0
+ content_barrier_deadline = time.monotonic() + 30
+ while contents_clean:
+ round_changed = False
+ for client, bucket in unique:
+ try:
+ round_changed = (
+ self.cleanup_bucket_contents(client, bucket)
+ or round_changed
+ )
+ except BucketCleanupResidual as exc:
+ contents_clean = False
+ residuals.append(
+ {
+ "phase": "empty-contents",
+ **exc.inventory,
+ **self.cleanup_target(client, bucket),
+ }
+ )
+ except Exception as exc:
+ contents_clean = False
+ errors.append(
+ {
+ **self.cleanup_target(client, bucket),
+ "phase": "empty-contents",
+ "errorType": type(exc).__name__,
+ "message": str(exc),
+ }
+ )
+ if not contents_clean or not shared_site_bucket:
+ break
+ stable_empty_rounds = 0 if round_changed else stable_empty_rounds + 1
+ if stable_empty_rounds >= 2:
+ break
+ remaining_time = content_barrier_deadline - time.monotonic()
+ if remaining_time <= 0:
+ contents_clean = False
+ errors.append(
+ {
+ "site": "site-group",
+ "phase": "empty-contents-stability",
+ "errorType": "BucketCleanupStabilityTimeout",
+ "message": (
+ "shared site bucket contents did not remain empty "
+ "across two consecutive cleanup rounds"
+ ),
+ "inventories": [
+ {
+ **self.inspect_bucket_cleanup_inventory(
+ client,
+ bucket,
+ ),
+ **self.cleanup_target(client, bucket),
+ }
+ for client, bucket in unique
+ ],
+ }
+ )
+ break
+ time.sleep(min(self.args.poll_interval, remaining_time))
+
+ # An all-content barrier prevents a peer DeleteBucket hook from seeing
+ # live versions on the other side. If any side is uncertain, retain
+ # every namespace and let the existing isolation failure path preserve
+ # evidence instead of creating destructive retry debt.
+ if contents_clean:
+ for client, bucket in reversed(unique):
+ identity = (id(client), bucket)
+ try:
+ self.delete_empty_bucket(client, bucket)
+ except BucketCleanupResidual as exc:
+ residuals.append(
+ {
+ "phase": "delete-bucket",
+ **exc.inventory,
+ **self.cleanup_target(client, bucket),
+ }
+ )
+ except Exception as exc:
+ errors.append(
+ {
+ **self.cleanup_target(client, bucket),
+ "phase": "delete-bucket",
+ "errorType": type(exc).__name__,
+ "message": str(exc),
+ }
+ )
+ else:
+ remaining.discard(identity)
+
+ if retry_check_required:
+ try:
+ retry_after = self.site_retry_stats_snapshot()
+ except Exception as exc:
+ retry_isolation_unverified = True
+ errors.append(
+ {
+ "site": "site-group",
+ "phase": "retry-state-after-cleanup",
+ "errorType": type(exc).__name__,
+ "message": str(exc),
+ }
+ )
+ if retry_before is not None and retry_after is not None:
+ retry_regressions = self.site_retry_state_regressions(
+ retry_before,
+ retry_after,
+ )
+ if retry_regressions:
+ errors.append(
+ {
+ "site": "site-group",
+ "phase": "retry-state-isolation",
+ "errorType": "CleanupRetryStateRegression",
+ "message": (
+ "logical cleanup introduced or worsened site "
+ "replication retry debt"
+ ),
+ }
+ )
+
+ self.created_buckets = [
+ (client, bucket)
+ for client, bucket in unique
+ if (id(client), bucket) in remaining
+ ]
+ if residuals or errors:
+ tracked_buckets = self.tracked_bucket_summaries()
+ retry_isolation_failed = bool(retry_regressions) or (
+ retry_check_required and retry_isolation_unverified
+ )
+ isolation_restored = not tracked_buckets and not retry_isolation_failed
+ if isolation_restored:
+ self.isolation_failure = None
+ evidence = self.save_json(
+ case_id,
+ "cleanup-failure.json",
+ {
+ "classification": "logical-cleanup-failure",
+ "authoritativeCleanup": "replication phase data reset",
+ "isolationRestored": isolation_restored,
+ "trackedBuckets": tracked_buckets,
+ "residuals": residuals,
+ "errors": errors,
+ "retryState": (
+ {
+ "before": retry_before,
+ "after": retry_after,
+ "regressions": retry_regressions,
+ "verified": not retry_isolation_unverified,
+ }
+ if retry_check_required
+ else None
+ ),
+ },
+ )
+ self.events.record(
+ "case.cleanup.failure",
+ case=case_id,
+ evidence=evidence,
+ isolationRestored=isolation_restored,
+ residualBuckets=[
+ {"site": item["site"], "bucket": item["bucket"]}
+ for item in residuals
+ ],
+ errorCount=len(errors),
+ trackedBuckets=tracked_buckets,
+ )
+ raise LogicalCleanupFailure(
+ residuals=residuals,
+ errors=errors,
+ evidence=evidence,
+ tracked_buckets=tracked_buckets,
+ isolation_restored=isolation_restored,
+ )
+ self.isolation_failure = None
+
+ # ------------------------------------------------------------------
+ # Bucket replication cases
+ # ------------------------------------------------------------------
+
+ def case_bucket_source_versioning_required(self, case_id: str) -> list[str]:
+ source = self.bucket_name(case_id, "src")
+ target = self.bucket_name(case_id, "dst")
+ self.create_bucket(self.client_a, source)
+ self.create_bucket(self.client_b, target)
+ self.enable_versioning(self.client_b, target)
+ result = self.run_rc(
+ case_id,
+ "source-versioning-required",
+ [
+ "bucket",
+ "replication",
+ "add",
+ f"{self.alias_a}/{source}",
+ "--remote-bucket",
+ f"{self.alias_b}/{target}",
+ "--force",
+ ],
+ expect_success=False,
+ )
+ error = (result.stdout + result.stderr).lower()
+ if "version" not in error and "replication" not in error:
+ raise AssertionError(f"unexpected source-versioning error: {error[:500]}")
+ try:
+ self.client_a.get_bucket_replication(Bucket=source)
+ except ClientError as exc:
+ code = exc.response.get("Error", {}).get("Code", "")
+ if code not in {
+ "ReplicationConfigurationNotFoundError",
+ "NoSuchReplicationConfiguration",
+ "NoSuchConfiguration",
+ }:
+ raise
+ else:
+ raise AssertionError("rejected source acquired a replication configuration")
+ return ["cases/%s/source-versioning-required.json" % safe_name(case_id)]
+
+ def case_bucket_target_versioning_required(self, case_id: str) -> list[str]:
+ source = self.bucket_name(case_id, "src")
+ target = self.bucket_name(case_id, "dst")
+ self.create_bucket(self.client_a, source)
+ self.create_bucket(self.client_b, target)
+ self.enable_versioning(self.client_a, source)
+ result = self.run_rc(
+ case_id,
+ "target-versioning-required",
+ [
+ "bucket",
+ "replication",
+ "add",
+ f"{self.alias_a}/{source}",
+ "--remote-bucket",
+ f"{self.alias_b}/{target}",
+ "--force",
+ ],
+ expect_success=False,
+ )
+ error = (result.stdout + result.stderr).lower()
+ if "version" not in error and "remote" not in error:
+ raise AssertionError(f"unexpected target-versioning error: {error[:500]}")
+ return ["cases/%s/target-versioning-required.json" % safe_name(case_id)]
+
+ def case_bucket_configuration_roundtrip(self, case_id: str) -> list[str]:
+ source, target, config = self.setup_bucket_replication(
+ case_id, flags="delete,delete-marker,existing-objects", prefix="roundtrip/"
+ )
+ list_result = self.run_rc(
+ case_id,
+ "replication-list",
+ ["bucket", "replication", "list", f"{self.alias_a}/{source}", "--force"],
+ )
+ combined = list_result.stdout
+ rule = config["Rules"][0]
+ rule_id = rule.get("ID", "")
+ if source not in combined and rule_id not in combined:
+ raise AssertionError("rc list did not identify the configured rule")
+ destination = rule.get("Destination", {}).get("Bucket", "")
+ if not destination or target not in destination:
+ raise AssertionError(f"destination does not identify target bucket: {destination!r}")
+ if rule.get("Priority") != 1 or rule.get("Status") != "Enabled":
+ raise AssertionError(f"replication rule priority/status drifted: {rule}")
+ if rule.get("Filter", {}).get("Prefix") != "roundtrip/":
+ raise AssertionError(f"replication prefix did not round-trip: {rule}")
+ if rule.get("DeleteMarkerReplication", {}).get("Status") != "Enabled":
+ raise AssertionError("delete-marker flag did not round-trip")
+ if rule.get("ExistingObjectReplication", {}).get("Status") != "Enabled":
+ raise AssertionError("existing-object flag did not round-trip")
+ listed = self.json_body(list_result.stdout.encode("utf-8"))
+ listed_rules = listed.get("rules", []) if isinstance(listed, dict) else []
+ listed_rule = next(
+ (item for item in listed_rules if item.get("id") == rule_id), None
+ )
+ if not listed_rule:
+ raise AssertionError(f"rc list omitted configured rule {rule_id!r}")
+ expected_flags = {
+ "deleteReplication": True,
+ "deleteMarkerReplication": True,
+ "existingObjectReplication": True,
+ }
+ if any(listed_rule.get(name) is not value for name, value in expected_flags.items()):
+ raise AssertionError(f"rc list flag round-trip mismatch: {listed_rule}")
+ self.client_a.delete_bucket_replication(Bucket=source)
+ try:
+ self.client_a.get_bucket_replication(Bucket=source)
+ except ClientError:
+ pass
+ else:
+ raise AssertionError("replication configuration remained after delete")
+ return [
+ self.save_json(case_id, "roundtrip-final.json", {"ruleId": rule_id, "destination": destination})
+ ]
+
+ def case_bucket_put_object(self, case_id: str) -> list[str]:
+ source, target, _ = self.setup_bucket_replication(case_id)
+ key = "objects/basic.bin"
+ body = secrets.token_bytes(64 * 1024)
+ response = self.client_a.put_object(
+ Bucket=source, Key=key, Body=body, Metadata={"validation": "bucket-put"}
+ )
+ version = response.get("VersionId")
+ final = self.wait_object(
+ case_id, self.client_b, target, key, body, expected_version=version
+ )
+ if final.get("metadata", {}).get("validation") != "bucket-put":
+ raise AssertionError("object metadata did not replicate")
+ return [self.save_json(case_id, "object-final.json", final)]
+
+ def case_bucket_sync_put_object(self, case_id: str) -> list[str]:
+ source, target, _ = self.setup_bucket_replication(case_id, synchronous=True)
+ key = "sync/immediate.txt"
+ body = b"synchronous-replication-check"
+ started = time.monotonic()
+ response = self.client_a.put_object(Bucket=source, Key=key, Body=body)
+ elapsed = time.monotonic() - started
+ try:
+ target_response = self.client_b.get_object(Bucket=target, Key=key)
+ except ClientError as exc:
+ raise AssertionError("synchronous PUT returned before target was readable") from exc
+ target_body = target_response["Body"].read()
+ if target_body != body or target_response.get("VersionId") != response.get("VersionId"):
+ raise AssertionError("synchronous target object differs from source response")
+ return [
+ self.save_json(
+ case_id,
+ "sync-put.json",
+ {
+ "elapsedSeconds": elapsed,
+ "sourceVersionId": response.get("VersionId"),
+ "targetVersionId": target_response.get("VersionId"),
+ "sha256": hashlib.sha256(target_body).hexdigest(),
+ },
+ )
+ ]
+
+ def case_bucket_prefix_filter(self, case_id: str) -> list[str]:
+ source, target, _ = self.setup_bucket_replication(case_id, prefix="included/")
+ included_key = "included/object.txt"
+ excluded_key = "excluded/control.txt"
+ included = b"prefix-match"
+ self.client_a.put_object(Bucket=source, Key=included_key, Body=included)
+ self.client_a.put_object(Bucket=source, Key=excluded_key, Body=b"prefix-control")
+ final = self.wait_object(case_id, self.client_b, target, included_key, included)
+ negative = self.assert_absent_for(
+ case_id, self.client_b, target, excluded_key, self.args.negative_window
+ )
+ return [
+ self.save_json(case_id, "prefix-match.json", final),
+ self.save_json(case_id, "prefix-control.json", negative),
+ ]
+
+ def case_bucket_tag_filter(self, case_id: str) -> list[str]:
+ source, target, config = self.setup_bucket_replication(case_id)
+ rule = config["Rules"][0]
+ destination = rule["Destination"]["Bucket"]
+ filtered = {
+ "Role": config.get("Role", destination),
+ "Rules": [
+ {
+ "ID": "tag-filter",
+ "Priority": 1,
+ "Filter": {"Tag": {"Key": "class", "Value": "gold"}},
+ "Status": "Enabled",
+ "Destination": {"Bucket": destination},
+ "DeleteMarkerReplication": {"Status": "Disabled"},
+ "ExistingObjectReplication": {"Status": "Disabled"},
+ }
+ ],
+ }
+ self.client_a.put_bucket_replication(Bucket=source, ReplicationConfiguration=filtered)
+ self.save_json(case_id, "tag-replication-configuration.json", filtered)
+ matching_key = "tags/gold.txt"
+ control_key = "tags/silver.txt"
+ matching = b"gold-object"
+ self.client_a.put_object(Bucket=source, Key=matching_key, Body=matching, Tagging="class=gold")
+ self.client_a.put_object(Bucket=source, Key=control_key, Body=b"silver-object", Tagging="class=silver")
+ final = self.wait_object(case_id, self.client_b, target, matching_key, matching)
+ negative = self.assert_absent_for(
+ case_id, self.client_b, target, control_key, self.args.negative_window
+ )
+ return [
+ self.save_json(case_id, "tag-match.json", final),
+ self.save_json(case_id, "tag-control.json", negative),
+ ]
+
+ def case_bucket_zero_byte_special_key(self, case_id: str) -> list[str]:
+ source, target, _ = self.setup_bucket_replication(case_id)
+ fixtures = {
+ "zero/empty.bin": b"",
+ "special/space key.txt": b"space",
+ "special/中文-对象.txt": "你好 RustFS".encode("utf-8"),
+ "special/plus+percent%question?.txt": b"reserved",
+ }
+ manifest: list[dict[str, Any]] = []
+ for key, body in fixtures.items():
+ response = self.client_a.put_object(Bucket=source, Key=key, Body=body)
+ final = self.wait_object(
+ case_id,
+ self.client_b,
+ target,
+ key,
+ body,
+ expected_version=response.get("VersionId"),
+ )
+ manifest.append({"key": key, **final})
+ return [self.save_json(case_id, "special-key-manifest.json", manifest)]
+
+ def case_bucket_multipart_object(self, case_id: str) -> list[str]:
+ source, target, _ = self.setup_bucket_replication(case_id)
+ key = "multipart/complete.bin"
+ parts = [b"a" * (5 * 1024 * 1024), b"b" * (1024 * 1024)]
+ created = self.client_a.create_multipart_upload(Bucket=source, Key=key)
+ uploaded = []
+ for number, body in enumerate(parts, 1):
+ response = self.client_a.upload_part(
+ Bucket=source,
+ Key=key,
+ UploadId=created["UploadId"],
+ PartNumber=number,
+ Body=body,
+ )
+ uploaded.append({"PartNumber": number, "ETag": response["ETag"]})
+ complete = self.client_a.complete_multipart_upload(
+ Bucket=source,
+ Key=key,
+ UploadId=created["UploadId"],
+ MultipartUpload={"Parts": uploaded},
+ )
+ body = b"".join(parts)
+ final = self.wait_object(
+ case_id,
+ self.client_b,
+ target,
+ key,
+ body,
+ expected_version=complete.get("VersionId"),
+ )
+ return [
+ self.save_json(
+ case_id,
+ "multipart-final.json",
+ {"uploadId": created["UploadId"], "parts": uploaded, "object": final},
+ )
+ ]
+
+ def case_bucket_copy_object(self, case_id: str) -> list[str]:
+ source, target, _ = self.setup_bucket_replication(case_id)
+ original_key = "copy/original.txt"
+ copy_key = "copy/clone.txt"
+ body = b"copy-object-replication"
+ self.client_a.put_object(Bucket=source, Key=original_key, Body=body)
+ copied = self.client_a.copy_object(
+ Bucket=source,
+ Key=copy_key,
+ CopySource={"Bucket": source, "Key": original_key},
+ Metadata={"copied": "true"},
+ MetadataDirective="REPLACE",
+ )
+ final = self.wait_object(
+ case_id,
+ self.client_b,
+ target,
+ copy_key,
+ body,
+ expected_version=copied.get("VersionId"),
+ )
+ if final.get("metadata", {}).get("copied") != "true":
+ raise AssertionError("CopyObject metadata did not replicate")
+ return [self.save_json(case_id, "copy-final.json", final)]
+
+ def case_bucket_delete_marker(self, case_id: str) -> list[str]:
+ source, target, _ = self.setup_bucket_replication(case_id, flags="delete-marker")
+ key = "delete-marker/object.txt"
+ put = self.client_a.put_object(Bucket=source, Key=key, Body=b"delete-marker")
+ self.wait_object(
+ case_id, self.client_b, target, key, b"delete-marker", expected_version=put.get("VersionId")
+ )
+ deleted = self.client_a.delete_object(Bucket=source, Key=key)
+ marker_id = deleted.get("VersionId")
+
+ def probe() -> tuple[bool, Any]:
+ response = self.client_b.list_object_versions(Bucket=target, Prefix=key)
+ markers = [
+ item["VersionId"]
+ for item in response.get("DeleteMarkers", [])
+ if item["Key"] == key
+ ]
+ versions = [
+ item["VersionId"] for item in response.get("Versions", []) if item["Key"] == key
+ ]
+ return marker_id in markers and put.get("VersionId") in versions, {
+ "deleteMarkers": markers,
+ "versions": versions,
+ "expectedMarker": marker_id,
+ }
+
+ final = self.wait_until(case_id, "delete marker replication", probe)
+ return [self.save_json(case_id, "delete-marker-final.json", final)]
+
+ def case_bucket_version_delete(self, case_id: str) -> list[str]:
+ source, target, _ = self.setup_bucket_replication(case_id, flags="delete")
+ key = "version-delete/object.txt"
+ first = self.client_a.put_object(Bucket=source, Key=key, Body=b"version-one")
+ second = self.client_a.put_object(Bucket=source, Key=key, Body=b"version-two")
+ self.wait_object(
+ case_id, self.client_b, target, key, b"version-two", expected_version=second.get("VersionId")
+ )
+ self.client_a.delete_object(Bucket=source, Key=key, VersionId=first["VersionId"])
+
+ def probe() -> tuple[bool, Any]:
+ response = self.client_b.list_object_versions(Bucket=target, Prefix=key)
+ versions = [
+ item["VersionId"] for item in response.get("Versions", []) if item["Key"] == key
+ ]
+ return first["VersionId"] not in versions and second["VersionId"] in versions, {
+ "versions": versions,
+ "deleted": first["VersionId"],
+ "retained": second["VersionId"],
+ }
+
+ final = self.wait_until(case_id, "permanent version delete replication", probe)
+ return [self.save_json(case_id, "version-delete-final.json", final)]
+
+ def case_bucket_existing_object_enabled(self, case_id: str) -> list[str]:
+ key = "existing/before-rule.txt"
+ body = b"existing-object-enabled"
+ source, target, _ = self.setup_bucket_replication(
+ case_id, flags="existing-objects", preexisting=(key, body)
+ )
+ source_version = self.client_a.head_object(Bucket=source, Key=key).get("VersionId")
+ final = self.wait_object(
+ case_id,
+ self.client_b,
+ target,
+ key,
+ body,
+ expected_version=source_version,
+ timeout=max(self.args.timeout, 240),
+ )
+ return [self.save_json(case_id, "existing-object-final.json", final)]
+
+ def case_bucket_existing_object_disabled(self, case_id: str) -> list[str]:
+ key = "existing/before-rule.txt"
+ body = b"existing-object-disabled"
+ source, target, _ = self.setup_bucket_replication(case_id, preexisting=(key, body))
+ new_key = "existing/after-rule.txt"
+ new_body = b"new-object-positive-control"
+ self.client_a.put_object(Bucket=source, Key=new_key, Body=new_body)
+ positive = self.wait_object(case_id, self.client_b, target, new_key, new_body)
+ negative = self.assert_absent_for(
+ case_id, self.client_b, target, key, max(self.args.negative_window, 20)
+ )
+ return [
+ self.save_json(case_id, "new-object-control.json", positive),
+ self.save_json(case_id, "existing-object-control.json", negative),
+ ]
+
+ def case_bucket_target_check(self, case_id: str) -> list[str]:
+ source, target, _ = self.setup_bucket_replication(
+ case_id, flags="delete,delete-marker"
+ )
+ status, headers, body = self.signed_request(
+ self.site_a, "GET", f"/{source}", query="replication-check"
+ )
+ evidence = {
+ "httpStatus": status,
+ "headers": headers,
+ "body": body.decode("utf-8", errors="replace"),
+ }
+ if status != 200:
+ raise AssertionError(f"replication-check returned HTTP {status}: {evidence['body'][:500]}")
+ if not body.strip():
+ raise AssertionError("replication-check returned an empty response")
+ parsed = self.json_body(body)
+ evidence["parsed"] = parsed
+ overall = str(parsed.get("Status", parsed.get("status", ""))).upper()
+ if overall != "OK":
+ raise AssertionError(f"replication-check reported {overall}: {parsed}")
+ if parsed.get("ActiveMutation") is not True:
+ raise AssertionError("replication-check did not identify its active mutation")
+ if parsed.get("ProbeNamespace") != ".rustfs.sys/replication-check/":
+ raise AssertionError(f"unexpected probe namespace: {parsed.get('ProbeNamespace')!r}")
+ targets = parsed.get("Targets", [])
+ if not targets:
+ raise AssertionError("replication-check returned no target results")
+ required_phases = {
+ "Bucket",
+ "Versioning",
+ "ObjectLock",
+ "Put",
+ "VersionFidelity",
+ "DeleteMarker",
+ "VersionDelete",
+ "Cleanup",
+ }
+ for item in targets:
+ if str(item.get("Status", "")).upper() != "OK":
+ raise AssertionError(f"replication-check target failed: {item}")
+ phases = item.get("Phases", {})
+ missing = required_phases - set(phases)
+ failed = {
+ name: value
+ for name, value in phases.items()
+ if name in required_phases and str(value.get("Status", "")).upper() != "OK"
+ }
+ if missing or failed:
+ raise AssertionError(
+ f"replication-check phase mismatch: missing={sorted(missing)} failed={failed}"
+ )
+ leftovers = self.client_b.list_object_versions(
+ Bucket=target, Prefix=".rustfs.sys/replication-check/"
+ )
+ if leftovers.get("Versions") or leftovers.get("DeleteMarkers"):
+ raise AssertionError("replication-check left probe artifacts on the target")
+ return [
+ self.save_json(case_id, "replication-check.json", evidence),
+ self.save_json(case_id, "replication-check-leftovers.json", leftovers),
+ ]
+
+ def case_bucket_status_observability(self, case_id: str) -> list[str]:
+ source, target, config = self.setup_bucket_replication(case_id)
+ body = b"status-observability"
+ put = self.client_a.put_object(Bucket=source, Key="status/object.txt", Body=body)
+ self.wait_object(
+ case_id,
+ self.client_b,
+ target,
+ "status/object.txt",
+ body,
+ expected_version=put.get("VersionId"),
+ )
+ listed = self.run_rc(
+ case_id,
+ "status-list",
+ ["bucket", "replication", "list", f"{self.alias_a}/{source}", "--force"],
+ )
+ raw_status, raw_headers, raw_body = self.admin_request(
+ self.site_a,
+ "GET",
+ "replicationmetrics",
+ query=urllib.parse.urlencode({"bucket": source}),
+ )
+ raw_document = {
+ "httpStatus": raw_status,
+ "headers": raw_headers,
+ "body": raw_body.decode("utf-8", errors="replace"),
+ }
+ if raw_status != 200:
+ raise AssertionError(f"raw replication metrics returned HTTP {raw_status}")
+ raw_document["parsed"] = self.json_body(raw_body)
+ self.save_json(case_id, "raw-replication-metrics.json", raw_document)
+ status_arguments = [
+ "bucket",
+ "replication",
+ "status",
+ f"{self.alias_a}/{source}",
+ "--force",
+ ]
+
+ def metrics_probe() -> tuple[bool, Any]:
+ observed = self.rc_probe(status_arguments)
+ if observed.returncode != 0:
+ try:
+ error_document = json.loads(observed.stderr)
+ except json.JSONDecodeError:
+ error_document = {}
+ details = error_document.get("error", {}) if isinstance(error_document, dict) else {}
+ if details and details.get("retryable") is False:
+ raise AssertionError(
+ "rc could not decode the server replication metrics response: "
+ f"{details.get('message', observed.stderr[:1000])}"
+ )
+ return False, {
+ "exitCode": observed.returncode,
+ "stderr": observed.stderr[:1000],
+ }
+ try:
+ document = json.loads(observed.stdout)
+ except json.JSONDecodeError:
+ return False, {"stdout": observed.stdout[:1000]}
+ return replication_metrics_ready(document), document
+
+ metrics = self.wait_until(case_id, "non-zero replication status metrics", metrics_probe)
+ self.save_json(case_id, "status-metrics.json", metrics)
+ combined = listed.stdout + json.dumps(metrics)
+ identifiers = [source, config["Rules"][0].get("ID", ""), target]
+ if not any(identifier and identifier in combined for identifier in identifiers):
+ raise AssertionError("rc replication output contains no configured identifier")
+ if self.args.secret_key in combined:
+ raise AssertionError("rc replication status leaked the administrator secret")
+ return [
+ self.save_json(
+ case_id,
+ "observability-summary.json",
+ {
+ "identifiers": identifiers,
+ "outputBytes": len(combined),
+ "metrics": metrics,
+ },
+ )
+ ]
+
+ def case_bucket_outage_recovery(self, case_id: str) -> list[str]:
+ source, target, _ = self.setup_bucket_replication(case_id)
+ key = "outage/recover.txt"
+ body = b"target-outage-recovery"
+ evidence = [self.hook(case_id, "site-b-stop", self.args.site_b_stop_hook)]
+ try:
+ response = self.client_a.put_object(Bucket=source, Key=key, Body=body)
+ finally:
+ evidence.append(self.hook(case_id, "site-b-start", self.args.site_b_start_hook))
+ self.wait_api(case_id, self.client_b, "site-b")
+ final = self.wait_object(
+ case_id,
+ self.client_b,
+ target,
+ key,
+ body,
+ expected_version=response.get("VersionId"),
+ timeout=max(self.args.timeout, 180),
+ )
+ evidence.append(self.save_json(case_id, "outage-recovery-final.json", final))
+ return evidence
+
+ def case_bucket_source_restart_recovery(self, case_id: str) -> list[str]:
+ source, target, _ = self.setup_bucket_replication(case_id)
+ key = "mrf/restart.txt"
+ body = b"source-restart-mrf-recovery"
+ evidence = [self.hook(case_id, "site-b-stop", self.args.site_b_stop_hook)]
+ b_started = False
+ try:
+ response = self.client_a.put_object(Bucket=source, Key=key, Body=body)
+ evidence.append(self.hook(case_id, "site-a-restart", self.args.site_a_restart_hook))
+ self.wait_api(case_id, self.client_a, "site-a")
+ evidence.append(self.hook(case_id, "site-b-start", self.args.site_b_start_hook))
+ b_started = True
+ self.wait_api(case_id, self.client_b, "site-b")
+ finally:
+ if not b_started:
+ self.hook(case_id, "site-b-start-recovery", self.args.site_b_start_hook)
+ self.wait_api(case_id, self.client_b, "site-b")
+ final = self.wait_object(
+ case_id,
+ self.client_b,
+ target,
+ key,
+ body,
+ expected_version=response.get("VersionId"),
+ timeout=max(self.args.timeout, 240),
+ )
+ evidence.append(self.save_json(case_id, "source-restart-final.json", final))
+ return evidence
+
+ # ------------------------------------------------------------------
+ # Site replication cases
+ # ------------------------------------------------------------------
+
+ def site_info(self, site: Site) -> dict[str, Any]:
+ status, _, body = self.admin_request(site, "GET", "site-replication/info")
+ if status != 200:
+ raise AssertionError(
+ f"{site.id} site-replication info returned HTTP {status}: "
+ f"{body.decode(errors='replace')[:500]}"
+ )
+ parsed = self.json_body(body)
+ if not isinstance(parsed, dict):
+ raise AssertionError(f"{site.id} info is not an object")
+ return parsed
+
+ def site_group_snapshot(self) -> tuple[str, str, dict[str, Any]]:
+ info = {
+ site.id: self.site_info(site)
+ for site in (self.site_a, self.site_b)
+ }
+ state, reason = classify_site_group_state(
+ info,
+ (self.site_a.endpoint, self.site_b.endpoint),
+ )
+ return state, reason, info
+
+ def wait_site_group_active(self, case_id: str) -> dict[str, Any]:
+ def probe() -> tuple[bool, Any]:
+ try:
+ state, reason, info = self.site_group_snapshot()
+ except Exception as exc:
+ return False, {
+ "state": "unavailable",
+ "reason": f"{type(exc).__name__}: {exc}",
+ }
+ detail = {"state": state, "reason": reason, "sites": info}
+ return state == "active", detail
+
+ return self.wait_until(
+ case_id,
+ "stable two-site replication group",
+ probe,
+ timeout=max(self.args.timeout, 120),
+ )
+
+ def wait_site_group_disabled(self, case_id: str) -> dict[str, Any]:
+ def probe() -> tuple[bool, Any]:
+ try:
+ state, reason, info = self.site_group_snapshot()
+ except Exception as exc:
+ return False, {
+ "state": "unavailable",
+ "reason": f"{type(exc).__name__}: {exc}",
+ }
+ detail = {"state": state, "reason": reason, "sites": info}
+ return state == "disabled", detail
+
+ return self.wait_until(
+ case_id,
+ "cleanly disabled two-site replication group",
+ probe,
+ timeout=max(self.args.timeout, 120),
+ )
+
+ def wait_site_group_ilm_expiry_value(
+ self,
+ case_id: str,
+ expected: bool,
+ ) -> dict[str, Any]:
+ def probe() -> tuple[bool, Any]:
+ try:
+ state, state_reason, info = self.site_group_snapshot()
+ except Exception as exc:
+ return False, {
+ "state": "unavailable",
+ "stateReason": f"{type(exc).__name__}: {exc}",
+ }
+ valid, flag_reason = site_group_has_ilm_expiry_value(info, expected)
+ detail = {
+ "state": state,
+ "stateReason": state_reason,
+ "flagReason": flag_reason,
+ "sites": info,
+ }
+ return state == "active" and valid, detail
+
+ return self.wait_until(
+ case_id,
+ f"site replicate-ilm-expiry={str(expected).lower()}",
+ probe,
+ timeout=max(self.args.timeout, 120),
+ )
+
+ def ensure_site_group_active(
+ self,
+ case_id: str,
+ *,
+ evidence_prefix: str,
+ refuse_recreate: bool,
+ ) -> tuple[str, dict[str, Any]]:
+ self.events.record(
+ "setup.start",
+ case=case_id,
+ fixture=SITE_GROUP_ACTIVE_FIXTURE,
+ )
+ try:
+ state, reason, initial = self.site_group_snapshot()
+ self.save_json(
+ case_id,
+ f"{evidence_prefix}-initial.json",
+ {"state": state, "reason": reason, "sites": initial},
+ )
+ outcome = "reused"
+ if state == "disabled":
+ if refuse_recreate:
+ raise PreconditionError(
+ "site group became disabled after the diagnostic fixture was established"
+ )
+ self.add_site_replication_group(
+ case_id,
+ f"{evidence_prefix}-add-response.json",
+ require_clean_initial_sync=True,
+ )
+ outcome = "created"
+ elif state != "active":
+ raise PreconditionError(
+ f"site-group fixture refuses {state} control-plane state: {reason}"
+ )
+
+ final = (
+ {"state": state, "reason": reason, "sites": initial}
+ if state == "active"
+ else self.wait_site_group_active(case_id)
+ )
+ self.save_json(
+ case_id,
+ f"{evidence_prefix}-final.json",
+ {"outcome": outcome, **final},
+ )
+ self.events.record(
+ "setup.group.ready",
+ case=case_id,
+ fixture=SITE_GROUP_ACTIVE_FIXTURE,
+ outcome=outcome,
+ )
+ return outcome, final
+ except Exception as exc:
+ self.events.record(
+ "setup.error",
+ case=case_id,
+ fixture=SITE_GROUP_ACTIVE_FIXTURE,
+ errorType=type(exc).__name__,
+ message=self.redact(str(exc)),
+ )
+ if isinstance(exc, PreconditionError):
+ raise
+ raise PreconditionError(
+ f"site-group fixture could not establish an active group: "
+ f"{type(exc).__name__}: {exc}"
+ ) from exc
+
+ def ensure_planned_fixture(self, case_id: str) -> None:
+ if (
+ SITE_GROUP_ACTIVE_FIXTURE not in self.requested_fixtures
+ or case_id not in SITE_GROUP_ACTIVE_CASE_IDS
+ ):
+ return
+ try:
+ if self.site_group_tracker_state in {"tainted", "removed"}:
+ raise PreconditionError(
+ "site-group fixture refuses to repair a group after an earlier "
+ f"{self.site_group_tracker_state} transition"
+ )
+ outcome, final = self.ensure_site_group_active(
+ case_id,
+ evidence_prefix="setup-site-group",
+ refuse_recreate=(
+ self.site_group_tracker_state != "fresh"
+ or self.site_group_mutation_attempted
+ ),
+ )
+ self.site_group_tracker_state = "active"
+ if case_id == "site-replication.lifecycle-expiry-toggle":
+ valid, reason = site_group_has_ilm_expiry_value(
+ final["sites"],
+ False,
+ )
+ self.save_json(
+ case_id,
+ "setup-site-group-ilm-expiry.json",
+ {"valid": valid, "reason": reason, "sites": final["sites"]},
+ )
+ if not valid:
+ self.events.record(
+ "setup.constraint.error",
+ case=case_id,
+ fixture=SITE_GROUP_ACTIVE_FIXTURE,
+ message=reason,
+ )
+ raise PreconditionError(reason)
+ self.events.record(
+ "setup.complete",
+ case=case_id,
+ fixture=SITE_GROUP_ACTIVE_FIXTURE,
+ outcome=outcome,
+ )
+ self.fixture_outcomes.append(
+ {
+ "caseId": case_id,
+ "fixture": SITE_GROUP_ACTIVE_FIXTURE,
+ "state": "pass",
+ "outcome": outcome,
+ }
+ )
+ except Exception as exc:
+ message = (
+ str(exc)
+ if isinstance(exc, PreconditionError)
+ else f"{type(exc).__name__}: {exc}"
+ )
+ redacted_message = self.redact(f"precondition_error: {message}")
+ self.save_json(
+ case_id,
+ "setup-site-group-error.json",
+ {
+ "fixture": SITE_GROUP_ACTIVE_FIXTURE,
+ "state": "infra_error",
+ "message": redacted_message,
+ "trackerState": self.site_group_tracker_state,
+ },
+ )
+ self.events.record(
+ "setup.blocked",
+ case=case_id,
+ fixture=SITE_GROUP_ACTIVE_FIXTURE,
+ message=redacted_message,
+ )
+ self.fixture_outcomes.append(
+ {
+ "caseId": case_id,
+ "fixture": SITE_GROUP_ACTIVE_FIXTURE,
+ "state": "infra_error",
+ "message": redacted_message,
+ }
+ )
+ if isinstance(exc, PreconditionError):
+ raise
+ raise PreconditionError(message) from exc
+
+ def site_peer(self, info: dict[str, Any], endpoint: str) -> dict[str, Any]:
+ identity = _site_endpoint_identity(endpoint)
+ if identity is None:
+ raise AssertionError(f"invalid configured site endpoint: {endpoint}")
+ matches = [
+ peer
+ for peer in info.get("sites", [])
+ if _site_endpoint_identity(peer.get("endpoint")) == identity
+ ]
+ if len(matches) != 1:
+ raise AssertionError(f"expected one peer for {endpoint}, got {len(matches)}")
+ return matches[0]
+
+ def wait_bucket(self, case_id: str, client: Any, bucket: str, present: bool = True) -> Any:
+ def probe() -> tuple[bool, Any]:
+ try:
+ response = client.get_bucket_location(Bucket=bucket)
+ exists = True
+ detail: Any = without_response_metadata(response)
+ except ClientError as exc:
+ code = exc.response.get("Error", {}).get("Code", "unknown")
+ exists = code not in {"404", "NoSuchBucket", "NotFound"}
+ detail = {"error": code}
+ return exists is present, detail
+
+ return self.wait_until(case_id, f"bucket {bucket} present={present}", probe)
+
+ def create_site_bucket(self, case_id: str, role: str = "site") -> str:
+ bucket = self.bucket_name(case_id, role)
+ self.create_bucket(self.client_a, bucket)
+ self.wait_bucket(case_id, self.client_b, bucket)
+ self.created_buckets.append((self.client_b, bucket))
+ return bucket
+
+ def wait_versioning(self, case_id: str, bucket: str, status: str) -> dict[str, Any]:
+ def probe() -> tuple[bool, Any]:
+ response = self.client_b.get_bucket_versioning(Bucket=bucket)
+ observed = response.get("Status")
+ return observed == status, {"expected": status, "observed": observed}
+
+ return self.wait_until(case_id, f"versioning={status} on site-b", probe)
+
+ def case_site_unconfigured_info(self, case_id: str) -> list[str]:
+ info = {site.id: self.site_info(site) for site in (self.site_a, self.site_b)}
+ state, reason = classify_site_group_state(
+ info,
+ (self.site_a.endpoint, self.site_b.endpoint),
+ )
+ if state != "disabled":
+ raise AssertionError(
+ f"fresh sites are not cleanly unconfigured: {state}: {reason}"
+ )
+ return [self.save_json(case_id, "unconfigured-info.json", info)]
+
+ def case_site_invalid_credentials(self, case_id: str) -> list[str]:
+ payload = [
+ {
+ "name": "site-b-invalid",
+ "endpoints": self.site_b.endpoint,
+ "accessKey": self.site_b.access_key,
+ "secretKey": "definitely-invalid-site-secret",
+ }
+ ]
+ status, headers, body = self.admin_request(
+ self.site_a,
+ "PUT",
+ "site-replication/add",
+ query="replicateILMExpiry=false",
+ payload=payload,
+ )
+ self.secrets.add("definitely-invalid-site-secret")
+ document = {
+ "httpStatus": status,
+ "headers": headers,
+ "body": body.decode("utf-8", errors="replace"),
+ }
+ if status < 400:
+ parsed = self.required_json_object(body, "invalid-credential site add")
+ if parsed.get("success") is not False:
+ raise AssertionError(f"site add with invalid credentials succeeded: {parsed}")
+ info = {site.id: self.site_info(site) for site in (self.site_a, self.site_b)}
+ state, reason = classify_site_group_state(
+ info,
+ (self.site_a.endpoint, self.site_b.endpoint),
+ )
+ if state != "disabled":
+ raise AssertionError(
+ "invalid site add did not preserve a clean disabled state: "
+ f"{state}: {reason}"
+ )
+ return [
+ self.save_json(case_id, "invalid-add-response.json", document),
+ self.save_json(case_id, "invalid-add-info.json", info),
+ ]
+
+ def case_site_add_and_info(self, case_id: str) -> list[str]:
+ result, response_evidence = self.add_site_replication_group(
+ case_id, "add-response.json", require_clean_initial_sync=True
+ )
+ settled = self.wait_site_group_active(case_id)
+ info = settled["sites"]
+ serialized = json.dumps({"result": result, "info": info})
+ if self.args.secret_key in serialized:
+ raise AssertionError("site replication response leaked administrator secret")
+ return [
+ response_evidence,
+ self.save_json(case_id, "paired-info.json", settled),
+ ]
+
+ def add_site_replication_group(
+ self,
+ case_id: str,
+ evidence_name: str,
+ *,
+ require_clean_initial_sync: bool,
+ ) -> tuple[dict[str, Any], str]:
+ self.site_group_mutation_attempted = True
+ peers = [
+ {
+ "name": site.id,
+ "endpoints": site.endpoint,
+ "accessKey": site.access_key,
+ "secretKey": site.secret_key,
+ }
+ for site in (self.site_a, self.site_b)
+ ]
+ status, headers, body = self.admin_request(
+ self.site_a,
+ "PUT",
+ "site-replication/add",
+ query="replicateILMExpiry=false",
+ payload=peers,
+ )
+ if status != 200:
+ raise AssertionError(
+ f"site-replication add HTTP {status}: {body.decode(errors='replace')[:500]}"
+ )
+ result = self.required_json_object(body, "site-replication add")
+ response_evidence = self.save_json(
+ case_id,
+ evidence_name,
+ {"httpStatus": status, "headers": headers, "result": result},
+ )
+ if result.get("success") is not True:
+ raise AssertionError(f"site-replication add rejected: {result}")
+ if require_clean_initial_sync and result.get("initialSyncErrorMessage"):
+ raise AssertionError(f"site-replication initial sync was partial: {result}")
+ return result, response_evidence
+
+ def case_site_status_observability(self, case_id: str) -> list[str]:
+ query = (
+ "buckets=true&users=true&groups=true&policies=true&metrics=true&"
+ "peer-state=true&ilm-expiry-rules=true"
+ )
+ status_documents: dict[str, Any] = {}
+ for site in (self.site_a, self.site_b):
+ status, _, body = self.admin_request(
+ site, "GET", "site-replication/status", query=query
+ )
+ if status != 200:
+ raise AssertionError(f"{site.id} status returned HTTP {status}")
+ parsed = self.json_body(body)
+ status_documents[site.id] = parsed
+ if parsed.get("enabled") is not True:
+ raise AssertionError(f"{site.id} status is not enabled")
+ sites = parsed.get("Sites") or parsed.get("sites")
+ if not isinstance(sites, dict) or len(sites) != 2:
+ raise AssertionError(f"{site.id} status omitted peer sites")
+ for bound in (
+ "MaxBuckets",
+ "MaxUsers",
+ "MaxGroups",
+ "MaxPolicies",
+ "MaxILMExpiryRules",
+ ):
+ if not isinstance(parsed.get(bound), int):
+ raise AssertionError(f"{site.id} status omitted numeric {bound}")
+ if not isinstance(parsed.get("Metrics"), dict):
+ raise AssertionError(f"{site.id} status omitted Metrics scope")
+ serialized = json.dumps(status_documents)
+ if self.args.secret_key in serialized or "secretKey" in serialized:
+ raise AssertionError("status response leaked credentials")
+ return [self.save_json(case_id, "site-status.json", status_documents)]
+
+ def case_site_bucket_create(self, case_id: str) -> list[str]:
+ bucket = self.create_site_bucket(case_id, "bucket")
+ return [
+ self.save_json(
+ case_id,
+ "bucket-create.json",
+ {
+ "bucket": bucket,
+ "siteA": [item["Name"] for item in self.client_a.list_buckets().get("Buckets", [])],
+ "siteB": [item["Name"] for item in self.client_b.list_buckets().get("Buckets", [])],
+ },
+ )
+ ]
+
+ def case_site_object_bidirectional(self, case_id: str) -> list[str]:
+ bucket = self.create_site_bucket(case_id, "bidir")
+ self.enable_versioning(self.client_a, bucket)
+ self.wait_versioning(case_id, bucket, "Enabled")
+ fixtures = [
+ (self.client_a, self.client_b, "from-a.txt", b"active-active-a", "a-to-b"),
+ (self.client_b, self.client_a, "from-b.txt", b"active-active-b", "b-to-a"),
+ ]
+ observations = []
+ for source_client, target_client, key, body, direction in fixtures:
+ response = source_client.put_object(
+ Bucket=bucket, Key=key, Body=body, Metadata={"direction": direction}
+ )
+ final = self.wait_object(
+ case_id,
+ target_client,
+ bucket,
+ key,
+ body,
+ expected_version=response.get("VersionId"),
+ )
+ if final.get("metadata", {}).get("direction") != direction:
+ raise AssertionError(f"metadata mismatch for {direction}")
+ observations.append({"direction": direction, **final})
+ time.sleep(max(2, self.args.poll_interval * 2))
+ for client in (self.client_a, self.client_b):
+ versions = client.list_object_versions(Bucket=bucket)
+ relevant = [item for item in versions.get("Versions", []) if item["Key"] in {"from-a.txt", "from-b.txt"}]
+ if len(relevant) != 2:
+ raise AssertionError(f"replication loop or missing version detected: {relevant}")
+ return [self.save_json(case_id, "bidirectional-final.json", observations)]
+
+ def case_site_versioning_metadata(self, case_id: str) -> list[str]:
+ bucket = self.create_site_bucket(case_id, "versioning")
+ self.client_a.put_bucket_versioning(
+ Bucket=bucket, VersioningConfiguration={"Status": "Enabled"}
+ )
+ observations: dict[str, Any] = {
+ "enabled": self.wait_versioning(case_id, bucket, "Enabled")
+ }
+ try:
+ self.client_a.put_bucket_versioning(
+ Bucket=bucket, VersioningConfiguration={"Status": "Suspended"}
+ )
+ except ClientError as exc:
+ code = exc.response.get("Error", {}).get("Code", "")
+ if code != "InvalidBucketState":
+ raise
+ observations["suspendRejected"] = {
+ "error": code,
+ "message": exc.response.get("Error", {}).get("Message", ""),
+ }
+ else:
+ raise AssertionError(
+ "site-replicated bucket allowed versioning suspension while a replication rule was active"
+ )
+ source_status = self.client_a.get_bucket_versioning(Bucket=bucket).get("Status")
+ if source_status != "Enabled":
+ raise AssertionError(f"source versioning changed after rejected suspension: {source_status!r}")
+ observations["remainedEnabled"] = self.wait_versioning(
+ case_id, bucket, "Enabled"
+ )
+ self.client_a.put_bucket_versioning(
+ Bucket=bucket, VersioningConfiguration={"Status": "Enabled"}
+ )
+ observations["idempotentEnable"] = self.wait_versioning(
+ case_id, bucket, "Enabled"
+ )
+ return [self.save_json(case_id, "versioning-propagation.json", observations)]
+
+ def case_site_lifecycle_expiry_toggle(self, case_id: str) -> list[str]:
+ state, state_reason, initial_info = self.site_group_snapshot()
+ initial_evidence = self.save_json(
+ case_id,
+ "lifecycle-initial-site-info.json",
+ {"state": state, "reason": state_reason, "sites": initial_info},
+ )
+ if state != "active":
+ raise PreconditionError(
+ f"lifecycle case requires an active site group: {state}: {state_reason}"
+ )
+ flags_valid, flags_reason = site_group_has_ilm_expiry_value(
+ initial_info,
+ False,
+ )
+ if not flags_valid:
+ raise PreconditionError(flags_reason)
+
+ bucket = self.create_site_bucket(case_id, "ilm")
+ initial = {
+ "Rules": [
+ {
+ "ID": "expiry-disabled-at-site-level",
+ "Status": "Enabled",
+ "Filter": {"Prefix": "expiry/"},
+ "Expiration": {"Days": 30},
+ }
+ ]
+ }
+ self.client_a.put_bucket_lifecycle_configuration(
+ Bucket=bucket, LifecycleConfiguration=initial
+ )
+ deadline = time.monotonic() + self.args.negative_window
+ while time.monotonic() < deadline:
+ try:
+ remote = self.client_b.get_bucket_lifecycle_configuration(Bucket=bucket)
+ except ClientError as exc:
+ if exc.response.get("Error", {}).get("Code") not in {
+ "NoSuchLifecycleConfiguration",
+ "NoSuchLifecycle",
+ }:
+ raise
+ else:
+ if remote.get("Rules"):
+ raise AssertionError("expiry rules replicated while global ILM expiry was disabled")
+ time.sleep(self.args.poll_interval)
+ enable_attempted = False
+ primary_error: Exception | None = None
+ rollback_error: Exception | None = None
+ replicated: dict[str, Any] = {}
+ enabled_info: dict[str, Any] = {}
+ final_info: dict[str, Any] = {}
+ enable_evidence = ""
+ disable_evidence = ""
+ try:
+ enable_attempted = True
+ status, headers, body = self.admin_request(
+ self.site_a,
+ "PUT",
+ "site-replication/edit",
+ query="enableILMExpiryReplication=true",
+ payload={},
+ )
+ enable_result = self.required_json_object(
+ body,
+ "enable ILM expiry replication",
+ )
+ enable_evidence = self.save_json(
+ case_id,
+ "lifecycle-enable-response.json",
+ {
+ "httpStatus": status,
+ "headers": headers,
+ "result": enable_result,
+ },
+ )
+ if (
+ status != 200
+ or enable_result.get("success") is not True
+ ):
+ raise AssertionError(
+ f"enable ILM expiry replication failed: HTTP {status} {body!r}"
+ )
+ enabled_info = self.wait_site_group_ilm_expiry_value(case_id, True)
+ updated = {
+ "Rules": [
+ {
+ "ID": "expiry-enabled-at-site-level",
+ "Status": "Enabled",
+ "Filter": {"Prefix": "expiry/"},
+ "Expiration": {"Days": 29},
+ }
+ ]
+ }
+ self.client_a.put_bucket_lifecycle_configuration(
+ Bucket=bucket, LifecycleConfiguration=updated
+ )
+
+ def lifecycle_probe() -> tuple[bool, Any]:
+ try:
+ response = self.client_b.get_bucket_lifecycle_configuration(
+ Bucket=bucket
+ )
+ except ClientError as exc:
+ return False, {
+ "error": exc.response.get("Error", {}).get("Code")
+ }
+ rules = response.get("Rules", [])
+ return any(
+ rule.get("ID") == "expiry-enabled-at-site-level"
+ for rule in rules
+ ), response
+
+ replicated = self.wait_until(
+ case_id,
+ "ILM expiry metadata replication",
+ lifecycle_probe,
+ )
+ except Exception as exc:
+ primary_error = exc
+ finally:
+ if enable_attempted:
+ try:
+ status, headers, body = self.admin_request(
+ self.site_a,
+ "PUT",
+ "site-replication/edit",
+ query="disableILMExpiryReplication=true",
+ payload={},
+ )
+ disable_result = self.required_json_object(
+ body,
+ "disable ILM expiry replication",
+ )
+ disable_evidence = self.save_json(
+ case_id,
+ "lifecycle-disable-response.json",
+ {
+ "httpStatus": status,
+ "headers": headers,
+ "result": disable_result,
+ },
+ )
+ if (
+ status != 200
+ or disable_result.get("success") is not True
+ ):
+ raise AssertionError(
+ "disable ILM expiry replication failed: "
+ f"HTTP {status} {body!r}"
+ )
+ final_info = self.wait_site_group_ilm_expiry_value(
+ case_id,
+ False,
+ )
+ except Exception as exc:
+ rollback_error = exc
+
+ if rollback_error is not None:
+ self.site_group_tracker_state = "tainted"
+ if primary_error is not None:
+ raise RuntimeError(
+ f"{type(primary_error).__name__}: {primary_error}; "
+ "lifecycle rollback failed: "
+ f"{type(rollback_error).__name__}: {rollback_error}"
+ ) from rollback_error
+ raise rollback_error
+ if primary_error is not None:
+ raise primary_error
+ return [
+ initial_evidence,
+ enable_evidence,
+ self.save_json(case_id, "lifecycle-enabled-site-info.json", enabled_info),
+ self.save_json(case_id, "lifecycle-replicated.json", replicated),
+ disable_evidence,
+ self.save_json(case_id, "lifecycle-toggle-final-info.json", final_info),
+ ]
+
+ def case_site_bucket_policy_metadata(self, case_id: str) -> list[str]:
+ bucket = self.create_site_bucket(case_id, "policy")
+ policy = {
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Sid": "ReadObjects",
+ "Effect": "Allow",
+ "Principal": {"AWS": ["*"]},
+ "Action": ["s3:GetObject"],
+ "Resource": [f"arn:aws:s3:::{bucket}/*"],
+ }
+ ],
+ }
+ self.client_a.put_bucket_policy(Bucket=bucket, Policy=json.dumps(policy))
+ source_policy = json.loads(
+ self.client_a.get_bucket_policy(Bucket=bucket)["Policy"]
+ )
+
+ def policy_probe() -> tuple[bool, Any]:
+ try:
+ response = self.client_b.get_bucket_policy(Bucket=bucket)
+ except ClientError as exc:
+ return False, {"error": exc.response.get("Error", {}).get("Code")}
+ observed = json.loads(response["Policy"])
+ return observed == source_policy, observed
+
+ replicated = self.wait_until(case_id, "bucket policy replication", policy_probe)
+ self.client_a.delete_bucket_policy(Bucket=bucket)
+
+ def delete_probe() -> tuple[bool, Any]:
+ try:
+ self.client_b.get_bucket_policy(Bucket=bucket)
+ except ClientError as exc:
+ code = exc.response.get("Error", {}).get("Code", "")
+ return code in {"NoSuchBucketPolicy", "NoSuchPolicy", "NoSuchBucket"}, {"error": code}
+ return False, {"policy": "still present"}
+
+ deleted = self.wait_until(case_id, "bucket policy deletion replication", delete_probe)
+ return [
+ self.save_json(case_id, "policy-replicated.json", replicated),
+ self.save_json(case_id, "policy-deleted.json", deleted),
+ ]
+
+ def case_site_object_tags(self, case_id: str) -> list[str]:
+ bucket = self.create_site_bucket(case_id, "tags")
+ key = "tagged/object.txt"
+ body = b"site-replication-tags"
+ self.client_a.put_object(Bucket=bucket, Key=key, Body=body)
+ self.wait_object(case_id, self.client_b, bucket, key, body)
+ expected_sets = [
+ [{"Key": "stage", "Value": "one"}],
+ [{"Key": "stage", "Value": "two"}, {"Key": "owner", "Value": "qa"}],
+ ]
+ observations = []
+ for tags in expected_sets:
+ self.client_a.put_object_tagging(Bucket=bucket, Key=key, Tagging={"TagSet": tags})
+
+ def probe(tags=tags) -> tuple[bool, Any]:
+ response = self.client_b.get_object_tagging(Bucket=bucket, Key=key)
+ observed = sorted(response.get("TagSet", []), key=lambda item: item["Key"])
+ expected = sorted(tags, key=lambda item: item["Key"])
+ return observed == expected, observed
+
+ observations.append(self.wait_until(case_id, "object tag replication", probe))
+ return [self.save_json(case_id, "object-tags.json", observations)]
+
+ def case_site_delete_convergence(self, case_id: str) -> list[str]:
+ bucket = self.create_site_bucket(case_id, "delete")
+ self.enable_versioning(self.client_a, bucket)
+ self.wait_versioning(case_id, bucket, "Enabled")
+ key = "delete/object.txt"
+ first = self.client_a.put_object(Bucket=bucket, Key=key, Body=b"site-version-one")
+ second = self.client_a.put_object(Bucket=bucket, Key=key, Body=b"site-version-two")
+ self.wait_object(
+ case_id,
+ self.client_b,
+ bucket,
+ key,
+ b"site-version-two",
+ expected_version=second.get("VersionId"),
+ )
+ marker = self.client_a.delete_object(Bucket=bucket, Key=key).get("VersionId")
+
+ def marker_probe() -> tuple[bool, Any]:
+ response = self.client_b.list_object_versions(Bucket=bucket, Prefix=key)
+ markers = [item["VersionId"] for item in response.get("DeleteMarkers", []) if item["Key"] == key]
+ return marker in markers, {"markers": markers, "expected": marker}
+
+ marker_final = self.wait_until(case_id, "site delete marker convergence", marker_probe)
+ self.client_a.delete_object(Bucket=bucket, Key=key, VersionId=first["VersionId"])
+
+ def purge_probe() -> tuple[bool, Any]:
+ response = self.client_b.list_object_versions(Bucket=bucket, Prefix=key)
+ versions = [item["VersionId"] for item in response.get("Versions", []) if item["Key"] == key]
+ return first["VersionId"] not in versions and second["VersionId"] in versions, versions
+
+ purge_final = self.wait_until(case_id, "site version purge convergence", purge_probe)
+ return [
+ self.save_json(case_id, "delete-marker.json", marker_final),
+ self.save_json(case_id, "version-purge.json", purge_final),
+ ]
+
+ def rc_probe(self, arguments: list[str]) -> subprocess.CompletedProcess[str]:
+ return subprocess.run(
+ [self.rc_bin, "--json", "--no-color", *arguments],
+ env=self.rc_env,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True,
+ check=False,
+ )
+
+ def policy_file(self, case_id: str, name: str, actions: list[str]) -> Path:
+ policy = {
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": actions,
+ "Resource": ["arn:aws:s3:::*"],
+ }
+ ],
+ }
+ path = self.evidence_path(case_id, name)
+ path.write_text(json.dumps(policy, indent=2, sort_keys=True) + "\n", encoding="utf-8")
+ return path
+
+ def wait_rc_success(
+ self, case_id: str, description: str, arguments: list[str]
+ ) -> subprocess.CompletedProcess[str]:
+ last: subprocess.CompletedProcess[str] | None = None
+
+ def probe() -> tuple[bool, Any]:
+ nonlocal last
+ last = self.rc_probe(arguments)
+ return last.returncode == 0, {
+ "exitCode": last.returncode,
+ "stdout": last.stdout[:1000],
+ "stderr": last.stderr[:1000],
+ }
+
+ self.wait_until(case_id, description, probe)
+ assert last is not None
+ self.save_json(
+ case_id,
+ f"{safe_name(description)}.json",
+ {"exitCode": last.returncode, "stdout": last.stdout, "stderr": last.stderr},
+ )
+ return last
+
+ def assert_iam_credentials(
+ self,
+ case_id: str,
+ access_key: str,
+ secret_key: str,
+ label: str,
+ ) -> dict[str, Any]:
+ client = self.s3_client(self.site_b, access_key, secret_key)
+
+ def allowed_probe() -> tuple[bool, Any]:
+ try:
+ response = client.list_buckets()
+ return True, {"bucketCount": len(response.get("Buckets", []))}
+ except ClientError as exc:
+ return False, {"error": exc.response.get("Error", {}).get("Code")}
+
+ allowed = self.wait_until(case_id, f"{label} authorization on site-b", allowed_probe)
+ denied_bucket = self.bucket_name(case_id, f"deny-{label}")
+ try:
+ client.create_bucket(Bucket=denied_bucket)
+ except ClientError as exc:
+ code = exc.response.get("Error", {}).get("Code", "")
+ if code not in {"AccessDenied", "Unauthorized", "InvalidAccessKeyId"}:
+ raise
+ denied = {"operation": "CreateBucket", "error": code}
+ else:
+ raise AssertionError(f"{label} credentials exceeded the policy boundary")
+ return {"allowed": allowed, "denied": denied}
+
+ def case_site_iam_user_policy(self, case_id: str) -> list[str]:
+ user = self.identity_name(case_id, "user")
+ policy = self.identity_name(case_id, "policy")
+ secret = f"Rv-{secrets.token_hex(16)}"
+ self.secrets.add(secret)
+ policy_path = self.policy_file(case_id, "user-policy.json", ["s3:ListAllMyBuckets"])
+ self.run_rc(
+ case_id,
+ "iam-policy-create",
+ ["admin", "policy", "create", self.alias_a, policy, str(policy_path)],
+ )
+ self.run_rc(
+ case_id,
+ "iam-user-add",
+ ["admin", "user", "add", self.alias_a, user, secret],
+ )
+ self.run_rc(
+ case_id,
+ "iam-user-policy-attach",
+ ["admin", "policy", "attach", self.alias_a, policy, "--user", user],
+ )
+ self.wait_rc_success(
+ case_id, "remote-user-info", ["admin", "user", "info", self.alias_b, user]
+ )
+ self.wait_rc_success(
+ case_id, "remote-user-policy", ["admin", "policy", "info", self.alias_b, policy]
+ )
+ auth = self.assert_iam_credentials(case_id, user, secret, "user")
+ return [self.save_json(case_id, "iam-user-authorization.json", auth)]
+
+ def case_site_iam_group_policy(self, case_id: str) -> list[str]:
+ user = self.identity_name(case_id, "group-user")
+ group = self.identity_name(case_id, "group")
+ policy = self.identity_name(case_id, "group-policy")
+ secret = f"Rv-{secrets.token_hex(16)}"
+ self.secrets.add(secret)
+ policy_path = self.policy_file(case_id, "group-policy.json", ["s3:ListAllMyBuckets"])
+ self.run_rc(
+ case_id,
+ "group-policy-create",
+ ["admin", "policy", "create", self.alias_a, policy, str(policy_path)],
+ )
+ self.run_rc(
+ case_id,
+ "group-user-add",
+ ["admin", "user", "add", self.alias_a, user, secret],
+ )
+ self.run_rc(
+ case_id,
+ "group-add",
+ ["admin", "group", "add", self.alias_a, group, "--members", user],
+ )
+ self.run_rc(
+ case_id,
+ "group-policy-attach",
+ ["admin", "policy", "attach", self.alias_a, policy, "--group", group],
+ )
+ self.wait_rc_success(
+ case_id, "remote-group-info", ["admin", "group", "info", self.alias_b, group]
+ )
+ self.wait_rc_success(
+ case_id, "remote-group-policy", ["admin", "policy", "info", self.alias_b, policy]
+ )
+ auth = self.assert_iam_credentials(case_id, user, secret, "group-user")
+ return [self.save_json(case_id, "iam-group-authorization.json", auth)]
+
+ def case_site_service_account(self, case_id: str) -> list[str]:
+ access_key = self.identity_name(case_id, "svc")
+ secret = f"Svc-{secrets.token_hex(18)}"
+ self.secrets.add(secret)
+ policy_path = self.policy_file(case_id, "service-account-policy.json", ["s3:ListAllMyBuckets"])
+ self.run_rc(
+ case_id,
+ "service-account-create",
+ [
+ "admin",
+ "service-account",
+ "create",
+ self.alias_a,
+ access_key,
+ secret,
+ "--name",
+ "release-validation",
+ "--policy",
+ str(policy_path),
+ ],
+ )
+ self.wait_rc_success(
+ case_id,
+ "remote-service-account-info",
+ ["admin", "service-account", "info", self.alias_b, access_key],
+ )
+ auth = self.assert_iam_credentials(case_id, access_key, secret, "service-account")
+ return [self.save_json(case_id, "service-account-authorization.json", auth)]
+
+ def case_site_peer_edit(self, case_id: str) -> list[str]:
+ info_a = self.site_info(self.site_a)
+ peer = dict(self.site_peer(info_a, self.site_b.endpoint))
+ deployment_id = str(peer.get("deploymentID", ""))
+ if not deployment_id:
+ raise AssertionError("remote peer has no deployment ID")
+ new_name = self.identity_name(case_id, "renamed-site")
+ peer["name"] = new_name
+ status, headers, body = self.admin_request(
+ self.site_a,
+ "PUT",
+ "site-replication/edit",
+ payload=peer,
+ )
+ if status != 200:
+ raise AssertionError(f"site peer edit returned HTTP {status}: {body!r}")
+ parsed = self.json_body(body)
+ if parsed and parsed.get("success") is False:
+ raise AssertionError(f"site peer edit was rejected: {parsed}")
+
+ def probe() -> tuple[bool, Any]:
+ infos = {site.id: self.site_info(site) for site in (self.site_a, self.site_b)}
+ observed: dict[str, Any] = {}
+ complete = True
+ for site_id, info in infos.items():
+ matches = [
+ item
+ for item in info.get("sites", [])
+ if str(item.get("deploymentID", "")) == deployment_id
+ ]
+ observed[site_id] = matches
+ complete = complete and len(matches) == 1 and matches[0].get("name") == new_name
+ if matches and str(matches[0].get("endpoint", "")).rstrip("/") != self.site_b.endpoint:
+ raise AssertionError("peer rename changed its endpoint")
+ return complete, observed
+
+ final = self.wait_until(case_id, "peer rename convergence", probe)
+ return [
+ self.save_json(
+ case_id,
+ "peer-edit.json",
+ {"httpStatus": status, "headers": headers, "response": parsed, "final": final},
+ )
+ ]
+
+ def case_site_resync(self, case_id: str) -> list[str]:
+ info = self.site_info(self.site_a)
+ peer = self.site_peer(info, self.site_b.endpoint)
+ responses: dict[str, Any] = {}
+ status, _, body = self.admin_request(
+ self.site_a,
+ "PUT",
+ "site-replication/resync/op",
+ query="operation=start",
+ payload=peer,
+ )
+ if status != 200:
+ raise AssertionError(f"site resync start returned HTTP {status}: {body!r}")
+ responses["start"] = self.json_body(body)
+ status, _, body = self.admin_request(
+ self.site_a,
+ "PUT",
+ "site-replication/resync/op",
+ query="operation=status",
+ payload=peer,
+ )
+ if status != 200:
+ raise AssertionError(f"site resync status returned HTTP {status}: {body!r}")
+ responses["status"] = self.json_body(body)
+ resync_id = responses["status"].get("id") or responses["start"].get("id")
+ if not resync_id:
+ raise AssertionError(f"site resync did not return a durable ID: {responses}")
+ state = str(responses["status"].get("state", responses["status"].get("status", ""))).lower()
+ if state not in {"completed", "complete", "success", "failed", "cancelled", "canceled"}:
+ status, _, body = self.admin_request(
+ self.site_a,
+ "PUT",
+ "site-replication/resync/op",
+ query="operation=cancel",
+ payload=peer,
+ )
+ if status != 200:
+ raise AssertionError(f"site resync cancel returned HTTP {status}: {body!r}")
+ responses["cancel"] = self.json_body(body)
+ else:
+ responses["cancel"] = {"notRequired": True, "terminalState": state}
+ return [self.save_json(case_id, "site-resync.json", responses)]
+
+ def case_site_outage_recovery(self, case_id: str) -> list[str]:
+ bucket = self.bucket_name(case_id, "outage")
+ key = "outage/site-retry.txt"
+ body = b"site-replication-outage"
+ evidence = [self.hook(case_id, "site-b-stop", self.args.site_b_stop_hook)]
+ b_started = False
+ try:
+ self.create_bucket(self.client_a, bucket)
+ self.client_a.put_object(Bucket=bucket, Key=key, Body=body)
+
+ def retry_probe() -> tuple[bool, Any]:
+ info = self.site_info(self.site_a)
+ stats = info.get("retryStats")
+ pending = int((stats or {}).get("pending", 0) or 0)
+ failed = int((stats or {}).get("failed", 0) or 0)
+ return bool(stats) and (pending > 0 or failed > 0 or bool(stats.get("lastError"))), info
+
+ retry = self.wait_until(
+ case_id, "source retry observability while peer is offline", retry_probe, timeout=60
+ )
+ evidence.append(self.save_json(case_id, "retry-visible.json", retry))
+ evidence.append(self.hook(case_id, "site-b-start", self.args.site_b_start_hook))
+ b_started = True
+ self.wait_api(case_id, self.client_b, "site-b")
+ finally:
+ if not b_started:
+ self.hook(case_id, "site-b-start-recovery", self.args.site_b_start_hook)
+ self.wait_api(case_id, self.client_b, "site-b")
+ self.wait_bucket(case_id, self.client_b, bucket)
+ self.created_buckets.append((self.client_b, bucket))
+ final = self.wait_object(
+ case_id, self.client_b, bucket, key, body, timeout=max(self.args.timeout, 240)
+ )
+ evidence.append(self.save_json(case_id, "outage-final.json", final))
+
+ def retry_cleared_probe() -> tuple[bool, Any]:
+ info = self.site_info(self.site_a)
+ stats = info.get("retryStats")
+ if not stats:
+ return True, info
+ pending = int(stats.get("pending", 0) or 0)
+ failed = int(stats.get("failed", 0) or 0)
+ return pending == 0 and failed == 0 and not stats.get("lastError"), info
+
+ cleared = self.wait_until(
+ case_id,
+ "source retry queue to clear after peer recovery",
+ retry_cleared_probe,
+ timeout=max(self.args.timeout, 240),
+ )
+ evidence.append(self.save_json(case_id, "retry-cleared.json", cleared))
+ return evidence
+
+ def site_smoke(self, case_id: str, prefix: str) -> dict[str, Any]:
+ bucket = self.create_site_bucket(case_id, prefix)
+ fixtures = [
+ (self.client_a, self.client_b, "from-a.txt", f"{prefix}-a".encode()),
+ (self.client_b, self.client_a, "from-b.txt", f"{prefix}-b".encode()),
+ ]
+ final = []
+ for source, target, key, body in fixtures:
+ response = source.put_object(Bucket=bucket, Key=key, Body=body)
+ final.append(
+ self.wait_object(
+ case_id,
+ target,
+ bucket,
+ key,
+ body,
+ expected_version=response.get("VersionId"),
+ )
+ )
+ return {"bucket": bucket, "objects": final}
+
+ def case_site_restart_persistence(self, case_id: str) -> list[str]:
+ evidence = [
+ self.hook(case_id, "site-a-restart", self.args.site_a_restart_hook),
+ self.hook(case_id, "site-b-restart", self.args.site_b_restart_hook),
+ ]
+ self.wait_api(case_id, self.client_a, "site-a")
+ self.wait_api(case_id, self.client_b, "site-b")
+ info = self.wait_site_group_active(case_id)
+ smoke = self.site_smoke(case_id, "restart")
+ evidence.extend(
+ (
+ self.save_json(case_id, "restart-info.json", info),
+ self.save_json(case_id, "restart-smoke.json", smoke),
+ )
+ )
+ return evidence
+
+ def case_site_service_account_rotation(self, case_id: str) -> list[str]:
+ evidence: list[str] = []
+ if (
+ self.args.selection_mode == "filtered"
+ and self.site_group_tracker_state == "tainted"
+ ):
+ raise PreconditionError(
+ "service-account rotation refuses a site group tainted by an "
+ "earlier selected case"
+ )
+ try:
+ state, reason, current = self.site_group_snapshot()
+ except Exception as exc:
+ raise PreconditionError(
+ "service-account rotation could not classify the initial site group: "
+ f"{type(exc).__name__}: {exc}"
+ ) from exc
+ evidence.append(
+ self.save_json(
+ case_id,
+ "rotation-initial-site-info.json",
+ {"state": state, "reason": reason, "sites": current},
+ )
+ )
+ if state == "disabled":
+ if (
+ self.args.selection_mode == "filtered"
+ and self.site_group_tracker_state not in {"fresh", "removed"}
+ ):
+ raise PreconditionError(
+ "service-account rotation refuses to recreate a site group "
+ "lost after an earlier selected case"
+ )
+ _, setup_evidence = self.add_site_replication_group(
+ case_id,
+ "rotation-setup-add-response.json",
+ require_clean_initial_sync=True,
+ )
+ evidence.append(setup_evidence)
+ setup_final = self.wait_site_group_active(case_id)
+ evidence.append(
+ self.save_json(case_id, "rotation-setup-final.json", setup_final)
+ )
+ elif state != "active":
+ raise PreconditionError(
+ "service-account rotation refuses initial "
+ f"{state} control-plane state: {reason}"
+ )
+ status, headers, body = self.admin_request(
+ self.site_a, "POST", "site-replication/rotate-svc-acct"
+ )
+ result = self.required_json_object(body, "service account rotation")
+ response_evidence = self.save_json(
+ case_id,
+ "rotation-response.json",
+ {"httpStatus": status, "headers": headers, "result": result},
+ )
+ if status != 200:
+ raise AssertionError(f"service account rotation returned HTTP {status}: {body!r}")
+ if result.get("success") is not True:
+ raise AssertionError(f"service account rotation was incomplete: {result}")
+
+ settled = self.wait_site_group_active(case_id)
+ smoke = self.site_smoke(case_id, "rotation")
+ evidence.extend(
+ [
+ response_evidence,
+ self.save_json(case_id, "rotation-settled.json", settled),
+ self.save_json(case_id, "rotation-smoke.json", smoke),
+ ]
+ )
+ return evidence
+
+ def case_site_remove_all(self, case_id: str) -> list[str]:
+ status, headers, body = self.admin_request(
+ self.site_a,
+ "PUT",
+ "site-replication/remove",
+ payload={"all": True},
+ )
+ response_text = body.decode("utf-8", errors="replace")
+ response_evidence = self.save_json(
+ case_id,
+ "remove-response.json",
+ {"httpStatus": status, "headers": headers, "body": response_text},
+ )
+ if status != 200:
+ raise AssertionError(f"site remove-all returned HTTP {status}: {body!r}")
+ result = self.required_json_object(body, "site remove-all")
+ if result.get("errorDetail") or str(result.get("status", "")).lower() == "partial":
+ raise AssertionError(f"site remove-all was partial: {result}")
+ info = self.wait_site_group_disabled(case_id)
+ bucket = self.bucket_name(case_id, "removed")
+ self.create_bucket(self.client_a, bucket)
+ self.client_a.put_object(Bucket=bucket, Key="control.txt", Body=b"must-not-replicate")
+ deadline = time.monotonic() + max(self.args.negative_window, 15)
+ observations = 0
+ while time.monotonic() < deadline:
+ observations += 1
+ try:
+ self.client_b.get_bucket_location(Bucket=bucket)
+ except ClientError as exc:
+ if exc.response.get("Error", {}).get("Code") not in {
+ "NoSuchBucket",
+ "404",
+ "NotFound",
+ }:
+ raise
+ else:
+ raise AssertionError("bucket propagated after site replication was removed")
+ time.sleep(self.args.poll_interval)
+ return [
+ response_evidence,
+ self.save_json(
+ case_id,
+ "remove-all.json",
+ {
+ "httpStatus": status,
+ "headers": headers,
+ "response": result,
+ "info": info,
+ "negativeObservations": observations,
+ },
+ )
+ ]
+
+ # ------------------------------------------------------------------
+ # Execution and result contracts
+ # ------------------------------------------------------------------
+
+ def classify_exception(self, exc: BaseException) -> str:
+ if isinstance(exc, AssertionError):
+ return "fail"
+ if isinstance(exc, INFRA_EXCEPTIONS):
+ return "infra_error"
+ if isinstance(exc, ClientError):
+ code = exc.response.get("Error", {}).get("Code", "")
+ return "infra_error" if code in INFRA_ERROR_CODES else "fail"
+ return "infra_error"
+
+ def update_site_group_tracker(self, case_id: str, result: CaseResult) -> None:
+ if not case_id.startswith("site-replication."):
+ return
+ if case_id in {
+ "site-replication.unconfigured-info",
+ "site-replication.invalid-credentials",
+ }:
+ self.site_group_tracker_state = (
+ "fresh" if result.state == "pass" else "tainted"
+ )
+ return
+ if case_id == "site-replication.add-and-info":
+ self.site_group_tracker_state = (
+ "active" if result.state == "pass" else "tainted"
+ )
+ return
+ if case_id == "site-replication.remove-all":
+ self.site_group_tracker_state = (
+ "removed" if result.state == "pass" else "tainted"
+ )
+ return
+ if case_id == "site-replication.service-account-rotation":
+ self.site_group_tracker_state = (
+ "active" if result.state == "pass" else "tainted"
+ )
+ return
+ if result.message.startswith("precondition_error:"):
+ self.site_group_tracker_state = "tainted"
+ return
+ if result.state == "pass":
+ self.site_group_tracker_state = "active"
+ return
+ if case_id == "site-replication.lifecycle-expiry-toggle":
+ if self.site_group_tracker_state == "tainted":
+ return
+ try:
+ state, reason, info = self.site_group_snapshot()
+ flags_valid, flags_reason = site_group_has_ilm_expiry_value(
+ info,
+ False,
+ )
+ self.site_group_tracker_state = (
+ "active" if state == "active" and flags_valid else "tainted"
+ )
+ self.events.record(
+ "setup.tracker.probe",
+ case=case_id,
+ state=state,
+ reason=reason,
+ flagReason=flags_reason,
+ )
+ except Exception as exc:
+ self.site_group_tracker_state = "tainted"
+ self.events.record(
+ "setup.tracker.error",
+ case=case_id,
+ message=self.redact(f"{type(exc).__name__}: {exc}"),
+ )
+ return
+ try:
+ state, reason, _ = self.site_group_snapshot()
+ self.site_group_tracker_state = (
+ "active" if state == "active" else "tainted"
+ )
+ self.events.record(
+ "setup.tracker.probe",
+ case=case_id,
+ state=state,
+ reason=reason,
+ )
+ except Exception as exc:
+ self.site_group_tracker_state = "tainted"
+ self.events.record(
+ "setup.tracker.error",
+ case=case_id,
+ message=self.redact(f"{type(exc).__name__}: {exc}"),
+ )
+
+ def record_isolation_blocked(self, case_id: str) -> CaseResult:
+ failure = self.isolation_failure
+ if failure is None:
+ raise RuntimeError("module isolation is not blocked")
+ started_at = utc_now()
+ message = self.redact(
+ "precondition_error: module isolation is blocked by logical cleanup "
+ f"from {failure['sourceCase']}; authoritative reset is required"
+ )
+ blocked_evidence = self.save_json(
+ case_id,
+ "case-isolation-blocked.json",
+ {
+ "state": "infra_error",
+ "message": message,
+ "sourceCase": failure["sourceCase"],
+ "sourceEvidence": failure["evidence"],
+ "trackedBuckets": failure["trackedBuckets"],
+ },
+ )
+ evidence = list(
+ dict.fromkeys([blocked_evidence, *failure["evidence"]])
+ )
+ if (
+ SITE_GROUP_ACTIVE_FIXTURE in self.requested_fixtures
+ and case_id in SITE_GROUP_ACTIVE_CASE_IDS
+ ):
+ self.fixture_outcomes.append(
+ {
+ "caseId": case_id,
+ "fixture": SITE_GROUP_ACTIVE_FIXTURE,
+ "state": "infra_error",
+ "message": message,
+ }
+ )
+ result = CaseResult(
+ id=case_id,
+ state="infra_error",
+ message=message,
+ started_at=started_at,
+ finished_at=utc_now(),
+ duration_seconds=0.0,
+ evidence=evidence,
+ )
+ self.results.append(result)
+ if case_id.startswith("site-replication."):
+ self.site_group_tracker_state = "tainted"
+ self.events.record(
+ "case.isolation.blocked",
+ case=case_id,
+ sourceCase=failure["sourceCase"],
+ evidence=evidence,
+ trackedBuckets=failure["trackedBuckets"],
+ )
+ self.events.record("case.complete", **asdict(result))
+ print(f"[INFRA_ERROR] {case_id}: {message}", flush=True)
+ return result
+
+ def run_case(
+ self,
+ case_id: str,
+ function: Callable[[str], list[str]],
+ ) -> CaseResult:
+ if self.isolation_failure is not None:
+ return self.record_isolation_blocked(case_id)
+ started_at = utc_now()
+ started = time.monotonic()
+ state = "pass"
+ message = "all assertions passed"
+ print(f"[RUN ] {case_id}", flush=True)
+ self.events.record("case.start", case=case_id)
+ try:
+ self.ensure_planned_fixture(case_id)
+ function(case_id)
+ except Exception as exc: # keep the remaining cases independent
+ if isinstance(exc, PreconditionError):
+ state = "infra_error"
+ message = f"precondition_error: {exc}"
+ else:
+ state = self.classify_exception(exc)
+ message = f"{type(exc).__name__}: {exc}"
+ self.events.record(
+ "case.error",
+ case=case_id,
+ state=state,
+ errorType=type(exc).__name__,
+ message=self.redact(message),
+ )
+ self.save_json(
+ case_id,
+ "case-error.json",
+ {
+ "state": state,
+ "errorType": type(exc).__name__,
+ "message": self.redact(message),
+ },
+ )
+ finally:
+ try:
+ # Replication phases intentionally continue after a failed case;
+ # therefore client-visible evidence is retained while logical test
+ # buckets are removed. The driver captures node logs before its
+ # authoritative phase reset.
+ self.cleanup_created_buckets(case_id)
+ except Exception as cleanup_exc:
+ cleanup_message = (
+ f"cleanup failed: {type(cleanup_exc).__name__}: {cleanup_exc}"
+ )
+ if state == "pass":
+ state = "infra_error"
+ message = cleanup_message
+ else:
+ message = f"{message}; {cleanup_message}"
+ self.events.record(
+ "case.cleanup.error", case=case_id, message=self.redact(str(cleanup_exc))
+ )
+ tracked_buckets = self.tracked_bucket_summaries()
+ isolation_restored = (
+ cleanup_exc.isolation_restored
+ if isinstance(cleanup_exc, LogicalCleanupFailure)
+ else not tracked_buckets
+ )
+ cleanup_error_evidence = self.save_json(
+ case_id,
+ "case-cleanup-error.json",
+ {
+ "state": "infra_error",
+ "errorType": type(cleanup_exc).__name__,
+ "message": self.redact(cleanup_message),
+ "isolationRestored": isolation_restored,
+ "trackedBuckets": tracked_buckets,
+ "cleanupEvidence": (
+ cleanup_exc.evidence
+ if isinstance(cleanup_exc, LogicalCleanupFailure)
+ else None
+ ),
+ },
+ )
+ if not isolation_restored:
+ source_evidence = [cleanup_error_evidence]
+ if isinstance(cleanup_exc, LogicalCleanupFailure):
+ source_evidence.insert(0, cleanup_exc.evidence)
+ self.isolation_failure = {
+ "state": "blocked",
+ "sourceCase": case_id,
+ "evidence": list(dict.fromkeys(source_evidence)),
+ "trackedBuckets": tracked_buckets,
+ }
+ self.events.record(
+ "module.isolation.blocked",
+ case=case_id,
+ evidence=self.isolation_failure["evidence"],
+ trackedBuckets=tracked_buckets,
+ )
+
+ directory = self.evidence_path(case_id, "evidence-index").parent
+ evidence = sorted(
+ str(path.relative_to(self.evidence_dir))
+ for path in directory.iterdir()
+ if path.is_file() and path.name != "evidence-index"
+ )
+ result = CaseResult(
+ id=case_id,
+ state=state,
+ message=self.redact(message),
+ started_at=started_at,
+ finished_at=utc_now(),
+ duration_seconds=round(time.monotonic() - started, 3),
+ evidence=evidence,
+ )
+ self.results.append(result)
+ if self.isolation_failure is not None and case_id.startswith(
+ "site-replication."
+ ):
+ self.site_group_tracker_state = "tainted"
+ else:
+ self.update_site_group_tracker(case_id, result)
+ self.events.record("case.complete", **asdict(result))
+ label = "PASS" if state == "pass" else state.upper()
+ print(f"[{label:5}] {case_id}: {result.message}", flush=True)
+ return result
+
+ def case_functions(self) -> dict[str, Callable[[str], list[str]]]:
+ return {
+ "bucket-replication.source-versioning-required": self.case_bucket_source_versioning_required,
+ "bucket-replication.target-versioning-required": self.case_bucket_target_versioning_required,
+ "bucket-replication.configuration-roundtrip": self.case_bucket_configuration_roundtrip,
+ "bucket-replication.put-object": self.case_bucket_put_object,
+ "bucket-replication.sync-put-object": self.case_bucket_sync_put_object,
+ "bucket-replication.prefix-filter": self.case_bucket_prefix_filter,
+ "bucket-replication.tag-filter": self.case_bucket_tag_filter,
+ "bucket-replication.zero-byte-special-key": self.case_bucket_zero_byte_special_key,
+ "bucket-replication.multipart-object": self.case_bucket_multipart_object,
+ "bucket-replication.copy-object": self.case_bucket_copy_object,
+ "bucket-replication.delete-marker": self.case_bucket_delete_marker,
+ "bucket-replication.version-delete": self.case_bucket_version_delete,
+ "bucket-replication.existing-object-enabled": self.case_bucket_existing_object_enabled,
+ "bucket-replication.existing-object-disabled": self.case_bucket_existing_object_disabled,
+ "bucket-replication.target-check": self.case_bucket_target_check,
+ "bucket-replication.status-observability": self.case_bucket_status_observability,
+ "bucket-replication.outage-recovery": self.case_bucket_outage_recovery,
+ "bucket-replication.source-restart-recovery": self.case_bucket_source_restart_recovery,
+ "site-replication.unconfigured-info": self.case_site_unconfigured_info,
+ "site-replication.invalid-credentials": self.case_site_invalid_credentials,
+ "site-replication.add-and-info": self.case_site_add_and_info,
+ "site-replication.status-observability": self.case_site_status_observability,
+ "site-replication.bucket-create": self.case_site_bucket_create,
+ "site-replication.object-bidirectional": self.case_site_object_bidirectional,
+ "site-replication.versioning-metadata": self.case_site_versioning_metadata,
+ "site-replication.lifecycle-expiry-toggle": self.case_site_lifecycle_expiry_toggle,
+ "site-replication.bucket-policy-metadata": self.case_site_bucket_policy_metadata,
+ "site-replication.object-tags": self.case_site_object_tags,
+ "site-replication.delete-convergence": self.case_site_delete_convergence,
+ "site-replication.iam-user-policy": self.case_site_iam_user_policy,
+ "site-replication.iam-group-policy": self.case_site_iam_group_policy,
+ "site-replication.service-account": self.case_site_service_account,
+ "site-replication.peer-edit": self.case_site_peer_edit,
+ "site-replication.resync": self.case_site_resync,
+ "site-replication.outage-recovery": self.case_site_outage_recovery,
+ "site-replication.restart-persistence": self.case_site_restart_persistence,
+ "site-replication.remove-all": self.case_site_remove_all,
+ "site-replication.service-account-rotation": self.case_site_service_account_rotation,
+ }
+
+ def write_results(self, started_at: str, started: float) -> str:
+ ordered = BUCKET_CASE_IDS if self.args.module == "bucket" else SITE_CASE_IDS
+ selected_cases = tuple(
+ case_id for case_id in ordered if case_id in set(self.args.case or ordered)
+ )
+ case_order = {case_id: index for index, case_id in enumerate(ordered)}
+ self.results.sort(key=lambda item: case_order.get(item.id, len(case_order)))
+ states = {result.state for result in self.results}
+ if "fail" in states:
+ overall = "fail"
+ elif "infra_error" in states:
+ overall = "infra_error"
+ else:
+ overall = "pass"
+ finished_at = utc_now()
+ document = {
+ "apiVersion": "validation.rustfs.io/v1alpha1",
+ "kind": "TestRunResult",
+ "metadata": {
+ "runId": self.args.run_id,
+ "environmentId": self.args.environment_id,
+ },
+ "status": {
+ "state": overall,
+ "startedAt": started_at,
+ "finishedAt": finished_at,
+ "cases": [
+ {
+ "id": item.id,
+ "state": item.state,
+ "message": item.message,
+ "evidence": item.evidence,
+ }
+ for item in self.results
+ ],
+ "evidence": ["events.jsonl", "run-manifest.json", "junit.xml"],
+ },
+ }
+ (self.evidence_dir / "result.json").write_text(
+ json.dumps(document, indent=2, sort_keys=True) + "\n", encoding="utf-8"
+ )
+ manifest = {
+ "runId": self.args.run_id,
+ "environmentId": self.args.environment_id,
+ "module": self.args.module,
+ "endpointA": self.site_a.endpoint,
+ "endpointB": self.site_b.endpoint,
+ "startedAt": started_at,
+ "finishedAt": finished_at,
+ "durationSeconds": round(time.monotonic() - started, 3),
+ "pollIntervalSeconds": self.args.poll_interval,
+ "negativeWindowSeconds": self.args.negative_window,
+ "boto3Version": boto3.__version__,
+ "botocoreVersion": botocore.__version__,
+ "rcBinaryPath": self.rc_bin,
+ "rcVersion": self.rc_version,
+ "rcBinarySha256": self.rc_binary_sha256,
+ "expectedRcBinarySha256": self.expected_rc_binary_sha256 or None,
+ "executionPlan": {
+ "selectionMode": self.args.selection_mode,
+ "module": self.args.module,
+ "scoredCases": list(selected_cases),
+ "scoredCaseCount": len(selected_cases),
+ "fixtures": sorted(self.requested_fixtures),
+ },
+ "fixtureOutcomes": self.fixture_outcomes,
+ "siteGroupTrackerState": self.site_group_tracker_state,
+ "isolationOutcome": self.isolation_failure
+ or {
+ "state": "pass",
+ "message": "all tracked logical buckets were removed",
+ "trackedBuckets": [],
+ },
+ }
+ (self.evidence_dir / "run-manifest.json").write_text(
+ json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8"
+ )
+ self.write_junit(overall, time.monotonic() - started)
+ return overall
+
+ def write_junit(self, overall: str, duration: float) -> None:
+ suite = ET.Element(
+ "testsuite",
+ {
+ "name": f"rustfs-fixed-environment-{self.args.module}-replication",
+ "tests": str(len(self.results)),
+ "failures": str(sum(item.state == "fail" for item in self.results)),
+ "errors": str(sum(item.state == "infra_error" for item in self.results)),
+ "skipped": "0",
+ "time": f"{duration:.3f}",
+ },
+ )
+ for item in self.results:
+ case = ET.SubElement(
+ suite,
+ "testcase",
+ {
+ "classname": f"rustfs.{self.args.module}_replication",
+ "name": item.id,
+ "time": f"{item.duration_seconds:.3f}",
+ },
+ )
+ if item.state == "fail":
+ ET.SubElement(case, "failure", {"message": item.message}).text = item.message
+ elif item.state == "infra_error":
+ ET.SubElement(case, "error", {"message": item.message}).text = item.message
+ ET.SubElement(case, "system-out").text = "\n".join(item.evidence)
+ tree = ET.ElementTree(suite)
+ ET.indent(tree, space=" ")
+ tree.write(self.evidence_dir / "junit.xml", encoding="utf-8", xml_declaration=True)
+ self.events.record("run.complete", state=overall)
+
+ def run(self) -> int:
+ started_at = utc_now()
+ started = time.monotonic()
+ self.events.record(
+ "run.start",
+ runId=self.args.run_id,
+ environmentId=self.args.environment_id,
+ module=self.args.module,
+ selectionMode=self.args.selection_mode,
+ scoredCases=list(self.args.case or (
+ BUCKET_CASE_IDS if self.args.module == "bucket" else SITE_CASE_IDS
+ )),
+ fixtures=sorted(self.requested_fixtures),
+ )
+ try:
+ self.wait_api("preflight.connectivity", self.client_a, "site-a")
+ self.wait_api("preflight.connectivity", self.client_b, "site-b")
+ self.configure_rc()
+ except Exception as exc:
+ self.results.append(
+ CaseResult(
+ id="preflight.connectivity",
+ state="infra_error",
+ message=self.redact(f"{type(exc).__name__}: {exc}"),
+ started_at=started_at,
+ finished_at=utc_now(),
+ duration_seconds=round(time.monotonic() - started, 3),
+ )
+ )
+ overall = self.write_results(started_at, started)
+ return 2 if overall == "infra_error" else 1
+
+ functions = self.case_functions()
+ ordered = BUCKET_CASE_IDS if self.args.module == "bucket" else SITE_CASE_IDS
+ selected = set(self.args.case or ordered)
+ for case_id in ordered:
+ if case_id in selected:
+ self.run_case(case_id, functions[case_id])
+ overall = self.write_results(started_at, started)
+ print(
+ f"{self.args.module} replication validation {overall}: "
+ f"result={self.evidence_dir / 'result.json'}",
+ flush=True,
+ )
+ return {"pass": 0, "fail": 1, "infra_error": 2}[overall]
+
+
+def parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--module", choices=("bucket", "site"), required=True)
+ parser.add_argument("--endpoint-a", default=os.environ.get("RUSTFS_REPLICATION_ENDPOINT_A"))
+ parser.add_argument("--endpoint-b", default=os.environ.get("RUSTFS_REPLICATION_ENDPOINT_B"))
+ parser.add_argument("--access-key", default=os.environ.get("RUSTFS_ACCESS_KEY"))
+ parser.add_argument("--secret-key", default=os.environ.get("RUSTFS_SECRET_KEY"))
+ parser.add_argument("--region", default=os.environ.get("RUSTFS_REGION", "us-east-1"))
+ parser.add_argument("--environment-id", default="replication-2site")
+ parser.add_argument("--run-id", required=True)
+ parser.add_argument("--selection-mode", choices=("full", "filtered"))
+ parser.add_argument("--evidence-dir", required=True, type=Path)
+ parser.add_argument("--timeout", type=float, default=180)
+ parser.add_argument("--poll-interval", type=float, default=2)
+ parser.add_argument("--negative-window", type=float, default=12)
+ parser.add_argument("--rc-bin", default="rc")
+ parser.add_argument("--site-a-stop-hook")
+ parser.add_argument("--site-a-start-hook")
+ parser.add_argument("--site-a-restart-hook")
+ parser.add_argument("--site-b-stop-hook")
+ parser.add_argument("--site-b-start-hook")
+ parser.add_argument("--site-b-restart-hook")
+ parser.add_argument(
+ "--case",
+ action="append",
+ choices=BUCKET_CASE_IDS + SITE_CASE_IDS,
+ help="run only the named case; repeat to select multiple cases",
+ )
+ parser.add_argument(
+ "--fixture",
+ action="append",
+ choices=(SITE_GROUP_ACTIVE_FIXTURE,),
+ help="non-scored setup fixture selected by the case planner",
+ )
+ parser.add_argument("--cleanup-on-failure", action="store_true")
+ args = parser.parse_args()
+ selection_mode_was_explicit = args.selection_mode is not None
+ if not args.endpoint_a or not args.endpoint_b or not args.access_key or not args.secret_key:
+ parser.error("both endpoints and the administrator access/secret keys are required")
+ endpoint_identities: list[tuple[str, int]] = []
+ for endpoint in (args.endpoint_a, args.endpoint_b):
+ identity = _site_endpoint_identity(endpoint)
+ if identity is None:
+ parser.error("replication endpoints must be absolute HTTP(S) URLs")
+ endpoint_identities.append(identity)
+ if len(set(endpoint_identities)) != 2:
+ parser.error("replication endpoints must be distinct")
+ if args.timeout <= 0 or args.poll_interval <= 0 or args.negative_window <= 0:
+ parser.error("timing values must be positive")
+ selected = set(args.case or ())
+ allowed = set(BUCKET_CASE_IDS if args.module == "bucket" else SITE_CASE_IDS)
+ if selected - allowed:
+ parser.error(f"selected case does not belong to module {args.module}")
+ if args.selection_mode is None:
+ args.selection_mode = "filtered" if args.case else "full"
+ fixtures = set(args.fixture or ())
+ if args.selection_mode == "full":
+ if selected and selected != allowed:
+ parser.error("full selection mode requires the complete module case set")
+ if fixtures:
+ parser.error("full selection mode must not enable diagnostic fixtures")
+ else:
+ if not selected:
+ parser.error("filtered selection mode requires at least one case")
+ expected_fixtures = (
+ {SITE_GROUP_ACTIVE_FIXTURE}
+ if args.module == "site"
+ and SITE_GROUP_ACTIVE_CASE_IDS.intersection(selected)
+ else set()
+ )
+ if not selection_mode_was_explicit and args.fixture is None:
+ args.fixture = sorted(expected_fixtures)
+ fixtures = set(args.fixture)
+ if fixtures != expected_fixtures:
+ parser.error(
+ "filtered fixture set does not match the selected site-case "
+ f"requirements: expected={sorted(expected_fixtures)} "
+ f"actual={sorted(fixtures)}"
+ )
+ no_proxy = os.environ.get("NO_PROXY", os.environ.get("no_proxy", ""))
+ hosts = [identity[0] for identity in endpoint_identities]
+ os.environ["NO_PROXY"] = ",".join(filter(None, [no_proxy, *hosts, "127.0.0.1", "localhost"]))
+ os.environ["no_proxy"] = os.environ["NO_PROXY"]
+ return args
+
+
+def main() -> int:
+ if hasattr(os.sys.stdout, "reconfigure"):
+ os.sys.stdout.reconfigure(line_buffering=True)
+ args = parse_args()
+ harness = ReplicationHarness(args)
+ try:
+ return harness.run()
+ finally:
+ harness.close()
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/scripts/validation_replication/replication_metrics.py b/scripts/validation_replication/replication_metrics.py
new file mode 100644
index 00000000..4d3eceeb
--- /dev/null
+++ b/scripts/validation_replication/replication_metrics.py
@@ -0,0 +1,91 @@
+"""Strict helpers for consuming the stable rc replication output contract."""
+
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any
+
+
+U64_MAX = 2**64 - 1
+
+
+class ReplicationMetricsContractError(ValueError):
+ """The rc output is incompatible with the expected output-v3 contract."""
+
+
+def _mapping(value: Any, path: str) -> Mapping[str, Any]:
+ if not isinstance(value, Mapping):
+ raise ReplicationMetricsContractError(f"{path} must be a JSON object")
+ return value
+
+
+def _root(document: Any) -> Mapping[str, Any]:
+ root = _mapping(document, "document")
+ schema_version = root.get("schema_version")
+ if type(schema_version) is not int or schema_version != 3:
+ raise ReplicationMetricsContractError(
+ "document.schema_version must be the JSON integer 3"
+ )
+ if root.get("type") != "replication":
+ raise ReplicationMetricsContractError(
+ 'document.type must be the JSON string "replication"'
+ )
+ return root
+
+
+def _counter(container: Mapping[str, Any], name: str, path: str) -> int:
+ if name not in container:
+ return 0
+ value = container[name]
+ if type(value) is not int or not 0 <= value <= U64_MAX:
+ raise ReplicationMetricsContractError(
+ f"{path}.{name} must be an unsigned 64-bit JSON integer"
+ )
+ return value
+
+
+def _data(document: Any) -> Mapping[str, Any]:
+ root = _root(document)
+ if "data" not in root:
+ raise ReplicationMetricsContractError("document.data is required")
+ return _mapping(root["data"], "document.data")
+
+
+def replicated_object_count(document: Any) -> int:
+ """Return the greatest aggregate or per-target replicated object count."""
+
+ data = _data(document)
+ totals_value = data.get("totals", {})
+ totals = _mapping(totals_value, "document.data.totals")
+ observed = _counter(totals, "replicated_count", "document.data.totals")
+
+ targets_value = data.get("targets", [])
+ if not isinstance(targets_value, list):
+ raise ReplicationMetricsContractError("document.data.targets must be a JSON array")
+ for index, value in enumerate(targets_value):
+ target = _mapping(value, f"document.data.targets[{index}]")
+ observed = max(
+ observed,
+ _counter(
+ target,
+ "replicated_count",
+ f"document.data.targets[{index}]",
+ ),
+ )
+ return observed
+
+
+def replication_metrics_ready(document: Any) -> bool:
+ """Return whether a successful output-v3 document proves replication progress."""
+
+ root = _root(document)
+ if root.get("status") != "success":
+ return False
+ observed = replicated_object_count(root)
+ data = _data(root)
+ targets = data.get("targets", [])
+ return (
+ data.get("availability") == "available"
+ and bool(targets)
+ and observed >= 1
+ )
diff --git a/scripts/validation_replication/run_case.sh b/scripts/validation_replication/run_case.sh
new file mode 100644
index 00000000..371b1537
--- /dev/null
+++ b/scripts/validation_replication/run_case.sh
@@ -0,0 +1,136 @@
+#!/usr/bin/env bash
+set -Eeuo pipefail
+
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+source "${ROOT}/rustfs-replication-test.sh"
+endpoint_host_prefix() { printf 'rustfs-node'; }
+SITE2_ENDPOINT="http://rustfs-node1:19000"
+
+module_for_case() {
+ case "$1" in
+ bucket-replication.*) printf bucket ;;
+ site-replication.*) printf site ;;
+ *) return 2 ;;
+ esac
+}
+
+site_action() {
+ local site="$1" action="$2" node pid fail=0
+ case "${site}" in
+ a)
+ local pids=()
+ for node in "${NODES[@]}"; do
+ service_action "${action}" "${node}" & pids+=("$!")
+ done
+ for pid in "${pids[@]}"; do wait "${pid}" || fail=1; done
+ [ "${fail}" -eq 0 ] || die "site A ${action} failed"
+ if [ "${action}" != stop ]; then
+ for node in "${NODES[@]}"; do wait_service_active "${node}" "${RUSTFS_SERVICE}"; done
+ wait_http_live "${API_ENDPOINT}"
+ fi ;;
+ b)
+ service_action "${action}" "${NODES[0]}" "${SITE2_SERVICE}"
+ if [ "${action}" != stop ]; then
+ wait_service_active "${NODES[0]}" "${SITE2_SERVICE}"
+ wait_http_live "${SITE2_ENDPOINT}"
+ fi ;;
+ *) die "unknown replication site: ${site}" ;;
+ esac
+}
+
+hook_json() {
+ python3 -c 'import json,sys; print(json.dumps(sys.argv[1:]))' \
+ bash "${ROOT}/scripts/validation_replication/run_case.sh" hook "$1" "$2"
+}
+
+setup_environment() {
+ env_setup_main_cluster
+ env_setup_site2
+}
+
+run_validation() {
+ local case_id="" candidate="" testing_commit="" artifacts="" setup_rc=0 harness_rc=0 teardown_rc=0
+ while [ "$#" -gt 0 ]; do
+ case "$1" in
+ --validation-case) case_id="$2"; shift 2 ;;
+ --candidate-manifest) candidate="$2"; shift 2 ;;
+ --testing-commit) testing_commit="$2"; shift 2 ;;
+ --validation-artifacts-dir) artifacts="$2"; shift 2 ;;
+ --dry-run) DRY_RUN=1; shift ;;
+ -y|--yes) shift ;;
+ *) die "unknown replication validation option: $1" ;;
+ esac
+ done
+ local module
+ module="$(module_for_case "${case_id}")" || die "replication case required"
+ python3 -B -c 'import sys; sys.path.insert(0,sys.argv[1]); from scripts.validation_replication.case_plan import BUCKET_CASE_IDS,SITE_CASE_IDS; valid=BUCKET_CASE_IDS if sys.argv[2]=="bucket" else SITE_CASE_IDS; sys.exit(0 if sys.argv[3] in valid else 2)' \
+ "${ROOT}" "${module}" "${case_id}" || die "unknown canonical replication case: ${case_id}"
+ if [ "${DRY_RUN}" -eq 1 ]; then
+ setup_environment
+ return 0
+ fi
+ [ -f "${candidate}" ] || die "candidate manifest missing"
+ [ -n "${artifacts}" ] && [ ! -e "${artifacts}" ] || die "fresh artifact directory required"
+ [ "${testing_commit}" = "$(git -C "${ROOT}" rev-parse HEAD)" ] || die "testing commit differs from checkout"
+ PACKAGE_URL="$(jq -er '.package_url' "${candidate}")"
+ PACKAGE_SHA256="$(jq -er '.package_sha256' "${candidate}")"
+ [[ "${PACKAGE_SHA256}" =~ ^[0-9a-f]{64}$ ]] || die "invalid candidate SHA-256"
+ mkdir -p "${artifacts}/cases" "${artifacts}/evidence/${case_id}/replication"
+ local evidence="${artifacts}/evidence/${case_id}" harness="${artifacts}/evidence/${case_id}/replication"
+ python3 "${ROOT}/scripts/validation_result.py" --case-id "${case_id}" \
+ --profile replication-2site --result-file "${artifacts}/cases/${case_id}.json" \
+ --verdict RUNNING --cleanup-status PENDING
+ (setup_environment) > "${evidence}/setup.log" 2>&1 || setup_rc=$?
+ if [ "${setup_rc}" -ne 0 ]; then
+ cat "${evidence}/setup.log"
+ (env_teardown) > "${evidence}/teardown.log" 2>&1 || true
+ return 1
+ fi
+ local binary_sha
+ binary_sha="$(ssh "${SSH_OPTS[@]}" "${SSH_USER}@${NODES[0]}" sha256sum /usr/bin/rustfs | awk '{print $1}')"
+ [[ "${binary_sha}" =~ ^[0-9a-f]{64}$ ]] || die "installed RustFS binary SHA-256 unavailable"
+ python3 -B "${ROOT}/scripts/validation_replication/replication_behavior.py" \
+ --module "${module}" --case "${case_id}" --cleanup-on-failure \
+ --endpoint-a "${API_ENDPOINT}" --endpoint-b "${SITE2_ENDPOINT}" \
+ --access-key "${ACCESS_KEY}" --secret-key "${SECRET_KEY}" \
+ --run-id "${case_id}-$(date +%s)-$$" --evidence-dir "${harness}" \
+ --rc-bin "$(command -v rc)" \
+ --site-a-stop-hook "$(hook_json a stop)" \
+ --site-a-start-hook "$(hook_json a start)" \
+ --site-a-restart-hook "$(hook_json a restart)" \
+ --site-b-stop-hook "$(hook_json b stop)" \
+ --site-b-start-hook "$(hook_json b start)" \
+ --site-b-restart-hook "$(hook_json b restart)" \
+ > "${evidence}/test.log" 2>&1 || harness_rc=$?
+ cat "${evidence}/test.log"
+ (env_teardown) > "${evidence}/teardown.log" 2>&1 || teardown_rc=$?
+ local verdict=FAIL reason="replication behavior assertions failed; inspect evidence" cleanup_status=FAIL
+ if [ "${teardown_rc}" -eq 0 ]; then cleanup_status=PASS; fi
+ if [ "${harness_rc}" -eq 0 ] && [ "${teardown_rc}" -eq 0 ] && \
+ python3 -B "${ROOT}/scripts/validation_replication/verify_result.py" "${case_id}" "${harness}"; then
+ verdict=PASS; reason=""
+ elif jq -e '.status.state == "infra_error"' "${harness}/result.json" >/dev/null 2>&1; then
+ verdict=INFRA_ERROR; reason="replication environment or execution infrastructure failed; inspect evidence"
+ fi
+ local args=("${ROOT}/scripts/validation_result.py" --case-id "${case_id}"
+ --profile replication-2site --result-file "${artifacts}/cases/${case_id}.json"
+ --verdict "${verdict}" --cleanup-status "${cleanup_status}"
+ --candidate-manifest "${candidate}" --rc-bin "$(command -v rc)"
+ --testing-commit "${testing_commit}" --binary-sha256 "${binary_sha}"
+ --evidence "evidence/${case_id}/setup.log" --evidence "evidence/${case_id}/test.log"
+ --evidence "evidence/${case_id}/teardown.log")
+ if [ -n "${reason}" ]; then args+=(--reason "${reason}"); fi
+ if [ "${verdict}" = PASS ]; then
+ local file
+ while IFS= read -r -d '' file; do
+ args+=(--evidence "${file#"${artifacts}/"}")
+ done < <(find "${harness}" -type f -print0 | sort -z)
+ fi
+ python3 "${args[@]}"
+ [ "${verdict}" = PASS ]
+}
+
+case "${1:-}" in
+ hook) shift; site_action "$@" ;;
+ *) run_validation "$@" ;;
+esac
diff --git a/scripts/validation_replication/verify_result.py b/scripts/validation_replication/verify_result.py
new file mode 100644
index 00000000..46568c05
--- /dev/null
+++ b/scripts/validation_replication/verify_result.py
@@ -0,0 +1,48 @@
+#!/usr/bin/env python3
+"""Check one replication case, fixture, and logical isolation outcome."""
+
+from __future__ import annotations
+
+import json
+from pathlib import Path
+import sys
+
+
+def verify(case_id: str, evidence_dir: Path) -> None:
+ result = json.loads((evidence_dir / "result.json").read_text(encoding="utf-8"))
+ manifest = json.loads((evidence_dir / "run-manifest.json").read_text(encoding="utf-8"))
+ cases = result["status"]["cases"]
+ if result["status"]["state"] != "pass" or len(cases) != 1:
+ raise ValueError("replication run did not pass exactly one case")
+ case = cases[0]
+ if case["id"] != case_id or case["state"] != "pass":
+ raise ValueError("replication result does not pass selected case")
+ if manifest["executionPlan"]["scoredCases"] != [case_id]:
+ raise ValueError("replication execution plan differs from selected case")
+ if manifest["isolationOutcome"]["state"] != "pass" or manifest["isolationOutcome"].get("trackedBuckets"):
+ raise ValueError("replication bucket isolation did not pass")
+ if any(outcome["state"] != "pass" for outcome in manifest["fixtureOutcomes"]):
+ raise ValueError("replication fixture did not pass")
+ if not case["evidence"]:
+ raise ValueError("replication case has no assertion evidence")
+ root = evidence_dir.resolve(strict=True)
+ for name in case["evidence"]:
+ path = (root / name).resolve(strict=True)
+ if not path.is_relative_to(root) or not path.is_file():
+ raise ValueError(f"unsafe replication evidence path: {name}")
+
+
+def main() -> int:
+ if len(sys.argv) != 3:
+ print(f"usage: {sys.argv[0]} CASE_ID EVIDENCE_DIR", file=sys.stderr)
+ return 2
+ try:
+ verify(sys.argv[1], Path(sys.argv[2]))
+ except (OSError, ValueError, KeyError, TypeError, json.JSONDecodeError) as exc:
+ print(f"replication verification failed: {exc}", file=sys.stderr)
+ return 1
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/scripts/validation_report.py b/scripts/validation_report.py
new file mode 100644
index 00000000..810417b1
--- /dev/null
+++ b/scripts/validation_report.py
@@ -0,0 +1,196 @@
+#!/usr/bin/env python3
+"""Report migration progress and gate verified validation case results."""
+
+from __future__ import annotations
+
+import argparse
+from collections import Counter
+import json
+from pathlib import Path
+import re
+import sys
+
+try:
+ from .validation_catalog import validate_snapshot
+except ImportError:
+ from validation_catalog import validate_snapshot
+
+
+TERMINAL = {"PASS", "FAIL", "INFRA_ERROR", "BLOCKED", "UNSUPPORTED"}
+ALL_VERDICTS = TERMINAL | {"RUNNING"}
+GATE_SOURCE_STATUSES = {"implemented", "procedure-ready", "planned"}
+SHA256 = re.compile(r"[0-9a-f]{64}\Z")
+COMMIT = re.compile(r"[0-9a-f]{40}\Z")
+
+
+def load_results(directory: Path) -> tuple[dict[str, dict], list[str]]:
+ records: dict[str, dict] = {}
+ errors: list[str] = []
+ if not directory.is_dir():
+ return records, [f"missing results directory: {directory}"]
+ for path in sorted(directory.glob("*.json")):
+ try:
+ result = json.loads(path.read_text(encoding="utf-8"))
+ except (OSError, UnicodeError, json.JSONDecodeError) as exc:
+ errors.append(f"invalid result {path.name}: {exc}")
+ continue
+ if not isinstance(result, dict):
+ errors.append(f"result is not an object: {path.name}")
+ continue
+ case_id = result.get("case_id")
+ if not isinstance(case_id, str) or not case_id:
+ errors.append(f"result has no case_id: {path.name}")
+ elif case_id in records:
+ errors.append(f"duplicate result: {case_id}")
+ else:
+ records[case_id] = result
+ return records, errors
+
+
+def check_result(
+ case: dict, result: dict, *, gate: bool, artifacts_root: Path,
+ target_environments: list[str],
+) -> list[str]:
+ case_id = case["id"]
+ errors: list[str] = []
+ if result.get("schema_version") != 1:
+ errors.append(f"{case_id}: unsupported result schema")
+ verdict = result.get("verdict")
+ if verdict not in ALL_VERDICTS:
+ errors.append(f"{case_id}: invalid verdict {verdict!r}")
+ elif verdict == "RUNNING":
+ errors.append(f"{case_id}: result remained RUNNING")
+ if result.get("source_status") != case["source_status"]:
+ errors.append(f"{case_id}: source status does not match catalog")
+ profile = result.get("profile")
+ if not isinstance(profile, str) or not profile:
+ errors.append(f"{case_id}: missing environment profile")
+ elif target_environments and profile not in target_environments:
+ errors.append(f"{case_id}: profile {profile!r} is outside source environments")
+ if result.get("migration_status") not in {"unverified", "verified"}:
+ errors.append(f"{case_id}: invalid migration status")
+ if verdict == "PASS":
+ if result.get("migration_status") != "verified":
+ errors.append(f"{case_id}: PASS without verified migration")
+ evidence = result.get("assertions")
+ if not isinstance(evidence, list) or len(evidence) != len(case["assertions"]):
+ errors.append(f"{case_id}: PASS lacks one result per source assertion")
+ else:
+ for index, (source_assertion, item) in enumerate(zip(case["assertions"], evidence), 1):
+ if (
+ not isinstance(item, dict)
+ or item.get("source_assertion") != source_assertion
+ or item.get("status") != "PASS"
+ or not isinstance(item.get("evidence"), list)
+ or not item["evidence"]
+ or any(not isinstance(path, str) or not path for path in item["evidence"])
+ ):
+ errors.append(f"{case_id}: assertion {index} lacks matching source text or evidence")
+ elif any(
+ Path(path).is_absolute()
+ or ".." in Path(path).parts
+ or not (artifacts_root / path).is_file()
+ or not (artifacts_root / path).resolve().is_relative_to(artifacts_root.resolve())
+ for path in item["evidence"]
+ ):
+ errors.append(f"{case_id}: assertion {index} has missing or unsafe evidence paths")
+ if result.get("cleanup_status") != "PASS":
+ errors.append(f"{case_id}: PASS requires clean environment")
+ provenance = result.get("provenance")
+ if not isinstance(provenance, dict) or not (
+ isinstance(provenance.get("candidate_sha256"), str)
+ and SHA256.fullmatch(provenance["candidate_sha256"])
+ and isinstance(provenance.get("binary_sha256"), str)
+ and SHA256.fullmatch(provenance["binary_sha256"])
+ and isinstance(provenance.get("candidate_url"), str)
+ and bool(provenance["candidate_url"])
+ and isinstance(provenance.get("source_commit"), str)
+ and COMMIT.fullmatch(provenance["source_commit"])
+ and isinstance(provenance.get("testing_commit"), str)
+ and COMMIT.fullmatch(provenance["testing_commit"])
+ and isinstance(provenance.get("rc_path"), str)
+ and Path(provenance["rc_path"]).is_absolute()
+ and isinstance(provenance.get("rc_sha256"), str)
+ and SHA256.fullmatch(provenance["rc_sha256"])
+ ):
+ errors.append(f"{case_id}: PASS lacks candidate and client provenance")
+ if gate and verdict != "PASS":
+ errors.append(f"{case_id}: gate verdict is {verdict!r}")
+ if verdict in {"BLOCKED", "UNSUPPORTED", "INFRA_ERROR", "FAIL"} and not result.get("reason"):
+ errors.append(f"{case_id}: {verdict} requires a reason")
+ if verdict in {"PASS", "FAIL", "INFRA_ERROR"} and result.get("cleanup_status") not in {"PASS", "FAIL"}:
+ errors.append(f"{case_id}: terminal result requires known cleanup status")
+ return errors
+
+
+def validate_results(
+ catalog: dict, directory: Path, *, gate_statuses: set[str], gate_cases: set[str] | None = None,
+) -> tuple[list[dict], list[str]]:
+ validate_snapshot(catalog)
+ cases = {
+ case["id"]: (case, module["target_environments"])
+ for module in catalog["modules"] for case in module["cases"]
+ }
+ gate_cases = gate_cases or set()
+ unknown = gate_cases - set(cases)
+ if unknown:
+ raise ValueError(f"unknown gate case: {', '.join(sorted(unknown))}")
+ records, errors = load_results(directory)
+ for case_id in sorted(set(records) - set(cases)):
+ errors.append(f"unexpected case ID: {case_id}")
+ rows = []
+ for case_id, (case, target_environments) in cases.items():
+ result = records.get(case_id)
+ gated = case["source_status"] in gate_statuses or case_id in gate_cases
+ if result is None:
+ if gated:
+ errors.append(f"missing gate result: {case_id}")
+ rows.append({"case_id": case_id, "source_status": case["source_status"], "verdict": "MISSING"})
+ continue
+ errors.extend(check_result(
+ case, result, gate=gated, artifacts_root=directory.parent,
+ target_environments=target_environments,
+ ))
+ rows.append({"case_id": case_id, "source_status": case["source_status"], "verdict": result.get("verdict"),
+ "reason": result.get("reason", "")})
+ return rows, errors
+
+
+def render_report(rows: list[dict], errors: list[str]) -> str:
+ counts = Counter(row["verdict"] for row in rows)
+ lines = ["# Validation migration report", "", f"Cases: {len(rows)}", ""]
+ lines.extend(f"- {status}: {counts.get(status, 0)}" for status in ("PASS", "FAIL", "INFRA_ERROR", "BLOCKED", "UNSUPPORTED", "RUNNING", "MISSING"))
+ lines.extend(["", "| Case | Source status | Verdict | Reason |", "| --- | --- | --- | --- |"])
+ for row in rows:
+ reason = str(row.get("reason", "")).replace("|", "\\|").replace("\n", " ").replace("\r", " ")
+ lines.append(f"| {row['case_id']} | {row['source_status']} | {row['verdict']} | {reason} |")
+ if errors:
+ lines.extend(["", "## Validation errors", ""])
+ lines.extend(f"- {error}" for error in errors)
+ return "\n".join(lines) + "\n"
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--catalog", type=Path, default=Path(__file__).resolve().parent.parent / "validation-cases.json")
+ parser.add_argument("--results-dir", type=Path, required=True)
+ parser.add_argument("--output", type=Path, required=True)
+ parser.add_argument("--gate-status", action="append", choices=sorted(GATE_SOURCE_STATUSES), default=[])
+ parser.add_argument("--gate-case", action="append", default=[])
+ args = parser.parse_args()
+ try:
+ catalog = json.loads(args.catalog.read_text(encoding="utf-8"))
+ rows, errors = validate_results(
+ catalog, args.results_dir,
+ gate_statuses=set(args.gate_status), gate_cases=set(args.gate_case),
+ )
+ args.output.parent.mkdir(parents=True, exist_ok=True)
+ args.output.write_text(render_report(rows, errors), encoding="utf-8")
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
+ print(f"validation report error: {exc}", file=sys.stderr)
+ return 1
+ return 1 if errors else 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/scripts/validation_result.py b/scripts/validation_result.py
new file mode 100644
index 00000000..0cdfe291
--- /dev/null
+++ b/scripts/validation_result.py
@@ -0,0 +1,114 @@
+#!/usr/bin/env python3
+"""Atomically write one canonical release-validation case result."""
+
+from __future__ import annotations
+
+import argparse
+import hashlib
+import json
+import os
+from pathlib import Path
+import sys
+import tempfile
+
+from validation_catalog import validate_snapshot
+
+
+def write_result(path: Path, value: dict) -> None:
+ path.parent.mkdir(parents=True, exist_ok=True)
+ with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=path.parent, delete=False) as stream:
+ temporary = Path(stream.name)
+ json.dump(value, stream, ensure_ascii=False, indent=2)
+ stream.write("\n")
+ os.replace(temporary, path)
+
+
+def case_from_catalog(catalog: dict, case_id: str) -> dict:
+ validate_snapshot(catalog)
+ for module in catalog["modules"]:
+ for case in module["cases"]:
+ if case["id"] == case_id:
+ return case
+ raise ValueError(f"unknown validation case: {case_id}")
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--catalog", type=Path, default=Path(__file__).resolve().parent.parent / "validation-cases.json")
+ parser.add_argument("--case-id", required=True)
+ parser.add_argument("--profile", required=True)
+ parser.add_argument("--result-file", type=Path, required=True)
+ parser.add_argument("--verdict", choices=("RUNNING", "PASS", "FAIL", "INFRA_ERROR", "BLOCKED"), required=True)
+ parser.add_argument("--reason", default="")
+ parser.add_argument("--cleanup-status", default="PENDING")
+ parser.add_argument("--evidence", action="append", default=[])
+ parser.add_argument("--candidate-manifest", type=Path)
+ parser.add_argument("--rc-bin", type=Path)
+ parser.add_argument("--testing-commit")
+ parser.add_argument("--binary-sha256")
+ parser.add_argument("--predeployment-blocked", action="store_true",
+ help="record a BLOCKED result before any candidate package was installed")
+ args = parser.parse_args()
+ if args.predeployment_blocked and (args.verdict != "BLOCKED"
+ or args.case_id not in {
+ "heal.disk-replacement-formatted",
+ "heal.disk-replacement-unformatted",
+ }):
+ parser.error("--predeployment-blocked is reserved for replacement admission results")
+ catalog = json.loads(args.catalog.read_text(encoding="utf-8"))
+ case = case_from_catalog(catalog, args.case_id)
+ result = {
+ "schema_version": 1,
+ "case_id": args.case_id,
+ "source_status": case["source_status"],
+ "migration_status": "unverified" if args.verdict in {"RUNNING", "BLOCKED", "INFRA_ERROR"} else "verified",
+ "profile": args.profile,
+ "verdict": args.verdict,
+ "cleanup_status": args.cleanup_status,
+ }
+ if args.verdict != "RUNNING":
+ if not args.candidate_manifest or not args.rc_bin or not args.testing_commit:
+ parser.error("terminal results require candidate manifest, rc binary, and testing commit")
+ candidate = json.loads(args.candidate_manifest.read_text(encoding="utf-8"))
+ binary_sha256 = (None if args.predeployment_blocked else
+ args.binary_sha256 or candidate.get("binary_sha256"))
+ bootstrap_error = args.verdict == "INFRA_ERROR" and binary_sha256 is None
+ if not bootstrap_error and not args.predeployment_blocked and (not isinstance(binary_sha256, str) or len(binary_sha256) != 64 or any(
+ character not in "0123456789abcdef" for character in binary_sha256
+ )):
+ parser.error("terminal results require a valid installed binary SHA-256")
+ if not args.predeployment_blocked and candidate.get("binary_sha256") and candidate["binary_sha256"] != binary_sha256:
+ parser.error("installed binary SHA-256 differs from candidate manifest")
+ rc_path = args.rc_bin.resolve(strict=True)
+ result["provenance"] = {
+ "candidate_url": candidate["package_url"],
+ "candidate_sha256": candidate["package_sha256"],
+ "binary_sha256": binary_sha256,
+ "source_commit": candidate["source_sha"],
+ "testing_commit": args.testing_commit,
+ "rc_path": str(rc_path),
+ "rc_sha256": hashlib.sha256(rc_path.read_bytes()).hexdigest(),
+ }
+ if args.predeployment_blocked:
+ result["execution_stage"] = "pre-deployment"
+ result["provenance"]["binary_sha256"] = None
+ result["evidence"] = args.evidence
+ if args.verdict == "PASS":
+ if not args.evidence or args.cleanup_status != "PASS":
+ parser.error("PASS requires evidence and clean finalization")
+ result["assertions"] = [
+ {"source_assertion": text, "status": "PASS", "evidence": args.evidence}
+ for text in case["assertions"]
+ ]
+ else:
+ result["reason"] = args.reason or "case or environment failed; inspect evidence"
+ write_result(args.result_file, result)
+ return 0
+
+
+if __name__ == "__main__":
+ try:
+ sys.exit(main())
+ except (OSError, ValueError, KeyError, json.JSONDecodeError) as exc:
+ print(f"validation result error: {exc}", file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/validation_webhooks.py b/scripts/validation_webhooks.py
new file mode 100644
index 00000000..cbf65d7a
--- /dev/null
+++ b/scripts/validation_webhooks.py
@@ -0,0 +1,124 @@
+#!/usr/bin/env python3
+"""Check canonical event and audit webhook evidence without exporting secrets."""
+
+from __future__ import annotations
+
+import argparse
+import json
+import os
+from pathlib import Path
+import sys
+import time
+from urllib.parse import unquote_plus
+
+
+class IncompleteAuditRecords(ValueError):
+ """The requested operations have not both arrived yet."""
+
+
+def records(path: Path) -> list[dict]:
+ values = []
+ for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
+ if not line.strip():
+ continue
+ value = json.loads(line)
+ if not isinstance(value, dict):
+ raise ValueError(f"webhook line {number} is not an object")
+ values.append(value)
+ return values
+
+
+def event_summary(values: list[dict], bucket: str, key: str) -> dict:
+ matches = []
+ for value in values:
+ for event in value.get("Records", [value]):
+ if not isinstance(event, dict):
+ raise ValueError("event record is not an object")
+ s3 = event.get("s3") or {}
+ event_bucket = (s3.get("bucket") or {}).get("name") or event.get("Bucket")
+ event_key = (s3.get("object") or {}).get("key") or event.get("Key")
+ if event_bucket == bucket and isinstance(event_key, str) and unquote_plus(event_key) == key:
+ matches.append(event)
+ if len(matches) != 1:
+ raise ValueError(f"expected one matching object event, found {len(matches)}")
+ event = matches[0]
+ name = event.get("eventName") or event.get("EventName")
+ if name != "s3:ObjectCreated:Put":
+ raise ValueError(f"unexpected event type: {name!r}")
+ return {"bucket": bucket, "key": key, "delivery_count": 1, "event_name": name}
+
+
+def audit_summary(values: list[dict], bucket: str, key: str) -> dict:
+ serialized = json.dumps(values, ensure_ascii=False).lower()
+ for name in ("RUSTFS_SECRET_KEY", "VALIDATION_AUDIT_TOKEN", "VALIDATION_NOTIFY_TOKEN"):
+ secret = os.environ.get(name, "")
+ if secret and secret.lower() in serialized:
+ raise ValueError(f"audit payload exposes {name}")
+ matches: dict[str, dict] = {}
+ entries = []
+ for value in values:
+ batch = value.get("Records", [value])
+ if not isinstance(batch, list) or any(not isinstance(entry, dict) for entry in batch):
+ raise ValueError("audit Records must be an array of objects")
+ entries.extend(batch)
+ for value in entries:
+ api = value.get("api") or {}
+ if api.get("bucket") != bucket or api.get("object") != key:
+ continue
+ api_name = api.get("name")
+ name = {"PutObject": "PutObject", "GetObject": "GetObject",
+ "s3:PutObject": "PutObject", "s3:GetObject": "GetObject"}.get(api_name)
+ if name is None:
+ continue
+ request_id = value.get("requestID")
+ if not isinstance(request_id, str) or not request_id:
+ raise ValueError(f"{name} audit record lacks requestID")
+ if api.get("status_code") != 200:
+ raise ValueError(f"{name} audit record lacks successful status")
+ headers = value.get("requestHeader") or {}
+ for header, content in headers.items():
+ if header.lower() == "authorization" and isinstance(content, str) and content.strip("*"):
+ raise ValueError("audit payload exposes authorization header")
+ matches[name] = {"api_name": api_name, "request_id": request_id, "status_code": 200}
+ if set(matches) != {"PutObject", "GetObject"}:
+ raise IncompleteAuditRecords(
+ "matching PUT and GET audit records were not both delivered; "
+ f"observed operations: {sorted(matches)}"
+ )
+ if matches["PutObject"]["request_id"] == matches["GetObject"]["request_id"]:
+ raise ValueError("PUT and GET audit records share a request ID")
+ return {"bucket": bucket, "key": key, "operations": matches, "secret_check": "PASS"}
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("mode", choices=("events", "audit"))
+ parser.add_argument("--input", type=Path, required=True)
+ parser.add_argument("--bucket", required=True)
+ parser.add_argument("--key", required=True)
+ parser.add_argument("--output", type=Path, required=True)
+ parser.add_argument("--timeout", type=float, default=0,
+ help="Wait this many seconds for both audit operations; invalid records fail immediately.")
+ args = parser.parse_args()
+ if args.timeout < 0 or args.timeout > 300:
+ parser.error("--timeout must be between 0 and 300 seconds")
+ deadline = time.monotonic() + args.timeout
+ while True:
+ values = records(args.input)
+ try:
+ result = (event_summary if args.mode == "events" else audit_summary)(values, args.bucket, args.key)
+ break
+ except IncompleteAuditRecords:
+ if time.monotonic() >= deadline:
+ raise
+ time.sleep(min(2, max(0, deadline - time.monotonic())))
+ args.output.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8")
+ return 0
+
+
+if __name__ == "__main__":
+ try:
+ sys.exit(main())
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
+ print(f"validation webhook error: {exc}", file=sys.stderr)
+ sys.exit(1)
diff --git a/test_validation_heal.py b/test_validation_heal.py
new file mode 100644
index 00000000..7a177e51
--- /dev/null
+++ b/test_validation_heal.py
@@ -0,0 +1,4167 @@
+"""Check preservation of Heal branches and prevent evidence from becoming PASS."""
+
+import hashlib
+import json
+from pathlib import Path
+import tempfile
+import unittest
+import copy
+import gzip
+from datetime import UTC, datetime, timedelta
+import types
+from unittest.mock import patch, Mock
+
+from scripts.validation_heal.matrix import STAGES, TOPOLOGIES, audit_evidence, cases, compile_plan, select
+from scripts.validation_heal.healthy import validate_healthy, validate_topology, validate_rejection, Fleet, BASE_OPTIONS
+from scripts.validation_heal.fleet_node import paths, stopped, inspect_metadata
+from scripts.validation_heal.inspector import verify as verify_inspector
+from scripts.validation_heal.shards import (BRANCHES, files, map_target, validate_fault, validate_repaired_physical,
+ validate_repaired_status, peer_status_equal, validate_dry_run_status, validate_coordination_rejection,
+ validate_coordination_rejection_logs)
+from scripts.validation_heal.heal_fault import mutation
+from scripts.validation_heal.shards import validate_conditional_response
+from scripts.validation_heal.shards import run_task, verify_ranges
+from scripts.validation_heal.versions import (BRANCHES as VERSION_BRANCHES, validate_version_receipts,
+ branch_plan, suspend_fixture, validate_failed_version, validate_unrecoverable_get, verify_quorum_contract,
+ map_inline_target, verify_proof)
+from scripts.validation_heal.versions import seed_semantics, verify_semantics
+from scripts.validation_heal.versions import multi_loss_instances, multi_loss_plan, SPECIAL_KEYS, verify_pagination, boundary_plan
+from scripts.validation_heal.versions import validate_all_members_online, wait_all_members_online, release_permission_gate_live
+from scripts.validation_heal.heal_probe import Probe, validate_s3_request
+from scripts.validation_heal.prepare_heal_meta_inspector import quorum_contract, size_contract
+from scripts.validation_heal.boundaries import verify_size_contract, boundary_sizes, verify_boundary_ranges
+from scripts.validation_heal.node_outage import quorum_plan, put_ack, delete_ack, replica_census, validate_deleted_read
+from scripts.validation_heal.fleet_node import runtime_config, BACKGROUND_CONFIG
+from scripts.validation_heal.object_lock import (validate_protection, validate_protected_delete, cleanup_locked_fleet,
+ seed_locked, BRANCHES as LOCK_BRANCHES, KEYS as LOCK_KEYS, CLEANUP_SECONDS)
+from scripts.validation_heal.versions import mismatch_plan, seed_mismatch
+from scripts.validation_heal import heal_fault
+from scripts.validation_heal.read_repair import source_tasks, validate_read, tracked_read_task, ADMISSION_MESSAGE, ADMISSION_TARGET
+from scripts.validation_heal.drive_gate import apply_gate, activate_gate, release_gate, checked_gate, start_properties, validate_probe, validate_unwritable
+from scripts.validation_heal.scanner_repair import (source_tasks as scanner_sources, cycle_pairs,
+ observe_task as observe_scanner, JournalWindow)
+from scripts.validation_heal.fleet_node import logs as node_logs
+from scripts.validation_heal.markers import (INSTANCES as MARKER_INSTANCES, seed_markers, marker_plan,
+ stale_expected_scene, add_marker, remove_marker, validate_marker_error)
+from scripts.validation_heal.drive_gate import probe_gate
+from scripts.validation_heal.status import (validate_background, validate_replacement, validate_cli,
+ reject_unsafe_completion, watch_startup, stable_status, Cli)
+from scripts.validation_heal.cursor import (Window, OBJECTS as CURSOR_OBJECTS, SIZE as CURSOR_SIZE,
+ BRANCHES as CURSOR_BRANCHES, terminal_proof as cursor_terminal, validate_missing_batch, verify_all_parts)
+from scripts.validation_heal.fleet_node import resource_limits, verify_parts as node_verify_parts
+from scripts.validation_heal.stop import (partial_window, validate_retired_stop, validate_unknown_token, validate_cancelled,
+ cancelled_physical, idle_fleet as stop_idle_fleet, BRANCHES as STOP_BRANCHES)
+from scripts.validation_heal.cancel_restart import validate_journal, BRANCHES as CANCEL_RESTART_BRANCHES
+from scripts.validation_heal.fleet_node import task_records as node_task_records
+from scripts.validation_heal import overlap
+from scripts.validation_heal import rc_binary
+from scripts.validation_heal import root_restart, fleet_node
+from scripts.validation_heal import quorum_boundary
+from scripts.validation_heal import stale_rejoin
+from scripts.validation_heal import disk_loss
+from scripts.validation_heal import replace_drive, replacement
+import errno
+import stat
+
+
+def versioned_scene(keys, sizes=(100, 100, 61)):
+ base = HealShardOracleTest().snapshot()
+ bucket = "rv-heal-0123456789abcdef"
+ expected = []
+ snapshot = {"node1": {"formats": base["node1"]["formats"], "files": []}}
+ for key in keys:
+ for entry in base["node1"]["files"]:
+ if "/control/" in entry["path"]: continue
+ cloned = copy.deepcopy(entry)
+ cloned["path"] = cloned["path"].replace("objects/0008.bin", key)
+ if "metadata" in cloned:
+ cloned["metadata"]["key"] = key
+ versions = []
+ for nonce, size in enumerate(sizes):
+ version = copy.deepcopy(cloned["metadata"]["versions"][0])
+ version.update(versionId=f"v{nonce}", isLatest=nonce == 2, size=size,
+ dataDir=f"12345678-1234-1234-1234-{nonce:012d}")
+ version["parts"][0].update(size=size, actualSize=size)
+ versions.append(version)
+ cloned["metadata"]["versions"] = versions
+ snapshot["node1"]["files"].append(cloned)
+ else:
+ for nonce, size in enumerate(sizes):
+ part = copy.deepcopy(cloned)
+ part["path"] = part["path"].replace("12345678-1234-1234-1234-123456789abc", f"12345678-1234-1234-1234-{nonce:012d}")
+ part["sha256"] += f"v{nonce}"
+ part["size"] = (size + 1) // 2 + 32
+ snapshot["node1"]["files"].append(part)
+ expected.extend({"key": key, "versionId": f"v{nonce}", "deleteMarker": False, "isLatest": nonce == 2,
+ "payload": {"size": size}} for nonce, size in enumerate(sizes))
+ return snapshot, bucket, expected
+
+
+class HealContractTest(unittest.TestCase):
+ def test_preserves_all_source_cases_and_applicable_branches(self):
+ inventory = cases()
+ plan = compile_plan(inventory, TOPOLOGIES, "manual-heal-unit-test")
+ self.assertEqual(len(inventory), 84)
+ self.assertEqual(sum(len(item["variants"]) for item in inventory), 302)
+ self.assertEqual(plan["branchTemplateCount"], 534)
+ self.assertEqual(len(plan["nonApplicableCombinations"]), 4)
+ self.assertFalse(plan["automaticExecution"])
+ self.assertFalse(plan["livePassClaimed"])
+ self.assertFalse(plan["environmentBinding"]["verified"])
+ self.assertTrue(plan["environmentBinding"]["topologies"]["H4"]["requiresExistingAdditionalDrives"])
+
+ def test_narrowed_topology_keeps_missing_scope_explicit(self):
+ plan = compile_plan(select("A01,A03"), {"H1"}, "manual-heal-unit-test")
+ self.assertEqual(len(plan["instances"]), 2)
+ self.assertEqual({item["caseId"] for item in plan["scopeExclusions"]},
+ {"heal.admin-healthy-noop", "heal.admin-token-cross-node-status"})
+ self.assertFalse(plan["livePassClaimed"])
+ with self.assertRaisesRegex(ValueError, "same Heal case"):
+ select("A01,heal.admin-healthy-noop")
+
+ def fixture_archive(self, root, plan):
+ for instance in plan["instances"]:
+ directory = root / instance["instanceId"]
+ directory.mkdir()
+ proof = {"runId": plan["runId"], "instanceId": instance["instanceId"], "evidence": {}}
+ for stage in STAGES:
+ value = (stage + " live observation").encode()
+ (directory / (stage + ".txt")).write_bytes(value)
+ proof["evidence"][stage] = [{"path": stage + ".txt", "sha256": hashlib.sha256(value).hexdigest()}]
+ (directory / "evidence.json").write_text(json.dumps(proof))
+
+ def test_complete_archive_requires_review_and_never_manufactures_pass(self):
+ plan = compile_plan(select("A01"), {"H1"}, "manual-heal-unit-test")
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ self.fixture_archive(root, plan)
+ result = audit_evidence(plan, root)
+ self.assertEqual(result["state"], "EVIDENCE_COMPLETE_REVIEW_REQUIRED")
+ self.assertFalse(result["livePassClaimed"])
+ self.assertIsNone(result["canonicalVerdict"])
+ first = root / plan["instances"][0]["instanceId"]
+ (first / "physical.txt").write_text("different evidence")
+ with self.assertRaisesRegex(ValueError, "digest mismatch"):
+ audit_evidence(plan, root)
+
+
+ def test_rejects_dropped_branch_and_escaped_evidence(self):
+ plan = compile_plan(select("A01"), {"H1"}, "manual-heal-unit-test")
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ self.fixture_archive(root, plan)
+ dropped = {**plan, "instances": plan["instances"][:1]}
+ with self.assertRaisesRegex(ValueError, "omitted or changed"):
+ audit_evidence(dropped, root)
+ first = root / plan["instances"][0]["instanceId"]
+ index = first / "evidence.json"
+ proof = json.loads(index.read_text())
+ proof["evidence"]["physical"][0]["path"] = "../outside.txt"
+ index.write_text(json.dumps(proof))
+ with self.assertRaisesRegex(ValueError, "unsafe Heal evidence"):
+ audit_evidence(plan, root)
+
+
+class HealHealthyOracleTest(unittest.TestCase):
+ def test_inspector_rejects_other_candidate_tampered_binary_and_symlinks(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ header = bytearray(20)
+ header[:6] = b"\x7fELF\x02\x01"
+ header[18:20] = b"\x3e\x00"
+ (root / "heal-meta-inspect").write_bytes(header)
+ (root / "inspector-Cargo.lock").write_text("unit fixture lock")
+ helper = Path(__file__).parent / "scripts/validation_heal/tools/heal_meta_inspect.rs"
+ manifest = {"built": True, "candidateCommit": "a" * 40, "candidateLockSha256": "b" * 64,
+ "role": "read-only decoder; not the RustFS service under test", "target": "x86_64-unknown-linux-gnu",
+ "binarySha256": hashlib.sha256(header).hexdigest(),
+ "helperSourceSha256": hashlib.sha256(helper.read_bytes()).hexdigest(),
+ "inspectorLockSha256": hashlib.sha256((root / "inspector-Cargo.lock").read_bytes()).hexdigest()}
+ (root / "heal-meta-inspect.json").write_text(json.dumps(manifest))
+ self.assertEqual(verify_inspector(root, {"source_sha": "a" * 40}), manifest)
+ with self.assertRaisesRegex(ValueError, "candidate"):
+ verify_inspector(root, {"source_sha": "c" * 40})
+ (root / "heal-meta-inspect").write_bytes(header + b"different bytes")
+ with self.assertRaisesRegex(ValueError, "digest"):
+ verify_inspector(root, {"source_sha": "a" * 40})
+ (root / "heal-meta-inspect").unlink()
+ (root / "heal-meta-inspect").symlink_to(root / "inspector-Cargo.lock")
+ with self.assertRaisesRegex(ValueError, "artifact file"):
+ verify_inspector(root, {"source_sha": "a" * 40})
+
+ def test_request_rejection_needs_the_exact_server_code(self):
+ good = {"httpStatus": 400, "response": "InvalidArgument"}
+ validate_rejection(good, "InvalidArgument")
+ for bad in ({**good, "httpStatus": 200}, {"httpStatus": 400, "response": "AccessDenied"},
+ {"httpStatus": 400, "response": {"Code": "InvalidArgument", "clientToken": "admitted"}}):
+ with self.assertRaises(ValueError):
+ validate_rejection(bad, "InvalidArgument")
+
+ def receipt(self, disposition="metadata_healthy"):
+ return {"summary": "finished", "outcome": {"execution": {"state": "completed"},
+ "coverage": "complete", "objectsTruncated": False,
+ "counters": {"processed": 1, "healed": 0, "unchanged": 1, "skipped": 0,
+ "failed": 0, "unknown": 0, "attemptFailures": 0, "overflowed": False},
+ "objects": [{"identity": {"bucket": "fixture", "object": "object.bin", "poolIndex": 0,
+ "setIndex": 0, "bucketIncarnationId": "incarnation"},
+ "disposition": {"state": disposition}}]}}
+
+ def test_metadata_is_not_deep_integrity_and_legacy_finish_is_not_success(self):
+ healthy = self.receipt()
+ validate_healthy(healthy, "fixture", "object.bin", "metadata_healthy")
+ with self.assertRaisesRegex(ValueError, "expected verified_healthy"):
+ validate_healthy(healthy, "fixture", "object.bin", "verified_healthy")
+ validate_healthy(self.receipt("verified_healthy"), "fixture", "object.bin", "verified_healthy")
+ for changed in ({"summary": "finished"}, {**healthy, "truncated": True}):
+ with self.assertRaises(ValueError):
+ validate_healthy(changed, "fixture", "object.bin", "metadata_healthy")
+
+ def test_rejects_wrong_identity_unknown_counters_and_duplicate_receipts(self):
+ for kind in ("wrong-object", "unknown", "missing-incarnation", "duplicate", "boolean-counter"):
+ value = copy.deepcopy(self.receipt())
+ outcome = value["outcome"]
+ if kind == "wrong-object": outcome["objects"][0]["identity"]["object"] = "other.bin"
+ elif kind == "unknown": outcome["counters"]["unknown"] = 1
+ elif kind == "missing-incarnation": outcome["objects"][0]["identity"].pop("bucketIncarnationId")
+ elif kind == "duplicate": outcome["objects"].append(outcome["objects"][0])
+ else: outcome["counters"]["processed"] = True
+ with self.subTest(kind=kind), self.assertRaises(ValueError):
+ validate_healthy(value, "fixture", "object.bin", "metadata_healthy")
+
+ def test_h2_requires_one_set_spanning_all_sixteen_disks(self):
+ disks = [str(index) for index in range(16)]
+ snapshot = {f"node{node}": {"formats": [{"value": {"format": "xl", "id": "pool",
+ "xl": {"sets": [disks], "this": disks[(node - 1) * 4 + disk]}}} for disk in range(4)]}
+ for node in range(1, 5)}
+ self.assertEqual(validate_topology(snapshot, "H2")["independentDrives"], 16)
+ bad = copy.deepcopy(snapshot)
+ bad["node4"]["formats"][0]["value"]["xl"]["this"] = "0"
+ with self.assertRaisesRegex(ValueError, "mapping differs"):
+ validate_topology(bad, "H2")
+ with self.assertRaisesRegex(ValueError, "node count"):
+ validate_topology({"node1": snapshot["node1"]}, "H2")
+
+ def test_node_paths_cannot_select_shared_roots_or_arbitrary_run(self):
+ with tempfile.TemporaryDirectory() as directory, patch("scripts.validation_heal.fleet_node.STATE_ROOT", Path(directory).resolve()):
+ state, roots = paths("gh-12345-1-h1")
+ self.assertTrue(all(root.parts[-2:] == ("heal-validation", "gh-12345-1-h1") for root in roots))
+ for value in ("../mnmd", "manual-arbitrary", "gh-12345-1-h4"):
+ with self.assertRaisesRegex(ValueError, "run ID"):
+ paths(value)
+
+
+class HealShardOracleTest(unittest.TestCase):
+ def test_ranges_at_one_megabyte_boundary_stay_inside_the_real_payload(self):
+ for size in (1024**2 - 1, 1024**2, 1024**2 + 1):
+ body = b"x" * size
+ requested = []
+ def s3(operation, params):
+ start, end = map(int, params["Range"].removeprefix("bytes=").split("-"))
+ self.assertTrue(0 <= start <= end < size)
+ requested.append((start, end))
+ value = body[start:end + 1]
+ return {"acknowledged": True, "response": {"ResponseMetadata": {"HTTPStatusCode": 206},
+ "ContentRange": f"bytes {start}-{end}/{size}"},
+ "payload": {"size": len(value), "sha256": hashlib.sha256(value).hexdigest()}}
+ verify_ranges(types.SimpleNamespace(s3=s3), types.SimpleNamespace(record=lambda label, value: value),
+ "fixture", "objects/0000.bin", expected_body=body)
+ self.assertEqual(requested[-1][1], size - 1)
+ if size > 1024**2:
+ self.assertTrue(any(start < 1024**2 <= end for start, end in requested))
+
+ def test_bucket_and_literal_object_routes_keep_the_original_token_for_status(self):
+ for key, expected_path in (("", "/rustfs/admin/v3/heal/fixture"),
+ ("literal%2F/key", "/rustfs/admin/v3/heal/fixture/literal%252F/key")):
+ calls = []
+ def admin(path, query, body):
+ calls.append((path, copy.deepcopy(query), body))
+ if not query:
+ return {"httpStatus": 200, "response": {"clientToken": "original-token"}}
+ return {"httpStatus": 200, "response": HealHealthyOracleTest().receipt()}
+ active, admissions = [], []
+ run_task(types.SimpleNamespace(admin=admin), types.SimpleNamespace(record=lambda label, value: value),
+ {"bucket": "fixture", "key": key}, 2, active,
+ option_overrides={"recursive": True}, admissions=admissions)
+ self.assertEqual(len(calls), 4)
+ self.assertTrue(all(path == expected_path for path, _, _ in calls))
+ self.assertTrue(all(query == [("clientToken", "original-token")] for _, query, _ in calls[1:]))
+ self.assertTrue(json.loads(calls[0][2])["recursive"])
+ self.assertEqual(active, [])
+ self.assertEqual(admissions, [{"path": expected_path, "clientToken": "original-token"}])
+
+ def test_exact_coordinator_rejection_is_branch_scoped_and_never_admits_a_task(self):
+ body = ('InternalError'
+ 'cluster heal coordination unavailable')
+ observed = {"method": "POST", "httpStatus": 500, "response": body,
+ "startedAt": "2026-10-23T06:03:32.490+00:00",
+ "finishedAt": "2026-10-23T06:03:32.520+00:00", "endpoint": "node1",
+ "responseHeaders": {"x-request-id": "request-123"}}
+ calls, records, active, admissions = [], [], [], []
+
+ def admin(path, query, request_body):
+ calls.append((path, query, request_body))
+ return {**observed, "path": path}
+
+ result = run_task(types.SimpleNamespace(admin=admin),
+ types.SimpleNamespace(record=lambda label, value: records.append((label, value)) or value,
+ nodes=["vm000"], run_id="gh-39909050467-1-h1"),
+ {"bucket": "fixture", "key": "objects/0008.bin"}, 2, active,
+ admissions=admissions, allow_coordination_rejection=True)
+ self.assertEqual(result["admissionState"], "coordination-rejected")
+ self.assertEqual(result["httpStatus"], 500)
+ self.assertFalse(result["taskAdmitted"])
+ self.assertIsNone(result["clientToken"])
+ self.assertFalse(result["responsibilityReported"])
+ self.assertEqual(len(calls), 1)
+ self.assertEqual(result["requestWindow"]["requestId"], "request-123")
+ self.assertEqual(result["requestWindow"]["serviceNode"], "vm000")
+ self.assertEqual(active, [])
+ self.assertEqual(admissions, [])
+ with self.assertRaisesRegex(ValueError, "repair admission failed"):
+ run_task(types.SimpleNamespace(admin=admin),
+ types.SimpleNamespace(record=lambda _label, value: value),
+ {"bucket": "fixture", "key": "objects/0008.bin"}, 2, [],
+ allow_coordination_rejection=False)
+
+ def test_coordinator_rejection_requires_exact_status_and_xml(self):
+ exact = {"httpStatus": 500, "response": (''
+ 'InternalErrorcluster heal coordination unavailable')}
+ self.assertEqual(validate_coordination_rejection(exact)["errorCode"], "InternalError")
+ for changed in (
+ {**exact, "httpStatus": 503},
+ {**exact, "response": exact["response"].replace("unavailable", "temporarily unavailable")},
+ {**exact, "response": exact["response"] + " "},
+ ):
+ with self.subTest(changed=changed):
+ with self.assertRaisesRegex(ValueError, "reviewed coordinator failure"):
+ validate_coordination_rejection(changed)
+ request = {"method": "POST", "httpStatus": 500,
+ "path": "/rustfs/admin/v3/heal/fixture/objects/0008.bin",
+ "startedAt": "2026-10-23T06:03:32.490+00:00",
+ "finishedAt": "2026-10-23T06:03:32.520+00:00", "response": exact["response"],
+ "requestId": "request-123", "endpoint": "node1", "serviceNode": "vm000",
+ "unitFragment": "gh-39909050467-1-h1"}
+ base_event = {"timestamp": "2026-10-23T06:03:32.511021252Z", "level": "WARN",
+ "message": "cluster heal coordination failed", "event": "admin_request_failed",
+ "component": "admin_api", "subsystem": "heal_admin", "operation": "heal_control",
+ "result": "failed", "reason": "local_coordinator_failed",
+ "error": 'code: \'Internal error\', message: "heal control start admission failed"',
+ "target": "rustfs::admin::handlers::heal"}
+ def logs_for(event, *, node="vm000", unit="rustfs-validation-heal-gh-39909050467-1-h1.service"):
+ return {"request": request, "peers": {node: {"node": node, "unit": unit,
+ "journal": "Oct 23 06:03:32 rustfs000000 rustfs[874327]: " + json.dumps(event)}}}
+ proof = validate_coordination_rejection_logs(logs_for(base_event))
+ self.assertTrue(proof["coordinatorFailureLogged"])
+ self.assertEqual(proof["preAdmissionFailure"], "heal control start admission failed")
+
+ capability = {**base_event, "reason": "cluster_capability_unavailable",
+ "error": "remote rpc client unavailable: can not get heal control client, err: transport error"}
+ request["startedAt"] = "2026-10-23T06:30:38.471+00:00"
+ request["finishedAt"] = "2026-10-23T06:30:38.493+00:00"
+ request["unitFragment"] = "gh-39911462644-1-h2"
+ capability["timestamp"] = "2026-10-23T06:30:38.4969218Z"
+ self.assertEqual(validate_coordination_rejection_logs(logs_for(capability,
+ unit="rustfs-validation-heal-gh-39911462644-1-h2.service"))["reason"],
+ "cluster_capability_unavailable")
+
+ unrelated = {**capability, "reason": "coordinator_request_failed"}
+ with self.assertRaisesRegex(ValueError, "exact pre-admission failure"):
+ validate_coordination_rejection_logs(logs_for(unrelated,
+ unit="rustfs-validation-heal-gh-39911462644-1-h2.service"))
+ with self.assertRaisesRegex(ValueError, "request-serving node"):
+ validate_coordination_rejection_logs(logs_for(capability, node="vm001",
+ unit="rustfs-validation-heal-gh-39911462644-1-h2.service"))
+ with self.assertRaisesRegex(ValueError, "active test run"):
+ validate_coordination_rejection_logs(logs_for(capability,
+ unit="rustfs-validation-heal-gh-39909050467-1-h1.service"))
+ late = {**capability, "timestamp": "2026-10-23T06:30:41.000Z"}
+ with self.assertRaisesRegex(ValueError, "exact pre-admission failure"):
+ validate_coordination_rejection_logs(logs_for(late,
+ unit="rustfs-validation-heal-gh-39911462644-1-h2.service"))
+
+ def test_condition_denial_is_not_a_transport_failure_or_success_payload(self):
+ baseline = {"properties": {"ETag": '"etag"'}, "payload": {"size": 3, "sha256": "correct"}}
+ good = {"response": {"ResponseMetadata": {"HTTPStatusCode": 412}, "Error": {"Code": "PreconditionFailed"}},
+ "acknowledged": False, "resultUnknown": False}
+ validate_conditional_response(good, 412, baseline, code="PreconditionFailed")
+ for changed in ({**good, "payload": {"size": 3}}, {**good, "resultUnknown": True}, {**good, "acknowledged": True}):
+ with self.assertRaises(ValueError): validate_conditional_response(changed, 412, baseline, code="PreconditionFailed")
+ success = {"response": {"ResponseMetadata": {"HTTPStatusCode": 200}, "ETag": '"etag"'},
+ "acknowledged": True, "payload": baseline["payload"]}
+ validate_conditional_response(success, 200, baseline, full_body=True)
+ with self.assertRaisesRegex(ValueError, "full content"):
+ validate_conditional_response({**success, "payload": {"size": 3, "sha256": "wrong"}}, 200, baseline, full_body=True)
+ def test_ssh_script_compiles_with_fault_module_and_keeps_structured_request(self):
+ observed = []
+ def invoke(args, **params):
+ source = params["input"]
+ compile(source, "ssh-node-script", "exec")
+ observed.append(source)
+ return types.SimpleNamespace(returncode=0, stdout='{"checked":true}', stderr="")
+ with tempfile.TemporaryDirectory() as directory, patch("scripts.validation_heal.healthy.subprocess.run", side_effect=invoke):
+ fleet = Fleet(["node1"], Path(directory), "gh-12345-1-h1")
+ for action in ("preflight", "verify-part", "verify-inline", "apply-fault", "recover-fault", "start", "cleanup", "drive-gate", "drive-gate-probe"):
+ self.assertTrue(fleet.remote("node1", action, disk=1, relativePath="literal$()'part.1")["checked"])
+ self.assertEqual(len(observed), 9)
+
+ def snapshot(self):
+ bucket, key, data_dir = "rv-heal-0123456789abcdef", "objects/0008.bin", "12345678-1234-1234-1234-123456789abc"
+ disks, distribution = [str(index) for index in range(4)], [4, 1, 2, 3]
+ observation = {"formats": [], "files": []}
+ for disk, index in enumerate(distribution):
+ observation["formats"].append({"disk": disk, "value": {"format": "xl", "id": "pool",
+ "xl": {"sets": [disks], "this": disks[disk]}}})
+ metadata = {"bucket": bucket, "key": key, "versions": [{"versionId": None, "isLatest": True,
+ "deleted": False, "dataDir": data_dir, "size": 100, "inline": None, "usesLegacyChecksum": False,
+ "role": "data" if index <= 2 else "parity", "parts": [{"number": 1, "size": 100, "actualSize": 100}],
+ "erasure": {"index": index, "data_blocks": 2, "parity_blocks": 2, "block_size": 1048576,
+ "distribution": distribution, "algorithm": "rs-vandermonde"}}]}
+ observation["files"].extend([
+ {"disk": disk, "path": f"{bucket}/{key}/xl.meta", "size": 600, "sha256": "m" + str(disk), "metadata": metadata},
+ {"disk": disk, "path": f"{bucket}/{key}/{data_dir}/part.1", "size": 82, "sha256": "s" + str(disk)},
+ {"disk": disk, "path": f"{bucket}/control/xl.meta", "size": 600, "sha256": "c" + str(disk)}])
+ return {"node1": observation}
+
+ def target(self, before, role="data"):
+ return map_target(before, "rv-heal-0123456789abcdef", "objects/0008.bin", role, 1)
+
+ def test_executor_preserves_each_source_branch_and_uses_distinct_objects(self):
+ inventory = {item["id"]: item for item in cases()}
+ for case_id, branches in BRANCHES.items():
+ self.assertEqual([branch[0] for branch in branches], inventory[case_id]["variants"])
+ self.assertEqual(len({branch[1] for branch in branches}), len(branches))
+ self.assertEqual(inventory[case_id]["contract"]["probes"]["topology"],
+ "H2" if case_id == "heal.admin-token-cross-node-status" else "H1,H2")
+ for case_id, branches in VERSION_BRANCHES.items():
+ self.assertEqual(list(branches), inventory[case_id]["variants"])
+
+ def test_peer_status_cannot_converge_with_wrong_task_unknown_or_incomplete_receipts(self):
+ coordinator = {**HealHealthyOracleTest().receipt("repaired"), "startTime": "exact-task-start"}
+ task = ("/rustfs/admin/v3/heal/fixture/object", "original-token")
+ self.assertTrue(peer_status_equal(coordinator, copy.deepcopy(coordinator), task=task, peer_task=task))
+ rerendered = {**copy.deepcopy(coordinator), "startTime": "fresh response timestamp"}
+ self.assertTrue(peer_status_equal(coordinator, rerendered, task=task, peer_task=task))
+ for invalid in ("task-start", "summary", "coverage", "outcome", "truncated"):
+ peer = copy.deepcopy(coordinator)
+ peer_task = task
+ if invalid == "task-start": peer_task = (task[0], "different-task")
+ elif invalid == "summary": peer["summary"] = "unknown"
+ elif invalid == "coverage": peer["outcome"]["coverage"] = "partial"
+ elif invalid == "outcome": peer["outcome"]["counters"]["unknown"] = 1
+ else: peer["truncated"] = True
+ with self.subTest(invalid=invalid): self.assertFalse(peer_status_equal(coordinator, peer, task=task, peer_task=peer_task))
+
+ def test_peer_convergence_retains_expected_failures_without_certifying_repair(self):
+ terminal = HealHealthyOracleTest().receipt("failed")
+ terminal["summary"] = "stopped"
+ terminal["outcome"]["execution"]["state"] = "completed_with_errors"
+ terminal["outcome"]["counters"].update(unchanged=0, failed=1, attemptFailures=1)
+ terminal["settings"] = {"recursive": False, "pool": 0, "set": 0}
+ task = ("/rustfs/admin/v3/heal/fixture/object.bin", "original-token")
+ self.assertTrue(peer_status_equal(terminal, copy.deepcopy(terminal), task=task, peer_task=task))
+ with self.assertRaises(ValueError): validate_healthy(terminal, "fixture", "object.bin", "verified_healthy")
+ for mutation in ("aborted", "running", "finished", "zero-failures", "bool-failures", "settings",
+ "truncated", "partial", "token"):
+ peer, original = copy.deepcopy(terminal), copy.deepcopy(terminal)
+ peer_task = task
+ if mutation == "aborted": original["outcome"]["execution"] = {"state": "aborted", "reason": "untraversable"}
+ elif mutation == "running": original["outcome"]["execution"]["state"] = "running"
+ elif mutation == "finished": original["summary"] = "finished"
+ elif mutation == "zero-failures": original["outcome"]["counters"]["failed"] = 0
+ elif mutation == "bool-failures": original["outcome"]["counters"]["failed"] = True
+ elif mutation == "settings": peer["settings"]["set"] = 1
+ elif mutation == "truncated": original["truncated"] = True
+ elif mutation == "partial": original["outcome"]["coverage"] = "partial"
+ else: peer_task = (task[0], "replacement-token")
+ if mutation not in {"settings", "token"}: peer = copy.deepcopy(original)
+ with self.subTest(mutation=mutation):
+ self.assertFalse(peer_status_equal(original, peer, task=task, peer_task=peer_task))
+
+ def test_target_role_comes_from_metadata_and_actual_format_slot(self):
+ before = self.snapshot()
+ self.assertEqual(self.target(before)["disk"], 1)
+ self.assertEqual(self.target(before, "parity")["disk"], 0)
+ for invalid in ("index", "role", "dataDir", "missing-part", "same-shards"):
+ changed = copy.deepcopy(before)
+ metadata = changed["node1"]["files"][0]["metadata"]["versions"][0]
+ if invalid == "index": metadata["erasure"]["index"] = 1
+ elif invalid == "role": metadata["role"] = "data"
+ elif invalid == "dataDir": metadata["dataDir"] = "../control"
+ elif invalid == "missing-part": changed["node1"]["files"].pop(1)
+ else:
+ for entry in changed["node1"]["files"]:
+ if entry["path"].endswith("part.1"): entry["sha256"] = "same"
+ with self.subTest(invalid=invalid), self.assertRaises(ValueError):
+ self.target(changed)
+
+ def test_fault_requires_exact_target_and_unchanged_controls_and_metadata(self):
+ before = self.snapshot()
+ target = self.target(before)
+ after = copy.deepcopy(before)
+ after["node1"]["files"] = [entry for entry in after["node1"]["files"]
+ if not (entry["disk"] == target["disk"] and entry["path"] == target["relativePath"])]
+ receipt = {"relativePath": target["relativePath"], "before": {"sha256": target["sha256"], "size": 82}, "fault": None}
+ validate_fault(before, after, target, receipt)
+ with self.assertRaisesRegex(ValueError, "baseline file"):
+ validate_repaired_physical(before, after, target)
+ changed = copy.deepcopy(after)
+ changed["node1"]["files"][0]["sha256"] = "metadata changed"
+ with self.assertRaisesRegex(ValueError, "metadata, controls"):
+ validate_fault(before, changed, target, receipt)
+ validate_repaired_physical(before, before, target)
+ rewritten = copy.deepcopy(before)
+ rewritten["node1"]["files"][0]["sha256"] = "legitimate rewrite"
+ rewritten["node1"]["files"][0]["metadata"]["versions"][0]["erasure"]["checksums"] = ["new checksum record"]
+ validate_repaired_physical(before, rewritten, target)
+ rewritten["node1"]["files"][0]["metadata"]["versions"][0]["dataDir"] = "other generation"
+ with self.assertRaisesRegex(ValueError, "version/part identity"):
+ validate_repaired_physical(before, rewritten, target)
+ changed = copy.deepcopy(before)
+ changed["node1"]["files"][-1]["sha256"] = "control changed"
+ with self.assertRaisesRegex(ValueError, "changed a control"):
+ validate_repaired_physical(before, changed, target)
+
+ def test_completed_mpu_sidecar_cleanup_is_bound_to_target_drive_and_manifest(self):
+ before = self.snapshot()
+ target = self.target(before)
+ path = target["relativePath"] + ".meta"
+ before["node1"]["files"].append({"disk": target["disk"], "path": path, "size": 100, "sha256": "old staging"})
+ after = copy.deepcopy(before)
+ after["node1"]["files"].pop()
+ validate_repaired_physical(before, after, target)
+ before["node1"]["files"][-1]["disk"] = 0
+ with self.assertRaisesRegex(ValueError, "lost a baseline file"):
+ validate_repaired_physical(before, after, target)
+
+ def test_repeated_target_backup_requires_exact_current_metadata_and_target_identity(self):
+ before = self.snapshot()
+ target = self.target(before)
+ meta = next(entry for entry in before["node1"]["files"]
+ if entry["disk"] == target["disk"] and entry["path"] == f"{target['bucket']}/{target['key']}/xl.meta")
+ backup = {"disk": target["disk"], "path": f"{target['bucket']}/{target['key']}/{target['dataDir']}/xl.meta.bkp",
+ "size": 599, "sha256": "previous-repair-metadata"}
+ before["node1"]["files"].append(backup)
+ after = copy.deepcopy(before)
+ after["node1"]["files"][-1].update(size=meta["size"], sha256=meta["sha256"])
+ validate_repaired_physical(before, after, target)
+ for invalid in ("bytes", "length", "other-drive", "other-generation", "payload"):
+ changed_before, changed_after = copy.deepcopy(before), copy.deepcopy(after)
+ if invalid == "bytes": changed_after["node1"]["files"][-1]["sha256"] = "not-authoritative"
+ elif invalid == "length": changed_after["node1"]["files"][-1]["size"] += 1
+ elif invalid == "other-drive":
+ for value in (changed_before, changed_after): value["node1"]["files"][-1]["disk"] = 0
+ elif invalid == "other-generation":
+ for value in (changed_before, changed_after): value["node1"]["files"][-1]["path"] = backup["path"].replace(target["dataDir"], "other-generation")
+ else:
+ changed_after["node1"]["files"][1]["sha256"] = "changed-payload"
+ with self.subTest(invalid=invalid), self.assertRaises(ValueError):
+ validate_repaired_physical(changed_before, changed_after, target)
+ added_before = self.snapshot()
+ added_after = copy.deepcopy(added_before)
+ added_after["node1"]["files"].append(after["node1"]["files"][-1])
+ validate_repaired_physical(added_before, added_after, target)
+ added_after["node1"]["files"][-1]["sha256"] = "unexpected"
+ with self.assertRaisesRegex(ValueError, "unexpected physical file"):
+ validate_repaired_physical(added_before, added_after, target)
+
+ def test_unknown_or_wrong_version_receipt_cannot_certify_physical_repair(self):
+ target = self.target(self.snapshot())
+ receipt = HealHealthyOracleTest().receipt("repaired")
+ receipt["outcome"]["counters"].update(healed=1, unchanged=0)
+ receipt["outcome"]["objects"][0]["identity"].update(bucket=target["bucket"], object=target["key"], versionId=None)
+ validate_repaired_status(receipt, target)
+ for invalid in ("unknown", "wrong-version", "missing-incarnation"):
+ changed = copy.deepcopy(receipt)
+ if invalid == "unknown": changed["outcome"]["counters"]["unknown"] = 1
+ elif invalid == "wrong-version": changed["outcome"]["objects"][0]["identity"]["versionId"] = "another"
+ else: changed["outcome"]["objects"][0]["identity"].pop("bucketIncarnationId")
+ with self.subTest(invalid=invalid), self.assertRaises(ValueError):
+ validate_repaired_status(changed, target)
+
+ def test_dry_run_requires_all_version_marker_receipts_and_never_claims_repair(self):
+ target = self.target(self.snapshot())
+ expected = [{"key": target["key"], "versionId": version} for version in ("old", "new", "marker")]
+ receipt = HealHealthyOracleTest().receipt("dry_run_observed")
+ receipt["outcome"]["counters"].update(processed=3, unchanged=0, skipped=3)
+ original = receipt["outcome"]["objects"][0]
+ original["identity"].update(bucket=target["bucket"], object=target["key"], bucketIncarnationId=None)
+ receipt["outcome"]["objects"] = [copy.deepcopy(original) for _ in expected]
+ for entry, version in zip(receipt["outcome"]["objects"], expected): entry["identity"]["versionId"] = version["versionId"]
+ validate_dry_run_status(receipt, target, expected)
+ for invalid in ("missing-marker", "repaired", "unknown", "healed-counter"):
+ value = copy.deepcopy(receipt)
+ if invalid == "missing-marker": value["outcome"]["objects"].pop()
+ elif invalid == "healed-counter": value["outcome"]["counters"]["healed"] = 1
+ else: value["outcome"]["objects"][0]["disposition"]["state"] = invalid
+ with self.subTest(invalid=invalid), self.assertRaises(ValueError):
+ validate_dry_run_status(value, target, expected)
+
+ def test_fault_requires_actual_service_quiescence_and_exact_truncate_length(self):
+ for value in ("MainPID=99\nActiveState=inactive\nControlGroup=\n", "MainPID=0\nActiveState=active\n", ""):
+ with patch("scripts.validation_heal.fleet_node.command", return_value=types.SimpleNamespace(stdout=value)):
+ with self.assertRaisesRegex(ValueError, "service to be stopped"):
+ stopped({"unit": "owned.service"})
+ with patch("scripts.validation_heal.fleet_node.command", return_value=types.SimpleNamespace(
+ stdout="MainPID=0\nActiveState=inactive\nControlGroup=\n")):
+ stopped({"unit": "owned.service"})
+ self.assertEqual(mutation(b"abcdef", "truncate", 5), b"abcde")
+ for length in (-1, 6, True):
+ with self.assertRaisesRegex(ValueError, "truncate length"):
+ mutation(b"abcdef", "truncate", length)
+
+
+class HealVersionOracleTest(unittest.TestCase):
+ def test_size_contract_derives_distinct_actual_inline_limits_and_rejects_changed_policy(self):
+ source = {
+ "crates/ecstore/src/config/storageclass.rs": '''pub static DEFAULT_INLINE_BLOCK: usize = 128 * 1024;
+ const DEFAULT_INLINE_OBJECT_BUDGET: usize = 2 * DEFAULT_INLINE_BLOCK;
+ pub fn should_inline(&self, shard_size: i64, data_shards: usize, versioned: bool) -> bool {
+ if shard_size < 0 || data_shards == 0 { return false; }
+ let shard_size = shard_size as usize; let inline_block = self.effective_inline_block(data_shards);
+ if versioned { shard_size <= inline_block / 8 } else { shard_size <= inline_block }
+ }
+ pub(crate) fn effective_inline_block(&self, data_shards: usize) -> usize {
+ if data_shards == 0 { return 0; }
+ if self.initialized && self.inline_block_explicit { self.inline_block }
+ else { DEFAULT_INLINE_OBJECT_BUDGET.div_ceil(data_shards).min(DEFAULT_INLINE_BLOCK) }
+ }''',
+ "crates/ecstore/src/set_disk/mod.rs": '''fn inline_admission_shard_size(erasure: &coding::Erasure, stored_size: i64, actual_size: i64) -> i64 {
+ if stored_size >= 0 { return erasure.shard_file_size(stored_size); } actual_size }''',
+ "crates/ecstore/src/set_disk/ops/object.rs": '''let is_inline_buffer = storage_class_config.should_inline(
+ inline_admission_shard_size(&erasure, put_object_size, data.actual_size()), erasure.data_shards, opts.versioned,
+ ) && usize::try_from(inline_admission_size).is_ok_and(|size| size <= erasure.block_size);''',
+ "crates/ecstore/src/erasure/coding/erasure.rs": '''pub fn calc_shard_size(block_size: usize, data_shards: usize) -> usize {
+ block_size.div_ceil(data_shards) }'''}
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ for path, text in source.items():
+ file = root / path
+ file.parent.mkdir(parents=True, exist_ok=True)
+ file.write_text(text)
+ contract = size_contract(root, "a" * 40)
+ verify_size_contract({"sizeContract": contract}, {"source_sha": "a" * 40})
+ processes = {"node": {"storageClassEnvironment": {
+ "RUSTFS_STORAGE_CLASS_INLINE_BLOCK": None, "RUSTFS_STORAGE_CLASS_STANDARD": None}}}
+ for data, inline in ((2, 262144), (12, 262152)):
+ target = {"logical": {"usesLegacyChecksum": False, "erasure": {"data_blocks": data, "block_size": 1048576}}}
+ parameters = boundary_sizes(contract, target, processes)
+ self.assertEqual(parameters["actualLogicalInlineLimit"], inline)
+ self.assertEqual(parameters["groups"]["inline-minus-exact-plus"], [inline - 1, inline, inline + 1])
+ self.assertEqual(parameters["groups"]["stripe-minus-exact-plus"], [1048575, 1048576, 1048577])
+ with self.assertRaises(ValueError): boundary_sizes(contract, target, {"node": {"storageClassEnvironment": {}}})
+ for field, value in (("recognized", False), ("extractorSourceSha256", "0" * 64), ("candidateCommit", "b" * 40)):
+ with self.assertRaises(ValueError):
+ verify_size_contract({"sizeContract": {**contract, field: value}}, {"source_sha": "a" * 40})
+ path = root / "crates/ecstore/src/erasure/coding/erasure.rs"
+ path.write_text(source[str(path.relative_to(root))].replace("div_ceil", "div_floor"))
+ self.assertFalse(size_contract(root, "a" * 40)["recognized"])
+
+ def test_zero_boundary_faults_metadata_and_never_invents_a_part(self):
+ snapshot = HealShardOracleTest().snapshot()
+ observation = snapshot["node1"]
+ observation["files"] = [entry for entry in observation["files"] if "metadata" in entry]
+ for entry in observation["files"]:
+ version = entry["metadata"]["versions"][0]
+ version["size"] = 0
+ version["parts"][0].update(size=0, actualSize=0)
+ expected = [{"key": "objects/0008.bin", "versionId": "null", "deleteMarker": False,
+ "payload": {"size": 0, "sha256": hashlib.sha256(b"").hexdigest()}}]
+ instance = {"key": expected[0]["key"], "size": 0, "role": "parity", "expectedShape": "zero-metadata", "kind": "bitflip"}
+ plan = boundary_plan(instance, snapshot, "rv-heal-0123456789abcdef", expected)
+ self.assertEqual(plan["kind"], "header")
+ self.assertTrue(plan["proofs"][0]["zeroProof"])
+ self.assertEqual(plan["faults"][0]["role"], "parity")
+ self.assertTrue(plan["faults"][0]["relativePath"].endswith("/xl.meta"))
+ observation["files"].append(next(entry for entry in HealShardOracleTest().snapshot()["node1"]["files"] if "metadata" not in entry))
+ with self.assertRaisesRegex(ValueError, "zero object"):
+ boundary_plan(instance, snapshot, "rv-heal-0123456789abcdef", expected)
+
+ def test_actual_size_ranges_require_206_bytes_and_empty_range_requires_exact_416(self):
+ from scripts.validation_heal.heal_probe import payload
+ bucket, block = "fixture", 4096
+ expected = [{"key": str(size), "payload": {"size": size}} for size in (0, 1, 4097)]
+ calls = []
+ def s3(operation, params):
+ size = int(params["Key"])
+ calls.append(params)
+ if not size:
+ return {"acknowledged": False, "resultUnknown": False, "response": {
+ "ResponseMetadata": {"HTTPStatusCode": 416}, "Error": {"Code": "InvalidRange"}}}
+ start, end = map(int, params["Range"].removeprefix("bytes=").split("-"))
+ value = payload(f"{bucket}/{params['Key']}/0", size)[start:end + 1]
+ return {"acknowledged": True, "response": {"ResponseMetadata": {"HTTPStatusCode": 206},
+ "ContentRange": f"bytes {start}-{end}/{size}"},
+ "payload": {"size": len(value), "sha256": hashlib.sha256(value).hexdigest()}}
+ fleet = types.SimpleNamespace(record=lambda label, value: value)
+ verify_boundary_ranges(types.SimpleNamespace(s3=s3), fleet, bucket, expected, block)
+ self.assertTrue(any(params["Range"] == "bytes=4079-4096" for params in calls))
+ for wrong in ({"acknowledged": False, "resultUnknown": True}, {"acknowledged": True, "payload": {"size": 0}}):
+ with self.assertRaises(ValueError):
+ verify_boundary_ranges(types.SimpleNamespace(s3=lambda *args: wrong), fleet, bucket, expected[:1], block)
+
+ def test_explicit_small_list_pages_preserve_distinct_special_keys(self):
+ bucket = "rv-heal-pages-unit"
+ keys = [*SPECIAL_KEYS, *(f"pages/{index:04d}.bin" for index in range(257))]
+ for failure in (None, "duplicate-version", "omitted-current", "oversized-page"):
+ calls = []
+ def paginator(operation):
+ def paginate(**params):
+ calls.append((operation, params))
+ self.assertEqual(params["PaginationConfig"], {"PageSize": 17})
+ listed = list(keys)
+ if failure == "duplicate-version" and operation == "list_object_versions": listed[-1] = listed[0]
+ if failure == "omitted-current" and operation == "list_objects_v2": listed.pop()
+ width = 18 if failure == "oversized-page" else 17
+ for start in range(0, len(listed), width):
+ page = listed[start:start + width]
+ yield {"Versions": [{"Key": key, "VersionId": "null", "IsLatest": True} for key in page]} \
+ if operation == "list_object_versions" else {"Contents": [{"Key": key, "Size": 1, "ETag": key} for key in page]}
+ return types.SimpleNamespace(paginate=paginate)
+ def s3(operation, params):
+ if operation == "get_object":
+ return {"acknowledged": True, "response": {"ETag": params["Key"]}, "payload": {"size": 1, "sha256": "digest"}}
+ return {"acknowledged": True, "response": {}}
+ probe = Probe.__new__(Probe)
+ probe.binding = {"bucketPrefix": bucket, "maxInventoryObjects": 512, "maxInventoryBytes": 512}
+ probe.s3_client = lambda: types.SimpleNamespace(get_paginator=paginator)
+ probe.s3 = s3
+ if failure:
+ with self.subTest(failure=failure), self.assertRaises(ValueError):
+ probe.inventory(bucket, unversioned_current_tags=True, page_size=17)
+ else:
+ observed = probe.inventory(bucket, unversioned_current_tags=True, page_size=17)
+ proof = verify_pagination(observed)
+ self.assertEqual(proof["acknowledgedObjects"], 265)
+ self.assertEqual(proof["currentPages"], 16)
+ self.assertEqual(proof["versionPages"], 16)
+ self.assertEqual({entry["key"] for entry in observed["entries"]}, set(keys))
+
+ def test_multi_loss_covers_every_actual_loss_count_and_role_composition(self):
+ cohort = {"logical": {"erasure": {"data_blocks": 12, "parity_blocks": 4}}, "cohort": list(range(16))}
+ expected = [{"key": "object", "versionId": "old", "deleteMarker": False}]
+ with patch("scripts.validation_heal.versions.choose_target", return_value=cohort):
+ instances = multi_loss_instances({}, "fixture", expected, "DV")
+ self.assertEqual(len(instances), 14)
+ self.assertEqual({(item["loss"], item["dataLoss"], item["parityLoss"]) for item in instances},
+ {(loss, data, loss - data) for loss in range(1, 5) for data in range(loss + 1)})
+ self.assertEqual(len({item["key"] for item in instances}), 14)
+ self.assertEqual({item["sourceBranch"] for item in instances}, set(VERSION_BRANCHES["heal.multiple-shard-loss"]))
+ for item in instances:
+ self.assertGreaterEqual(item["remainingSameVersionShards"], item["readQuorum"])
+ before = HealShardOracleTest().snapshot()
+ entry = {"key": "objects/0008.bin", "versionId": "null", "deleteMarker": False}
+ instance = {**instances[2], "key": entry["key"], "dataLoss": 1, "parityLoss": 1,
+ "loss": 2, "readQuorum": 2}
+ plan = multi_loss_plan(instance, before, "rv-heal-0123456789abcdef", [entry], "DB")
+ self.assertEqual({item["role"] for item in plan["faults"]}, {"data", "parity"})
+ self.assertEqual(len({item["disk"] for item in plan["faults"]}), 2)
+ self.assertEqual(plan["requiredRepairs"], {(entry["key"], "null")})
+ self.assertTrue(all(item["versionId"] == "null" for item in plan["faults"]))
+ with self.assertRaisesRegex(ValueError, "quorum"):
+ multi_loss_plan({**instance, "dataLoss": 3, "loss": 4}, before,
+ "rv-heal-0123456789abcdef", [entry], "DB")
+
+ def test_metadata_fixture_has_real_encoded_content_and_exact_distinct_version_tags(self):
+ uploaded = []
+ def call(operation, params, body=None):
+ if operation == "put_object":
+ self.assertEqual(params["ContentEncoding"], "gzip")
+ self.assertTrue(gzip.decompress(body))
+ self.assertEqual(len(gzip.decompress(body)), 5 * 1024**2 + 123)
+ self.assertTrue(all(value.isascii() for value in params["Metadata"].values()))
+ uploaded.append((params["Key"], params["Metadata"]["generation"]))
+ return {"VersionId": "version-" + str(len(uploaded))}
+ return {}
+ expected, public = seed_semantics(types.SimpleNamespace(call=call), "fixture")
+ self.assertEqual(len(expected), 9)
+ self.assertEqual(len({entry["versionId"] for entry in expected}), 9)
+ self.assertEqual(len({key for key, _ in uploaded}), 3)
+ baseline = {"entries": [{"key": spec["key"], "versionId": spec["versionId"],
+ "properties": copy.deepcopy(spec["properties"]), "tags": copy.deepcopy(spec["tags"])} for spec in public]}
+ verify_semantics(public, baseline)
+ for kind in ("header", "metadata", "versioned-tags"):
+ changed = copy.deepcopy(baseline)
+ if kind == "header": changed["entries"][0]["properties"]["ContentEncoding"] = "identity"
+ elif kind == "metadata": changed["entries"][0]["properties"]["Metadata"]["userkey"] = "another"
+ else: changed["entries"][0]["tags"] = copy.deepcopy(changed["entries"][1]["tags"])
+ with self.subTest(kind=kind), self.assertRaises(ValueError): verify_semantics(public, changed)
+
+ def test_decoder_failure_is_evidence_only_for_the_exact_witnessed_corruption(self):
+ entry = {"disk": 0, "path": "fixture/key/xl.meta", "sha256": "changed-sha", "size": 9}
+ fault = {entry["path"]: {"sha256": "changed-sha", "size": 9}}
+ rejected = types.SimpleNamespace(returncode=1, stderr="invalid captured metadata", stdout="")
+ with patch("scripts.validation_heal.fleet_node.command", return_value=rejected):
+ observed = inspect_metadata("tool", "path", "fixture", "key", entry, fault)
+ self.assertFalse(observed["metadataError"]["validMetadataClaimed"])
+ self.assertNotIn("metadata", observed)
+ for incorrect in ({}, {entry["path"]: {"sha256": "different", "size": 9}},
+ {entry["path"]: {"sha256": "changed-sha", "size": 10}}):
+ with self.assertRaises(ValueError): inspect_metadata("tool", "path", "fixture", "key", entry, incorrect)
+ decoded = {"bucket": "fixture", "key": "another", "physicalBitrotVerified": False}
+ with patch("scripts.validation_heal.fleet_node.command", return_value=types.SimpleNamespace(
+ returncode=0, stdout=json.dumps(decoded))):
+ with self.assertRaises(ValueError): inspect_metadata("tool", "path", "fixture", "key", entry, fault)
+
+ def test_inline_fault_uses_unique_candidate_decoded_slice_and_frozen_role_digest(self):
+ original = HealShardOracleTest().snapshot()
+ bucket, key = "rv-heal-0123456789abcdef", "objects/0008.bin"
+ for entry in original["node1"]["files"]:
+ if "metadata" in entry:
+ entry["metadata"]["versions"][0]["inline"] = {"length": 82, "rawOffset": 200,
+ "rawOccurrenceCount": 1, "sha256": str(entry["disk"]) * 64}
+ expected = {"key": key, "versionId": "null", "payload": {"size": 100}}
+ target = map_inline_target(original, bucket, expected)
+ self.assertEqual(target["disk"], 1)
+ def remote(node, action, **params):
+ entry = next(entry for entry in target["cohort"] if entry["disk"] == params["disk"])
+ return {"physicalBitrotVerified": True, "inlineLength": entry["inlineValue"]["length"], "inlineSha256": entry["inlineValue"]["sha256"]}
+ fleet = types.SimpleNamespace(record=lambda label, value: value, remote=remote)
+ self.assertTrue(verify_proof(fleet, target, "inline")['originalInlineSha256Restored'])
+ with patch.object(fleet, "remote", return_value={"inlineLength": 82, "inlineSha256": "wrong"}):
+ with self.assertRaises(ValueError): verify_proof(fleet, target, "inline")
+ for kind in ("ambiguous", "outside", "wrong-role", "wrong-digest"):
+ bad = copy.deepcopy(original)
+ value = next(entry["metadata"]["versions"][0] for entry in bad["node1"]["files"] if "metadata" in entry)
+ if kind == "ambiguous": value["inline"]["rawOccurrenceCount"] = 2
+ elif kind == "outside": value["inline"]["rawOffset"] = 590
+ elif kind == "wrong-role": value["role"] = "data"
+ else: value["inline"]["sha256"] = "invalid"
+ with self.subTest(kind=kind), self.assertRaises(ValueError): map_inline_target(bad, bucket, expected)
+
+ def test_below_quorum_needs_exact_failure_and_never_accepts_transport_or_false_repair(self):
+ status, expected = self.fixture()
+ status["summary"] = "stopped"
+ outcome = status["outcome"]
+ outcome["execution"]["state"] = "completed_with_errors"
+ outcome["objects"][0]["disposition"] = {"state": "failed", "details": "retry_exhausted"}
+ outcome["counters"].update(unchanged=3, failed=1, attemptFailures=2)
+ validate_failed_version(status, "fixture", expected, expected[0])
+ for kind in ("finished", "running", "completed", "unknown", "repaired", "wrong-version", "hidden-attempts", "wrong-counter"):
+ value = copy.deepcopy(status)
+ if kind in {"finished", "running"}: value["summary"] = kind
+ elif kind == "completed": value["outcome"]["execution"]["state"] = "completed"
+ elif kind in {"unknown", "repaired"}: value["outcome"]["objects"][0]["disposition"]["state"] = kind
+ elif kind == "wrong-version": value["outcome"]["objects"][0]["identity"]["versionId"] = "different"
+ elif kind == "hidden-attempts": value["outcome"]["counters"]["attemptFailures"] = 0
+ else: value["outcome"]["counters"]["failed"] = True
+ with self.subTest(kind=kind), self.assertRaises(ValueError):
+ validate_failed_version(value, "fixture", expected, expected[0])
+ contract = {"unrecoverableGet": {"httpStatus": 503, "code": "SlowDownRead"}}
+ observation = {"acknowledged": False, "resultUnknown": False,
+ "response": {"ResponseMetadata": {"HTTPStatusCode": 503}, "Error": {"Code": "SlowDownRead"}}}
+ validate_unrecoverable_get(observation, contract)
+ for changed in ({**observation, "resultUnknown": True}, {**observation, "payload": {"size": 0}},
+ {**observation, "acknowledged": True}):
+ with self.assertRaises(ValueError): validate_unrecoverable_get(changed, contract)
+
+ def test_tool_freezes_recognized_candidate_quorum_and_rejects_stale_or_changed_contract(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ error = root / "rustfs/src/error.rs"
+ metadata = root / "crates/ecstore/src/set_disk/metadata.rs"
+ error.parent.mkdir(parents=True)
+ metadata.parent.mkdir(parents=True)
+ error.write_text('const SLOW_DOWN_READ_CODE: &str = "SlowDownRead";\n'
+ 'StorageError::ErasureReadQuorum | StorageError::InsufficientReadQuorum(_, _) => '
+ '{ S3ErrorCode::Custom(SLOW_DOWN_READ_CODE.into()) }\n'
+ 'S3ErrorCode::Custom(custom) if &**custom == SLOW_DOWN_READ_CODE => Some(StatusCode::SERVICE_UNAVAILABLE)')
+ metadata.write_text('pub(super) fn object_quorum_from_meta() {\n'
+ 'let data_blocks = parts_metadata.len() as i32 - parity_blocks;\nOk((data_blocks, write_quorum))\n}')
+ contract = quorum_contract(root, "a" * 40)
+ self.assertTrue(contract["recognized"])
+ verify_quorum_contract({"quorumContract": contract}, {"source_sha": "a" * 40})
+ for changed in ({**contract, "candidateCommit": "b" * 40},
+ {**contract, "extractorSourceSha256": "0" * 64}, {**contract, "recognized": False}):
+ with self.assertRaises(ValueError):
+ verify_quorum_contract({"quorumContract": changed}, {"source_sha": "a" * 40})
+ metadata.write_text('pub(super) fn object_quorum_from_meta() {\n'
+ 'let data_blocks = parts_metadata.len() as i32 - parity_blocks;\n'
+ '// Ok((data_blocks, write_quorum))\nOk((data_blocks + 1, write_quorum))\n}')
+ self.assertFalse(quorum_contract(root, "a" * 40)["recognized"])
+
+ def test_suspended_null_fixture_retains_historical_identities_and_requires_real_marker_acks(self):
+ historical = [{"key": f"objects/{index:04d}.bin", "versionId": f"history-{index}-{version}",
+ "deleteMarker": False, "isLatest": version == 2}
+ for index in range(3) for version in range(3)]
+ calls = []
+ def invoke(operation, params, body=None):
+ calls.append((operation, copy.deepcopy(params)))
+ if operation == "get_bucket_versioning": return {"Status": "Suspended"}
+ if operation == "put_object": return {"VersionId": "null"}
+ if operation == "delete_object": return {"VersionId": "null", "DeleteMarker": True}
+ return {}
+ recorder = types.SimpleNamespace(call=invoke)
+ expected = suspend_fixture(recorder, "fixture", historical)
+ self.assertEqual(len(expected), 12)
+ self.assertEqual({item["versionId"] for item in expected if item["versionId"] != "null"},
+ {item["versionId"] for item in historical})
+ self.assertTrue(all(not item["isLatest"] for item in expected if item["versionId"] != "null"))
+ markers = [item for item in expected if item["deleteMarker"]]
+ self.assertEqual(markers, [{"key": "objects/0002.bin", "versionId": "null", "isLatest": True, "deleteMarker": True}])
+ self.assertEqual(sum(operation == "delete_object" and params.get("VersionId") == "null"
+ for operation, params in calls), 3)
+ for kind in ("fake-version", "missing-marker"):
+ def invalid(operation, params, body=None):
+ result = invoke(operation, params, body)
+ if kind == "fake-version" and operation == "put_object": result["VersionId"] = "unexpected-new-version"
+ elif kind == "missing-marker" and operation == "delete_object": result["DeleteMarker"] = False
+ return result
+ with self.subTest(kind=kind), self.assertRaises(ValueError):
+ suspend_fixture(types.SimpleNamespace(call=invalid), "fixture", historical)
+ self.assertTrue(all(item["isLatest"] == (index % 3 == 2) for index, item in enumerate(historical)))
+
+ def fixture(self):
+ expected = [{"key": "object", "versionId": version, "deleteMarker": version == "marker"}
+ for version in ("old", "middle", "current", "marker")]
+ status = HealHealthyOracleTest().receipt()
+ original = status["outcome"]["objects"][0]
+ original["identity"].update(bucket="fixture", object="object")
+ status["outcome"]["objects"] = [copy.deepcopy(original) for _ in expected]
+ for receipt, entry in zip(status["outcome"]["objects"], expected):
+ receipt["identity"]["versionId"] = entry["versionId"]
+ receipt["disposition"]["state"] = "metadata_healthy" if entry["deleteMarker"] else "verified_healthy"
+ status["outcome"]["counters"].update(processed=4, unchanged=4)
+ return status, expected
+
+ def test_all_version_receipts_and_counters_are_required(self):
+ status, expected = self.fixture()
+ status["outcome"]["objects"][0]["disposition"]["state"] = "repaired"
+ status["outcome"]["counters"].update(healed=1, unchanged=3)
+ validate_version_receipts(status, "fixture", expected, 2, required_repairs={("object", "old")})
+ for kind in ("missing-marker", "duplicate", "wrong-version", "unknown", "incarnation", "boolean-counter", "healthy-fault"):
+ bad = copy.deepcopy(status)
+ objects = bad["outcome"]["objects"]
+ if kind == "missing-marker": objects.pop()
+ elif kind == "duplicate": objects[-1] = copy.deepcopy(objects[0])
+ elif kind == "wrong-version": objects[0]["identity"]["versionId"] = "another"
+ elif kind == "unknown": objects[0]["disposition"]["state"] = "unknown"
+ elif kind == "incarnation": objects[0]["identity"]["bucketIncarnationId"] = "other"
+ elif kind == "boolean-counter": bad["outcome"]["counters"]["healed"] = True
+ else: objects[0]["disposition"]["state"] = "verified_healthy"
+ with self.subTest(kind=kind), self.assertRaises(ValueError):
+ validate_version_receipts(bad, "fixture", expected, 2, required_repairs={("object", "old")})
+
+ def test_repeat_requires_mode_specific_health_and_zero_repairs(self):
+ status, expected = self.fixture()
+ validate_version_receipts(status, "fixture", expected, 2)
+ with self.assertRaises(ValueError): validate_version_receipts(status, "fixture", expected, 1)
+ for entry in status["outcome"]["objects"]: entry["disposition"]["state"] = "metadata_healthy"
+ validate_version_receipts(status, "fixture", expected, 1)
+ with self.assertRaises(ValueError): validate_version_receipts(status, "fixture", expected, 2)
+ status["outcome"]["objects"][0]["disposition"]["state"] = "repaired"
+ status["outcome"]["counters"].update(healed=1, unchanged=3)
+ with self.assertRaises(ValueError): validate_version_receipts(status, "fixture", expected, 1)
+
+ def test_admin_nil_uuid_maps_only_to_the_acknowledged_s3_null_version(self):
+ status, expected = self.fixture()
+ expected[0]["versionId"] = "null"
+ status["outcome"]["objects"][0]["identity"]["versionId"] = "00000000-0000-0000-0000-000000000000"
+ validate_version_receipts(status, "fixture", expected, 2)
+ wrong = copy.deepcopy(status)
+ wrong["outcome"]["objects"][0]["identity"]["versionId"] = "00000000-0000-0000-0000-000000000001"
+ with self.assertRaises(ValueError):
+ validate_version_receipts(wrong, "fixture", expected, 2)
+ expected[0]["versionId"] = "old"
+ with self.assertRaises(ValueError):
+ validate_version_receipts(status, "fixture", expected, 2)
+
+ def test_metadata_repair_may_repair_all_receipts_but_marker_cannot_claim_deep_payload(self):
+ status, expected = self.fixture()
+ with self.assertRaisesRegex(ValueError, "never attributed"):
+ validate_version_receipts(status, "fixture", expected, 2, metadata_repairs={"object"})
+ for entry in status["outcome"]["objects"]: entry["disposition"]["state"] = "repaired"
+ status["outcome"]["counters"].update(healed=4, unchanged=0)
+ validate_version_receipts(status, "fixture", expected, 2, metadata_repairs={"object"})
+ status["outcome"]["objects"][-1]["disposition"]["state"] = "verified_healthy"
+ status["outcome"]["counters"].update(healed=3, unchanged=1)
+ with self.assertRaises(ValueError):
+ validate_version_receipts(status, "fixture", expected, 2, metadata_repairs={"object"})
+
+ def test_version_fault_plan_uses_ack_versions_distinct_disks_and_exact_recursive_scope(self):
+ original = HealShardOracleTest().snapshot()
+ observation = original["node1"]
+ templates = {entry["disk"]: entry for entry in observation["files"] if "metadata" in entry}
+ shards = {entry["disk"]: entry for entry in observation["files"] if entry["path"].endswith("part.1")}
+ snapshot = {"node1": {"formats": observation["formats"], "files": []}}
+ bucket, expected = "rv-heal-0123456789abcdef", []
+ for key_index in range(4):
+ key = f"objects/{key_index:04d}.bin"
+ version_ids = [f"00000000-0000-0000-0000-{key_index * 3 + index + 1:012d}" for index in range(3)]
+ for index, version in enumerate(version_ids):
+ expected.append({"key": key, "versionId": version, "deleteMarker": False,
+ "isLatest": index == 2 and key_index % 2 == 1})
+ if key_index % 2 == 0:
+ expected.append({"key": key, "versionId": "marker-" + str(key_index), "deleteMarker": True, "isLatest": True})
+ for disk in range(4):
+ meta = copy.deepcopy(templates[disk])
+ meta["path"] = f"{bucket}/{key}/xl.meta"
+ meta["metadata"]["key"] = key
+ version_template = meta["metadata"]["versions"][0]
+ meta["metadata"]["versions"] = []
+ for index, version in enumerate(version_ids):
+ value = copy.deepcopy(version_template)
+ value.update(versionId=version, dataDir=version, isLatest=index == 2 and key_index % 2 == 1)
+ meta["metadata"]["versions"].append(value)
+ shard = copy.deepcopy(shards[disk])
+ shard.update(path=f"{bucket}/{key}/{version}/part.1", sha256=f"{key_index}-{index}-{disk}")
+ snapshot["node1"]["files"].append(shard)
+ snapshot["node1"]["files"].append(meta)
+ for branch in VERSION_BRANCHES["heal.historical-versions"]:
+ plan = branch_plan("heal.historical-versions", branch, snapshot, bucket, expected, "DV")
+ self.assertTrue(plan["recursive"])
+ if branch == "recursive-all-versions":
+ self.assertEqual(plan["scope"]["key"], "")
+ self.assertEqual(plan["expected"], expected)
+ self.assertTrue(any(item["relativePath"].endswith("xl.meta") for item in plan["faults"]))
+ if len(plan["faults"]) == 2:
+ self.assertEqual(len({(item["node"], item["disk"]) for item in plan["faults"]}), 2)
+ plan = branch_plan("heal.repeat-idempotency", "versioned-repeat", snapshot, bucket, expected, "DV")
+ self.assertEqual(len(plan["expected"]), 4)
+ self.assertEqual(len(plan["proofs"]), 3)
+ self.assertTrue(any(entry["deleteMarker"] for entry in plan["expected"]))
+ plan = branch_plan("heal.below-read-quorum", "r-minus-1-historical", snapshot, bucket, expected, "DV")
+ self.assertEqual(plan["readQuorum"], 2)
+ self.assertEqual(len(plan["retainedShards"]), 1)
+ self.assertEqual(len(plan["faults"]), 3)
+ self.assertEqual(len({entry["versionId"] for entry in plan["faults"]}), 1)
+ self.assertTrue(all(entry["versionId"] == "00000000-0000-0000-0000-000000000004" for entry in plan["faults"]))
+
+
+class HealNodeOutageTest(unittest.TestCase):
+ def snapshot(self):
+ bucket = "rv-heal-0123456789abcdef"
+ identities = ["00000000-0000-0000-0000-" + f"{index:012d}" for index in range(1, 17)]
+ expected = [{"key": "new.bin", "versionId": "new-version", "deleteMarker": False, "isLatest": False},
+ {"key": "new.bin", "versionId": "marker-version", "deleteMarker": True, "isLatest": True}]
+ observation = {}
+ data_dir = "00000000-0000-0000-0000-000000000100"
+ for node_index in range(4):
+ node = "node" + str(node_index + 1)
+ value = {"formats": [], "files": []}
+ for disk in range(4):
+ slot = node_index * 4 + disk
+ value["formats"].append({"disk": disk, "value": {"format": "xl", "id": "pool",
+ "xl": {"sets": [identities], "this": identities[slot]}}})
+ version = {"versionId": "new-version", "deleted": False, "isLatest": False, "inline": None,
+ "dataDir": data_dir, "size": 1048623, "usesLegacyChecksum": False,
+ "role": "data" if slot < 8 else "parity", "parts": [{"number": 1, "size": 1048623, "actualSize": 1048623}],
+ "erasure": {"data_blocks": 8, "parity_blocks": 8, "index": slot + 1,
+ "distribution": list(range(1, 17)), "block_size": 1048576, "algorithm": "rs-vandermonde"}}
+ marker = {"versionId": "marker-version", "deleted": True, "isLatest": True}
+ value["files"].extend([
+ {"disk": disk, "path": f"{bucket}/new.bin/xl.meta", "sha256": "meta" + str(slot), "size": 1000,
+ "metadata": {"bucket": bucket, "key": "new.bin", "versions": [version, marker]}},
+ {"disk": disk, "path": f"{bucket}/new.bin/{data_dir}/part.1", "sha256": "shard" + str(slot), "size": 131200}])
+ observation[node] = value
+ return bucket, expected, observation
+
+ def test_mrf_profile_is_explicit_and_cannot_enable_unrelated_background_sources(self):
+ self.assertEqual(runtime_config("manual"), BACKGROUND_CONFIG)
+ legacy = runtime_config("frozen-race-legacy")
+ self.assertEqual(legacy["RUSTFS_SHARD_INTEGRITY_WRITE"], "false")
+ self.assertEqual(legacy["RUSTFS_SHARD_INTEGRITY_FLEET_CONFIRMED"], "false")
+ self.assertEqual(legacy["RUSTFS_PUT_RENAME_EARLY_ACK_ENABLE"], "false")
+ self.assertEqual(legacy["RUSTFS_HEAL_MAX_CONCURRENT_HEALS"], "1")
+ self.assertEqual(legacy["RUST_LOG"], "info")
+ replay = runtime_config("offline-replay")
+ self.assertEqual(replay.pop("RUSTFS_HEAL_MRF_ENABLE"), "true")
+ self.assertEqual(replay, {key: value for key, value in BACKGROUND_CONFIG.items() if key != "RUSTFS_HEAL_MRF_ENABLE"})
+ with self.assertRaises(ValueError): runtime_config("arbitrary")
+
+ def test_outage_calculates_same_set_quorum_and_rejects_unrecognized_write_policy(self):
+ bucket, expected, snapshot = self.snapshot()
+ target = map_target(snapshot, bucket, "new.bin", "data", 1, "new-version", latest_required=False)
+ contract = {"recognized": True, "writeQuorumRule": "data-plus-one-if-equal-parity"}
+ plan = quorum_plan(target, "node4", contract)
+ self.assertEqual((plan["lostSameSetDrives"], plan["remainingSameSetDrives"], plan["readQuorum"], plan["writeQuorum"]), (4, 12, 8, 9))
+ for changed in ({**contract, "writeQuorumRule": None}, {**contract, "recognized": False}):
+ with self.assertRaises(ValueError): quorum_plan(target, "node4", changed)
+ with self.assertRaises(ValueError): quorum_plan(target, "foreign-node", contract)
+ changed = copy.deepcopy(target)
+ changed["logical"]["erasure"].update(data_blocks=13, parity_blocks=3)
+ with self.assertRaises(ValueError): quorum_plan(changed, "node4", contract)
+
+ def test_ack_manifest_preserves_history_and_rejects_ambiguous_put_delete_receipts(self):
+ old = [{"key": "key", "versionId": "v1", "deleteMarker": False, "isLatest": False},
+ {"key": "key", "versionId": "v2", "deleteMarker": False, "isLatest": True}]
+ updated = put_ack(old, "key", b"new bytes", {"VersionId": "v3"}, "DV")
+ self.assertEqual([entry["versionId"] for entry in updated if entry["isLatest"]], ["v3"])
+ self.assertEqual(old[-1]["isLatest"], True)
+ marked = delete_ack(updated, "key", {"VersionId": "marker", "DeleteMarker": True}, "DV")
+ self.assertEqual([entry["versionId"] for entry in marked if entry["isLatest"]], ["marker"])
+ deleted = delete_ack(marked, "key", {"VersionId": "v1"}, "DV", version="v1")
+ self.assertEqual({entry["versionId"] for entry in deleted}, {"v2", "v3", "marker"})
+ for response in ({}, {"VersionId": "null"}, {"VersionId": "v2"}):
+ with self.subTest(response=response), self.assertRaises(ValueError): put_ack(old, "key", b"x", response, "DV")
+ for response in ({}, {"VersionId": "marker", "DeleteMarker": False}):
+ with self.subTest(response=response), self.assertRaises(ValueError): delete_ack(updated, "key", response, "DV")
+ with self.assertRaises(ValueError): delete_ack(old, "key", {"VersionId": "v2"}, "DV", version="v2")
+ unversioned = [{"key": "key", "versionId": "null", "deleteMarker": False, "isLatest": True}]
+ self.assertEqual(len(put_ack(unversioned, "key", b"x", {}, "DB")), 1)
+ self.assertEqual(delete_ack(unversioned, "key", {}, "DB"), [])
+
+ def test_physical_census_requires_each_drive_and_preserves_surviving_authority(self):
+ bucket, expected, repaired = self.snapshot()
+ offline = copy.deepcopy(repaired)
+ offline["node4"]["files"] = []
+ self.assertEqual(len(replica_census(repaired, bucket, expected, offline)), 1)
+ for poison in ("missing-one-replica", "missing-marker", "resurrected-version", "changed-survivor", "role-mismatch"):
+ changed = copy.deepcopy(repaired)
+ if poison == "missing-one-replica": changed["node4"]["files"].pop(0)
+ elif poison == "missing-marker": changed["node4"]["files"][0]["metadata"]["versions"].pop()
+ elif poison == "resurrected-version": changed["node4"]["files"][0]["metadata"]["versions"].append({"versionId": "deleted-version", "deleted": False, "isLatest": False})
+ elif poison == "changed-survivor": changed["node1"]["files"][1]["sha256"] = "different"
+ else: changed["node4"]["files"][0]["metadata"]["versions"][0]["role"] = "data"
+ with self.subTest(poison=poison), self.assertRaises(ValueError): replica_census(changed, bucket, expected, offline)
+
+ def test_explicit_h1_census_keeps_each_drive_version_and_surviving_shard_checks(self):
+ bucket, expected, h2 = self.snapshot()
+ h1 = {"node1": copy.deepcopy(h2["node1"])}
+ identities = [row["value"]["xl"]["this"] for row in h1["node1"]["formats"]]
+ for row in h1["node1"]["formats"]: row["value"]["xl"]["sets"] = [identities]
+ for row in h1["node1"]["files"]:
+ for version in (row.get("metadata") or {}).get("versions", []):
+ if version["deleted"]: continue
+ version["role"] = "data" if row["disk"] < 2 else "parity"
+ version["erasure"].update(data_blocks=2, parity_blocks=2, distribution=[1, 2, 3, 4])
+ self.assertEqual(len(replica_census(h1, bucket, expected, h1, topology="H1")), 1)
+ with self.assertRaisesRegex(ValueError, "wrong node count"):
+ replica_census(h1, bucket, expected, h1)
+ with self.assertRaisesRegex(ValueError, "wrong node count"):
+ replica_census(h2, bucket, expected, h2, topology="H1")
+ for poison in ("missing-drive", "missing-marker", "changed-survivor", "role-mismatch"):
+ changed = copy.deepcopy(h1)
+ if poison == "missing-drive": changed["node1"]["files"] = [row for row in changed["node1"]["files"] if row["disk"] != 3]
+ elif poison == "missing-marker": changed["node1"]["files"][0]["metadata"]["versions"].pop()
+ elif poison == "changed-survivor": changed["node1"]["files"][1]["sha256"] = "different"
+ else: changed["node1"]["files"][4]["metadata"]["versions"][0]["role"] = "data"
+ with self.subTest(poison=poison), self.assertRaises(ValueError):
+ replica_census(changed, bucket, expected, h1, topology="H1")
+
+ def test_candidate_write_quorum_recognition_is_independent_of_read_contract(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ (root / "rustfs/src").mkdir(parents=True)
+ (root / "crates/ecstore/src/set_disk").mkdir(parents=True)
+ (root / "rustfs/src/error.rs").write_text('const SLOW_DOWN_READ_CODE: &str = "SlowDownRead";\n'
+ 'StorageError::ErasureReadQuorum | StorageError::InsufficientReadQuorum(_, _) => { S3ErrorCode::Custom(SLOW_DOWN_READ_CODE.into()) }\n'
+ 'S3ErrorCode::Custom(custom) if &**custom == SLOW_DOWN_READ_CODE => Some(StatusCode::SERVICE_UNAVAILABLE)')
+ metadata = root / "crates/ecstore/src/set_disk/metadata.rs"
+ source = 'pub(super) fn object_quorum_from_meta() { let data_blocks = parts_metadata.len() as i32 - parity_blocks; let write_quorum = if data_blocks == parity_blocks { data_blocks + 1 } else { data_blocks }; Ok((data_blocks, write_quorum)) }'
+ metadata.write_text(source)
+ self.assertEqual(quorum_contract(root, "a" * 40)["writeQuorumRule"], "data-plus-one-if-equal-parity")
+ metadata.write_text(source.replace("data_blocks + 1", "data_blocks + 2"))
+ changed = quorum_contract(root, "a" * 40)
+ self.assertTrue(changed["recognized"])
+ self.assertIsNone(changed["writeQuorumRule"])
+ metadata.write_text(source.replace("Ok((data_blocks, write_quorum))", "let write_quorum = write_quorum + 1; Ok((data_blocks, write_quorum))"))
+ self.assertIsNone(quorum_contract(root, "a" * 40)["writeQuorumRule"])
+
+ def test_deleted_identity_requires_exact_s3_absence_without_payload_or_transport_ambiguity(self):
+ for version, code in (("null", "NoSuchKey"), ("deleted-version", "NoSuchVersion")):
+ good = {"acknowledged": False, "resultUnknown": False,
+ "response": {"ResponseMetadata": {"HTTPStatusCode": 404}, "Error": {"Code": code}}}
+ validate_deleted_read(good, version)
+ for changed in ({**good, "acknowledged": True}, {**good, "resultUnknown": True}, {**good, "payload": {}},
+ {**good, "response": {"ResponseMetadata": {"HTTPStatusCode": 403}, "Error": {"Code": "AccessDenied"}}}):
+ with self.subTest(version=version), self.assertRaises(ValueError): validate_deleted_read(changed, version)
+
+
+class HealDriveGateTest(unittest.TestCase):
+ def test_permission_gate_release_keeps_same_process_before_fresh_repair_token(self):
+ process = {name: "candidate-" + name for name in ("pid", "startTicks", "bootId", "unit",
+ "executable", "binarySha256", "mountNamespace", "commandSha256", "environmentFileSha256", "settings")}
+ process["pid"] = 4242
+ process_before = {**process, "state": "S"}
+ process_after = {**process, "state": "R"}
+ before = {"node1": {"files": [{"path": "bucket/objects/0000.bin/part.1", "sha256": "before"}],
+ "formats": [{"value": {"id": "format"}}]}}
+ after = copy.deepcopy(before)
+ after["node1"].update(pid=4242, binarySha256=process["binarySha256"])
+ events = []
+
+ def remote(node, action):
+ events.append(action)
+ if action == "drive-gate-release-live":
+ return {"sameOriginalProcess": True, "serviceRestarted": False,
+ "processBefore": process_before, "processAfter": process_after,
+ "originalPermissionsRestored": True}
+ if action == "process":
+ return {**process, "state": "D"}
+ raise AssertionError(action)
+
+ fleet = types.SimpleNamespace(drive_gates={"node1"}, remote=remote,
+ record=lambda label, value: events.append(label) or value,
+ snapshot=lambda label, **kwargs: after)
+ proof = release_permission_gate_live(fleet, "node1", before, {})
+ self.assertEqual(events, ["drive-gate-release-live", "drive-gate-released-live", "process",
+ "process-after-live-gate-release-before-snapshot", "process",
+ "process-after-live-gate-release-after-snapshot"])
+ self.assertEqual(fleet.drive_gates, set())
+ self.assertTrue(proof["physicalUnchangedUntilFreshToken"])
+ self.assertTrue(proof["processBoundSnapshot"])
+
+ def test_live_permission_activation_follows_actual_start_and_initial_readiness(self):
+ with tempfile.TemporaryDirectory() as directory:
+ fleet = Fleet(["node1"], Path(directory), "gh-12345-1-h1")
+ fleet.drive_gates.add("node1")
+ calls = []
+ def remote(node, action, **kwargs):
+ calls.append(action)
+ return {"mode": "permission-denied", "phase": "armed"} if action == "gate-state" else {}
+ with patch.object(fleet, "remote", side_effect=remote), \
+ patch.object(fleet, "wait_ready", side_effect=lambda: calls.append("ready")):
+ fleet.start()
+ self.assertEqual(calls, ["start", "ready", "gate-state", "drive-gate-activate"])
+ calls.clear()
+ with patch.object(fleet, "remote", side_effect=remote), \
+ patch.object(fleet, "wait_ready", side_effect=ValueError("startup failed")), self.assertRaises(ValueError):
+ fleet.start()
+ self.assertNotIn("drive-gate-activate", calls)
+
+ def test_partial_live_activation_restores_from_original_armed_marker(self):
+ with tempfile.TemporaryDirectory() as directory:
+ state, root = Path(directory) / "state", Path(directory) / "root"
+ state.mkdir()
+ (root / "data").mkdir(parents=True, mode=0o700)
+ original = Path.lstat
+ def root_owned(path):
+ info = original(path)
+ return types.SimpleNamespace(st_mode=info.st_mode, st_uid=0, st_nlink=info.st_nlink,
+ st_size=info.st_size, st_dev=info.st_dev, st_ino=info.st_ino)
+ with patch.object(Path, "lstat", root_owned):
+ apply_gate(state, (root,), 0, "permission-denied")
+ with patch("scripts.validation_heal.drive_gate.os.replace", side_effect=OSError("interrupted")), self.assertRaises(OSError):
+ activate_gate(state, (root,))
+ self.assertEqual(checked_gate(state, (root,))["phase"], "armed")
+ self.assertEqual(stat.S_IMODE((root / "data").stat().st_mode), 0)
+ self.assertTrue(release_gate(state, (root,))["originalPermissionsRestored"])
+ self.assertEqual(stat.S_IMODE((root / "data").stat().st_mode), 0o700)
+
+ def test_gate_only_changes_owned_permissions_and_restores_partial_intent(self):
+ for mode in ("read-only", "permission-denied"):
+ with self.subTest(mode=mode), tempfile.TemporaryDirectory() as directory:
+ state = Path(directory) / "state"
+ root = Path(directory) / "root"
+ state.mkdir()
+ (root / "data").mkdir(parents=True, mode=0o700)
+ other = root / "quarantine"
+ other.mkdir(mode=0o700)
+ (root / "data" / "original").write_bytes(b"owned-data")
+ original_lstat = Path.lstat
+ def root_owned(path):
+ observed = original_lstat(path)
+ return types.SimpleNamespace(st_mode=observed.st_mode, st_uid=0, st_nlink=observed.st_nlink,
+ st_size=observed.st_size, st_dev=observed.st_dev, st_ino=observed.st_ino)
+ with patch.object(Path, "lstat", root_owned):
+ gate = apply_gate(state, (root,), 0, mode)
+ self.assertEqual(checked_gate(state, (root,))["mode"], mode)
+ self.assertFalse(gate["hostMountRemounted"])
+ self.assertEqual(stat.S_IMODE((root / "data").stat().st_mode), 0o700)
+ if mode == "permission-denied":
+ self.assertEqual(gate["phase"], "armed")
+ activated = activate_gate(state, (root,))
+ self.assertTrue(activated["injectedAfterActualServiceStart"])
+ self.assertEqual(stat.S_IMODE((root / "data").stat().st_mode), 0)
+ self.assertEqual(checked_gate(state, (root,))["phase"], "active")
+ with self.assertRaises(ValueError): activate_gate(state, (root,))
+ self.assertEqual(stat.S_IMODE(other.stat().st_mode), 0o700)
+ with self.assertRaisesRegex(ValueError, "already active"):
+ apply_gate(state, (root,), 0, mode)
+ props = start_properties(gate)
+ self.assertIn("--property=PrivateMounts=true", props)
+ self.assertIn("--property=ReadOnlyPaths=" + str(root / "data") if mode == "read-only" else
+ "--property=CapabilityBoundingSet=~CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH", props)
+ self.assertTrue(release_gate(state, (root,))["originalPermissionsRestored"])
+ self.assertEqual(stat.S_IMODE((root / "data").stat().st_mode), 0o700)
+ self.assertEqual((root / "data" / "original").read_bytes(), b"owned-data")
+ self.assertFalse((state / "drive-gate.json").exists())
+ self.assertTrue(release_gate(state, (root,))["gateWasAbsent"])
+
+ def test_gate_refusal_proof_requires_same_uid_gid_errno_and_no_root_bypass(self):
+ identity = {"uid": 0, "gid": 0, "capEff": 0}
+ for mode, error in (("read-only", errno.EROFS), ("permission-denied", errno.EACCES)):
+ good = {"uid": 0, "gid": 0, "errno": error}
+ self.assertTrue(validate_probe(good, mode, identity)["sameServiceIdentity"])
+ for changed in ({**good, "errno": 0}, {**good, "errno": errno.ENOSPC}, {**good, "uid": 1000}, {**good, "gid": 1000}):
+ with self.assertRaises(ValueError): validate_probe(changed, mode, identity)
+ with self.assertRaisesRegex(ValueError, "bypasses"):
+ validate_probe({"uid": 0, "gid": 0, "errno": errno.EACCES}, "permission-denied", {**identity, "capEff": 1 << 1})
+
+ def test_unwritable_receipt_never_accepts_repaired_or_hidden_failures(self):
+ status = HealHealthyOracleTest().receipt("failed")
+ bucket, entry = "rv-heal-drive-unit", {"key": "target.bin", "versionId": "null"}
+ receipt = status["outcome"]["objects"][0]
+ receipt["identity"].update(bucket=bucket, object=entry["key"], versionId=None)
+ receipt["disposition"]["details"] = "permanent"
+ status["outcome"]["execution"]["state"] = "completed_with_errors"
+ status["outcome"]["counters"].update(healed=0, unchanged=0, failed=1, attemptFailures=1)
+ self.assertFalse(validate_unwritable(status, bucket, entry)["repairClaimedWhileUnwritable"])
+ stopped = {**copy.deepcopy(status), "summary": "stopped"}
+ validate_unwritable(stopped, bucket, entry)
+ stopped["outcome"]["execution"]["state"] = "aborted"
+ with self.assertRaises(ValueError): validate_unwritable(stopped, bucket, entry)
+ for invalid in ("repaired", "metadata_healthy", "unknown", "wrong-version", "healed-count", "hidden-failure", "incarnation"):
+ changed = copy.deepcopy(status)
+ if invalid in {"repaired", "metadata_healthy", "unknown"}: changed["outcome"]["objects"][0]["disposition"]["state"] = invalid
+ elif invalid == "wrong-version": changed["outcome"]["objects"][0]["identity"]["versionId"] = "foreign"
+ elif invalid == "healed-count": changed["outcome"]["counters"]["healed"] = 1
+ elif invalid == "hidden-failure": changed["outcome"]["execution"]["state"] = "completed"
+ else: changed["outcome"]["objects"][0]["identity"].pop("bucketIncarnationId")
+ with self.subTest(invalid=invalid), self.assertRaises(ValueError): validate_unwritable(changed, bucket, entry)
+ deferred = copy.deepcopy(status)
+ deferred["outcome"]["execution"]["state"] = "completed"
+ deferred["outcome"]["counters"].update(failed=0, skipped=1)
+ deferred["outcome"]["objects"][0]["disposition"] = {"state": "deferred", "details": {"reason": "deadline"}}
+ validate_unwritable(deferred, bucket, entry)
+ deferred["outcome"]["objects"][0]["disposition"]["details"]["reason"] = "dangling_delete_grace"
+ with self.assertRaises(ValueError): validate_unwritable(deferred, bucket, entry)
+
+ def test_gate_cleanup_precedes_fault_rescue_and_preserves_namespace_on_failure(self):
+ with tempfile.TemporaryDirectory() as directory:
+ fleet = Fleet(["node1"], Path(directory), "gh-12345-1-h1")
+ fleet.reserved.append("node1")
+ fleet.drive_gates.add("node1")
+ fleet.faults.append(("node1", {"disk": 1, "faultId": "B02-test"}))
+ calls = []
+ def remote(node, action, **params):
+ calls.append(action)
+ if action == "drive-gate": raise ValueError("gate root drifted")
+ return {}
+ with patch.object(fleet, "remote", side_effect=remote): self.assertFalse(fleet.cleanup())
+ self.assertNotIn("recover-fault", calls)
+ self.assertNotIn("cleanup", calls)
+ self.assertTrue(fleet.drive_gates)
+
+
+class HealReadRepairTest(unittest.TestCase):
+ def event(self, **changes):
+ return {"level": "DEBUG", "target": ADMISSION_TARGET, "message": ADMISSION_MESSAGE,
+ "bucket": "rv-heal-read-unit", "object": "target.bin", "part_number": 1,
+ "pool_index": 0, "set_index": 0, "request_id": "automatic-task", "admission": "accepted", **changes}
+
+ def test_source_requires_structured_exact_scope_and_new_task_identity(self):
+ event = self.event()
+ self.assertEqual(source_tasks({"node1": {"journal": "journal-prefix " + json.dumps(event)}}, event["bucket"], event["object"]),
+ {("node1", "automatic-task")})
+ for changed in ({**event, "target": "unrelated"}, {**event, "bucket": "another-bucket"},
+ {**event, "object": "other-version-key"}, {**event, "message": "Read-repair heal request not admitted"}):
+ self.assertFalse(source_tasks({"node1": {"journal": json.dumps(changed)}}, event["bucket"], event["object"]))
+ self.assertFalse(source_tasks({"node1": {"journal": str(event)}}, event["bucket"], event["object"]))
+ for name, value in (("admission", "merged"), ("pool_index", 1), ("set_index", True), ("part_number", 2), ("request_id", "")):
+ with self.subTest(field=name), self.assertRaises(ValueError):
+ source_tasks({"node1": {"journal": json.dumps({**event, name: value})}}, event["bucket"], event["object"])
+
+ def test_read_bytes_version_and_server_request_id_are_required(self):
+ entry = {"versionId": "historical", "payload": {"size": 3, "sha256": hashlib.sha256(b"abc").hexdigest()}}
+ good = {"acknowledged": True, "payload": entry["payload"], "response": {"VersionId": "historical",
+ "ResponseMetadata": {"HTTPStatusCode": 200, "RequestId": "server-read"}}}
+ self.assertEqual(validate_read(good, entry), "server-read")
+ for invalid in ("version", "bytes", "request-id", "transport", "status"):
+ changed = copy.deepcopy(good)
+ if invalid == "version": changed["response"]["VersionId"] = "latest"
+ elif invalid == "bytes": changed["payload"]["sha256"] = "not-acknowledged"
+ elif invalid == "request-id": changed["response"]["ResponseMetadata"].pop("RequestId")
+ elif invalid == "transport": changed["resultUnknown"] = True
+ else: changed["response"]["ResponseMetadata"]["HTTPStatusCode"] = 206
+ with self.subTest(invalid=invalid), self.assertRaises(ValueError): validate_read(changed, entry)
+ ranged = {**good, "payload": {"size": 2, "sha256": hashlib.sha256(b"bc").hexdigest()},
+ "response": {**good["response"], "ContentRange": "bytes 1-2/3",
+ "ResponseMetadata": {"HTTPStatusCode": 206, "RequestId": "server-range"}}}
+ validate_read(ranged, entry, expected_range=(1, 2), body=b"abc")
+ with self.assertRaises(ValueError): validate_read(ranged, entry, expected_range=(0, 1), body=b"abc")
+
+ def execute(self, *, stale_only=False):
+ bucket, key, calls = "rv-heal-read-unit", "target.bin", []
+ entry = {"key": key, "versionId": "historical", "payload": {"size": 3, "sha256": hashlib.sha256(b"abc").hexdigest()}}
+ fresh = self.event()
+ fleet = types.SimpleNamespace(runtime_profile="read-repair", nodes=["host-one"],
+ record=lambda label, value: value)
+ window = types.SimpleNamespace(capture=lambda: {"host-one": [] if stale_only else [fresh]})
+ def read(operation, params):
+ calls.append((operation, params))
+ return {"acknowledged": True, "payload": entry["payload"], "response": {"VersionId": "historical",
+ "ResponseMetadata": {"HTTPStatusCode": 200, "RequestId": "server-read"}}}
+ def status(path, query, body):
+ calls.append((path, query, body))
+ self.assertEqual(query, [("clientToken", "automatic-task")])
+ self.assertEqual(body, b"")
+ return {"httpStatus": 200, "response": {"summary": "finished", "source-receipt": "validated-by-physical-driver"}}
+ client = types.SimpleNamespace(binding={}, s3=read)
+ observer = types.SimpleNamespace(admin=status)
+ ticks = [0, 1, 181] if stale_only else [0, 1]
+ with patch("scripts.validation_heal.read_repair.Probe", return_value=observer) as factory, \
+ patch("scripts.validation_heal.read_repair.JournalWindow", return_value=window) as journal, \
+ patch("scripts.validation_heal.read_repair.time.monotonic", side_effect=ticks), \
+ patch("scripts.validation_heal.read_repair.time.sleep"):
+ if stale_only:
+ with self.assertRaisesRegex(ValueError, "ATTRIBUTION_NOT_OBSERVED"):
+ tracked_read_task(client, fleet, bucket, "historical-version-read", entry)
+ factory.assert_not_called()
+ else:
+ self.assertEqual(tracked_read_task(client, fleet, bucket, "historical-version-read", entry)["summary"], "finished")
+ factory.assert_called_once_with({}, "node1")
+ journal.assert_called_once_with(fleet, "read-source", event_targets={ADMISSION_TARGET})
+ self.assertEqual(sum(call[0] == "get_object" for call in calls), 1)
+ self.assertEqual(calls[0][1], {"Bucket": bucket, "Key": key, "VersionId": "historical"})
+ self.assertTrue(all(len(call) == 2 or call[1] == [("clientToken", "automatic-task")] for call in calls))
+
+ def test_only_one_target_read_and_observation_of_the_fresh_automatic_task(self):
+ self.execute()
+ self.execute(stale_only=True)
+
+ def test_complete_cursor_window_finds_admission_before_more_than_2000_tail_lines(self):
+ event = self.event()
+ def remote(node, action, **params):
+ self.assertEqual(action, "logs")
+ if params.get("checkpoint"):
+ return {"complete": True, "afterCursor": "before-read"}
+ self.assertEqual(params, {"afterCursor": "before-read"})
+ return {"complete": True, "beforeCursor": "before-read", "afterCursor": "after-read",
+ "hasMore": False, "journal": "\n".join([json.dumps(event)] + ['{"target":"unrelated"}'] * 2100)}
+ fleet = types.SimpleNamespace(nodes=["host-one"], remote=remote, record=lambda label, value: value)
+ window = JournalWindow(fleet, "read-source", event_targets={ADMISSION_TARGET})
+ events = window.capture()
+ self.assertEqual(events, {"host-one": [event]})
+ self.assertEqual(source_tasks({node: {"journal": "\n".join(json.dumps(row) for row in rows)}
+ for node, rows in events.items()}, event["bucket"], event["object"]),
+ {("host-one", "automatic-task")})
+
+ def test_read_profile_keeps_other_repair_sources_disabled(self):
+ profile = runtime_config("read-repair")
+ logging_filter = profile.pop("RUSTFS_OBS_LOGGER_LEVEL")
+ self.assertEqual(profile.pop("RUST_LOG"), logging_filter)
+ self.assertEqual(logging_filter.split(","), ["info", "rustfs_ecstore=debug", "rustfs::heal=debug"])
+ self.assertEqual(profile, {key: value for key, value in BACKGROUND_CONFIG.items() if key != "RUSTFS_OBS_LOGGER_LEVEL"})
+ snapshot, bucket, expected = versioned_scene(["objects/0001.bin"])
+ plan = branch_plan("heal.read-repair-attribution", "historical-version-read", snapshot, bucket, expected, "DV")
+ self.assertEqual(plan["expected"][0]["versionId"], "v0")
+ self.assertFalse(plan["expected"][0]["isLatest"])
+ self.assertEqual(len(plan["expected"]), 1)
+
+
+class HealScannerSourceTest(unittest.TestCase):
+ bucket, key = "rv-heal-scanner-unit", "objects/0008.bin"
+
+ def events(self):
+ def row(second, **values):
+ return {"timestamp": f"2026-10-10T12:00:{second:02d}Z", **values}
+ return [
+ row(1, target="rustfs::scanner", event="scanner_cycle_state", state="started", cycle=7, scan_mode="Deep"),
+ row(2, target="rustfs::scanner::folder", event="scanner_heal_admission", state="request_started",
+ bucket=self.bucket, object=self.key, version_id="00000000-0000-0000-0000-000000000000"),
+ row(3, target="rustfs::heal::channel", state="start_received", request_id="task-one",
+ bucket=self.bucket, object_prefix=self.key),
+ row(4, target="rustfs::heal::manager", message="Heal queue request accepted", request_id="task-one",
+ source="scanner", result="accepted"),
+ row(5, target="rustfs::heal::task", message="Heal object started", stage="start", task_id="task-one",
+ bucket=self.bucket, object=self.key, version_id="None"),
+ row(6, target="rustfs::heal::manager", state="task_completed", task_id="task-one"),
+ row(7, target="rustfs::scanner", event="scanner_cycle_state", state="completed", cycle=0, scan_mode="Deep"),
+ ]
+
+ def entry(self):
+ return {"key": self.key, "versionId": "null", "deleteMarker": False}
+
+ def test_source_chain_requires_exact_scanner_admission_version_cycle_and_task(self):
+ rows = self.events()
+ self.assertEqual(scanner_sources({"host": rows}, self.bucket, self.entry())[0]["taskId"], "task-one")
+ mutations = ((1, "bucket", "other"), (1, "object", "other"), (1, "version_id", "historical"),
+ (2, "object_prefix", "other"), (3, "source", "read-repair"), (3, "result", "merged_duplicate"),
+ (4, "task_id", "another"), (4, "version_id", "historical"), (5, "task_id", "another"),
+ (6, "state", "failed"), (0, "scan_mode", "Normal"), (1, "timestamp", "2026-10-10T12:00:08Z"))
+ for index, field, value in mutations:
+ changed = copy.deepcopy(rows)
+ changed[index][field] = value
+ with self.subTest(index=index, field=field):
+ self.assertFalse(scanner_sources({"host": changed}, self.bucket, self.entry()))
+ for index in (1, 4):
+ changed = copy.deepcopy(rows)
+ changed[index].pop("version_id")
+ self.assertFalse(scanner_sources({"host": changed}, self.bucket, self.entry()))
+ self.assertFalse(scanner_sources({"host": rows[:1] + rows[-1:]}, self.bucket, self.entry()))
+ self.assertFalse(scanner_sources({"host": rows[:4], "peer": rows[4:]}, self.bucket, self.entry()))
+
+ def test_task_may_start_after_discovery_cycle_but_cannot_precede_admission(self):
+ rows = self.events()
+ rows[4]["timestamp"] = "2026-10-10T12:00:08Z"
+ rows[5]["timestamp"] = "2026-10-10T12:00:09Z"
+ self.assertTrue(scanner_sources({"host": rows}, self.bucket, self.entry()))
+ rows[4]["timestamp"] = "2026-10-10T12:00:03Z"
+ self.assertFalse(scanner_sources({"host": rows}, self.bucket, self.entry()))
+
+ def remote_events(self):
+ rows = self.events()
+ uri = ("/rustfs/rpc/ns_scanner?ns_scanner_cycle=7&ns_scanner_request_id="
+ "410f652d-2861-4780-913a-539ccab29c47")
+ rows[1].update(request_id="remote-request", spans=[
+ {"name": "http-request", "method": "POST", "request_id": "remote-request", "uri": uri},
+ {"name": "nsscanner_disk", "scan_mode": "Deep"}])
+ return {"worker": rows[1:-1], "leader": [rows[0], rows[-1]]}
+
+ def test_remote_discovery_keeps_owner_and_binds_exact_rpc_cycle(self):
+ events = self.remote_events()
+ tasks = scanner_sources(events, self.bucket, self.entry())
+ self.assertEqual(len(tasks), 1)
+ self.assertEqual(tasks[0]["node"], "worker")
+ self.assertEqual(tasks[0]["cycle"]["node"], "leader")
+ self.assertEqual(tasks[0]["cycle"]["cycle"], 7)
+ self.assertEqual(tasks[0]["cycle"]["remoteRequest"]["requestId"], "remote-request")
+ for uri in ("/rustfs/rpc/other?ns_scanner_cycle=7",
+ "https://other/rustfs/rpc/ns_scanner?ns_scanner_cycle=7",
+ events["worker"][0]["spans"][0]["uri"].replace("cycle=7", "cycle=8"),
+ events["worker"][0]["spans"][0]["uri"] + "&ns_scanner_cycle=7",
+ events["worker"][0]["spans"][0]["uri"].replace("410f652d-2861-4780-913a-539ccab29c47", "invalid")):
+ changed = copy.deepcopy(events)
+ changed["worker"][0]["spans"][0]["uri"] = uri
+ with self.subTest(uri=uri):
+ self.assertFalse(scanner_sources(changed, self.bucket, self.entry()))
+ for field, value in (("request_id", "another"), ("method", "GET")):
+ changed = copy.deepcopy(events)
+ changed["worker"][0]["spans"][0][field] = value
+ self.assertFalse(scanner_sources(changed, self.bucket, self.entry()))
+ changed = copy.deepcopy(events)
+ changed["worker"][0]["spans"][1]["scan_mode"] = "Normal"
+ self.assertFalse(scanner_sources(changed, self.bucket, self.entry()))
+ changed = copy.deepcopy(events)
+ changed["other-leader"] = copy.deepcopy(events["leader"])
+ self.assertFalse(scanner_sources(changed, self.bucket, self.entry()))
+ changed = copy.deepcopy(events)
+ changed["leader"][-1]["state"] = "failed"
+ self.assertFalse(scanner_sources(changed, self.bucket, self.entry()))
+ for spans in (None, "invalid", [None], [{"name": "http-request", "uri": None}]):
+ changed = copy.deepcopy(events)
+ changed["worker"][0]["spans"] = spans
+ with self.subTest(spans=spans):
+ self.assertFalse(scanner_sources(changed, self.bucket, self.entry()))
+
+ def test_remote_boundary_queries_worker_and_counts_leader_cycles(self):
+ events = self.remote_events()
+ events["leader"] += [
+ {**events["leader"][0], "cycle": 8, "timestamp": "2026-10-10T12:00:08Z"},
+ {**events["leader"][1], "timestamp": "2026-10-10T12:00:09Z"}]
+ records = []
+ fleet = types.SimpleNamespace(runtime_profile="scanner", nodes=["worker", "leader"],
+ record=lambda label, value: records.append((label, value)) or value)
+ window = types.SimpleNamespace(capture=lambda: events)
+ status = {"summary": "finished", "outcome": {"objects": [{"disposition": {"state": "repaired"}}]}}
+ observer = types.SimpleNamespace(admin=lambda *args: {"httpStatus": 200, "response": status})
+ with patch("scripts.validation_heal.scanner_repair.Probe", return_value=observer) as probe:
+ observe_scanner(types.SimpleNamespace(binding={}), fleet, window, self.bucket, self.entry(), "scanner-cycle-boundary")
+ probe.assert_called_once_with({}, "node1")
+ self.assertEqual(records[-1][1]["postFaultCycles"], 2)
+ self.assertEqual(records[-1][1]["task"]["cycle"]["node"], "leader")
+
+ def test_cycle_pair_requires_ordered_positive_start_and_complete_boundary(self):
+ rows = self.events()
+ self.assertEqual(cycle_pairs(rows)[0]["cycle"], 7)
+ for change in ({"cycle": 9}, {"timestamp": "2026-10-10T11:00:00Z"}, {"timestamp": "invalid"}):
+ changed = copy.deepcopy(rows)
+ changed[-1].update(change)
+ self.assertFalse(cycle_pairs(changed))
+ failed = {**rows[-1], "state": "failed", "timestamp": "2026-10-10T12:00:06Z"}
+ self.assertFalse(cycle_pairs(rows[:-1] + [failed, rows[-1]]))
+
+ def test_observer_uses_status_only_and_rejects_healthy_substitution_for_repair(self):
+ fleet = types.SimpleNamespace(runtime_profile="scanner", nodes=["host"], record=lambda label, value: value)
+ window = types.SimpleNamespace(capture=lambda: {"host": self.events()})
+ def status(state):
+ return {"summary": "finished", "outcome": {"objects": [{"disposition": {"state": state}}]}}
+ client = types.SimpleNamespace(binding={})
+ calls = []
+ def query(path, args, body):
+ calls.append((path, args, body))
+ return {"httpStatus": 200, "response": status("repaired")}
+ with patch("scripts.validation_heal.scanner_repair.Probe", return_value=types.SimpleNamespace(admin=query)):
+ self.assertEqual(observe_scanner(client, fleet, window, self.bucket, self.entry(), "cold-missing"), status("repaired"))
+ self.assertEqual(calls, [("/rustfs/admin/v3/heal/" + self.bucket + "/" + self.key,
+ [("clientToken", "task-one")], b"")])
+ observer = types.SimpleNamespace(admin=lambda *args: {"httpStatus": 200, "response": status("verified_healthy")})
+ with patch("scripts.validation_heal.scanner_repair.Probe", return_value=observer):
+ with self.assertRaisesRegex(ValueError, "required exact"):
+ observe_scanner(client, fleet, window, self.bucket, self.entry(), "cold-missing")
+ observe_scanner(client, fleet, window, self.bucket, self.entry(), "cold-missing", healthy=True)
+
+ def test_boundary_branch_requires_two_complete_post_fault_cycles(self):
+ fleet = types.SimpleNamespace(runtime_profile="scanner", nodes=["host"], record=lambda label, value: value)
+ window = types.SimpleNamespace(capture=lambda: {"host": self.events()})
+ with patch("scripts.validation_heal.scanner_repair.time.monotonic", side_effect=[0, 241]), \
+ patch("scripts.validation_heal.scanner_repair.Probe") as observer:
+ with self.assertRaisesRegex(ValueError, "ATTRIBUTION_NOT_OBSERVED"):
+ observe_scanner(types.SimpleNamespace(binding={}), fleet, window, self.bucket, self.entry(), "scanner-cycle-boundary")
+ observer.assert_not_called()
+
+ def test_cursor_windows_are_complete_and_old_events_are_excluded(self):
+ calls = []
+ observations = [{"complete": True, "afterCursor": "cursor-1", "journal": json.dumps(self.events()[0])},
+ {"complete": True, "beforeCursor": "cursor-1", "afterCursor": "cursor-2", "journal": json.dumps(self.events()[1])}]
+ def remote(node, action, **params):
+ calls.append(params)
+ return observations.pop(0)
+ fleet = types.SimpleNamespace(nodes=["host"], record=lambda label, value: value, remote=remote)
+ window = JournalWindow(fleet, "unit")
+ self.assertEqual(window.capture(), {"host": [self.events()[1]]})
+ self.assertEqual(calls, [{"checkpoint": True}, {"afterCursor": "cursor-1"}])
+ window.fleet.remote = lambda *args, **kwargs: {"complete": False, "beforeCursor": "cursor-2", "afterCursor": "cursor-3"}
+ with self.assertRaisesRegex(ValueError, "completeness"): window.capture()
+
+ def test_remote_log_window_rejects_truncation_and_invalid_cursor(self):
+ owner = {"unit": "private.service"}
+ with patch("scripts.validation_heal.fleet_node.owned", return_value=(None, None, owner)), \
+ patch("scripts.validation_heal.fleet_node.command") as command:
+ command.return_value = types.SimpleNamespace(stdout=json.dumps({"MESSAGE": '{"message":"fresh"}',
+ "__CURSOR": "s=abc;i=def"}) + "\n")
+ value = node_logs({"runId": "unit", "afterCursor": "s=previous;i=1"})
+ self.assertTrue(value["complete"])
+ self.assertEqual(value["afterCursor"], "s=abc;i=def")
+ self.assertIn("--after-cursor", command.call_args.args)
+ self.assertIn("+10000", command.call_args.args)
+ self.assertIn("--all", command.call_args.args)
+ self.assertFalse(value["hasMore"])
+ with self.assertRaisesRegex(ValueError, "invalid journal cursor"):
+ node_logs({"runId": "unit", "afterCursor": "invalid\n--help"})
+ command.return_value = types.SimpleNamespace(stdout=json.dumps({"MESSAGE": list('完整消息'.encode()),
+ "__CURSOR": "s=abc;i=bytes"}) + "\n")
+ self.assertEqual(node_logs({"runId": "unit", "afterCursor": "s=previous;i=1"})["journal"], '完整消息')
+ command.return_value = types.SimpleNamespace(stdout=json.dumps({"MESSAGE": None, "__CURSOR": "s=abc;i=omitted"}) + "\n")
+ with self.assertRaisesRegex(ValueError, "complete text"): node_logs({"runId": "unit", "afterCursor": "s=previous;i=1"})
+ rows = [json.dumps({"MESSAGE": "row", "__CURSOR": f"s=abc;i={index}"}) for index in range(10000)]
+ command.return_value = types.SimpleNamespace(stdout="\n".join(rows))
+ page = node_logs({"runId": "unit", "afterCursor": "s=previous;i=1"})
+ self.assertTrue(page["hasMore"])
+ self.assertEqual(page["afterCursor"], "s=abc;i=9999")
+ command.return_value = types.SimpleNamespace(stdout="\n".join(rows + [json.dumps({"MESSAGE": "row", "__CURSOR": "s=abc;i=10000"})]))
+ with self.assertRaisesRegex(ValueError, "incomplete"):
+ node_logs({"runId": "unit", "afterCursor": "s=previous;i=1"})
+
+ def test_remote_log_window_retains_suppression_notice_and_refuses_complete_attribution(self):
+ owner = {"unit": "private.service"}
+ for notice in (
+ {"MESSAGE": "Suppressed 893 messages from private.service"},
+ {"MESSAGE": "localized journal loss notice", "MESSAGE_ID": "a596d6fe7bfa4994828e72309e95d61e"}):
+ with self.subTest(notice=notice), patch.object(fleet_node, "owned", return_value=(None, None, owner)), \
+ patch.object(fleet_node, "command") as command:
+ command.return_value = types.SimpleNamespace(stdout=json.dumps({**notice, "__CURSOR": "s=abc;i=loss"}) + "\n")
+ value = node_logs({"runId": "unit", "afterCursor": "s=abc;i=before"})
+ self.assertFalse(value["complete"])
+ self.assertEqual(value["journal"], notice["MESSAGE"])
+ self.assertEqual(value["afterCursor"], "s=abc;i=loss")
+ self.assertTrue(value["sourceMessageLoss"])
+ self.assertIn("MESSAGE_ID", next(arg for arg in command.call_args.args if arg.startswith("--output-fields=")))
+ records = []
+ def remote(node, action, **params):
+ return {"complete": True, "afterCursor": "s=abc;i=before"} if params.get("checkpoint") else value
+ fleet = types.SimpleNamespace(nodes=["host"], remote=remote,
+ record=lambda label, row: records.append(row) or row)
+ window = JournalWindow(fleet, "suppressed", event_targets={"rustfs::heal::manager"})
+ with self.assertRaisesRegex(ValueError, "completeness"):
+ window.capture()
+ self.assertEqual(records[-1], {"node": "host", **value})
+ self.assertFalse(window.events["host"])
+
+ def test_cursor_pagination_keeps_early_attribution_and_does_not_skip_pages(self):
+ events = self.events()
+ observations = [{"complete": True, "afterCursor": "checkpoint"},
+ {"complete": True, "beforeCursor": "checkpoint", "afterCursor": "page-one", "hasMore": True,
+ "journal": json.dumps(events[0])},
+ {"complete": True, "beforeCursor": "page-one", "afterCursor": "page-two", "hasMore": False,
+ "journal": json.dumps(events[1])}]
+ calls = []
+ def remote(node, action, **params):
+ calls.append(params)
+ return observations.pop(0)
+ fleet = types.SimpleNamespace(nodes=["host"], record=lambda label, value: value, remote=remote)
+ window = JournalWindow(fleet, "unit")
+ self.assertEqual(window.capture(), {"host": events[:2]})
+ self.assertEqual(calls[1:], [{"afterCursor": "checkpoint"}, {"afterCursor": "page-one"}])
+ window.fleet.remote = lambda *args, **kwargs: {"complete": True, "beforeCursor": "page-two",
+ "afterCursor": "page-two", "hasMore": True}
+ with self.assertRaisesRegex(ValueError, "did not advance"): window.capture()
+
+ def test_scanner_profile_enables_only_scanner_source_and_fixed_deep_timing(self):
+ config = runtime_config("scanner")
+ logging_filter = "info,rustfs::scanner=debug,rustfs::heal=debug"
+ self.assertEqual(config["RUSTFS_OBS_LOGGER_LEVEL"], logging_filter)
+ self.assertEqual(config["RUST_LOG"], logging_filter)
+ self.assertEqual(config["RUSTFS_SCANNER_ENABLED"], "true")
+ for name in ("RUSTFS_HEAL_AUTO_HEAL_ENABLE", "RUSTFS_HEAL_MRF_ENABLE"):
+ self.assertEqual(config[name], "false")
+ self.assertEqual(config["RUSTFS_SCANNER_BITROT_CYCLE_SECS"], "0")
+ self.assertEqual(config["RUSTFS_SCANNER_DEEP_VERIFY_COOLDOWN_SECS"], "0")
+ self.assertEqual(config["RUSTFS_HEAL_OBJECT_SELECT_PROB"], "1")
+
+
+class HealMarkerSemanticsTest(unittest.TestCase):
+ def response(self, version, **changes):
+ return {"VersionId": version, "DeleteMarker": True, "ResponseMetadata": {"HTTPStatusCode": 204}, **changes}
+
+ def test_ack_manifest_retains_three_data_versions_and_exact_marker_order(self):
+ expected = [{"key": "key", "versionId": f"v{nonce}", "deleteMarker": False, "isLatest": nonce == 2}
+ for nonce in range(3)]
+ current = add_marker(expected, "key", self.response("marker"))
+ self.assertEqual(current[-1], {"key": "key", "versionId": "marker", "deleteMarker": True, "isLatest": True})
+ self.assertTrue(all(not entry["isLatest"] for entry in current[:-1]))
+ self.assertEqual(remove_marker(current, "key", self.response("marker")), expected)
+ for response in (self.response("marker", DeleteMarker=False), self.response("v1"), self.response("null"),
+ self.response("marker", ResponseMetadata={"HTTPStatusCode": 200})):
+ with self.subTest(response=response), self.assertRaises(ValueError): add_marker(expected, "key", response)
+ with self.assertRaises(ValueError): remove_marker(current, "key", self.response("another"))
+
+ def test_seed_has_real_historical_and_current_marker_ack_positions(self):
+ sequence = 0
+ calls = []
+ def call(operation, params, body=None):
+ nonlocal sequence
+ sequence += 1
+ calls.append((operation, params))
+ if operation == "put_object": return {"VersionId": f"data-{sequence}"}
+ if operation == "delete_object": return self.response(f"marker-{sequence}")
+ return {}
+ recorder = types.SimpleNamespace(call=call)
+ expected = seed_markers(recorder, "rv-heal-markers", 100)
+ self.assertEqual(len([entry for entry in expected if not entry["deleteMarker"]]), 18)
+ for instance in MARKER_INSTANCES:
+ key = "markers/" + instance["name"] + ".bin"
+ entries = [entry for entry in expected if entry["key"] == key]
+ markers = [entry for entry in entries if entry["deleteMarker"]]
+ self.assertEqual(len(markers), int(instance["mode"] != "create"))
+ if markers:
+ self.assertEqual(markers[0]["isLatest"], instance["mode"] != "historical")
+ selected = [operation for operation, params in calls if params.get("Key") == key]
+ self.assertEqual(selected, ["put_object", "put_object", "delete_object", "put_object"]
+ if instance["mode"] == "historical" else ["put_object"] * 3 + ["delete_object"])
+
+ def scene(self):
+ instance = next(item for item in MARKER_INSTANCES if item["mode"] == "create")
+ key = "markers/" + instance["name"] + ".bin"
+ before, bucket, expected = versioned_scene([key])
+ plan = marker_plan(instance, before, bucket, expected)
+ target = plan["proofs"][0]
+ marker = {"versionId": "new-marker", "deleted": True, "isLatest": True,
+ "dataDir": None, "inline": None, "parts": [], "erasure": {}, "size": 0}
+ authorities = copy.deepcopy(before)
+ for entry in authorities[target["node"]]["files"]:
+ if "metadata" not in entry or entry["disk"] == target["disk"]: continue
+ for version in entry["metadata"]["versions"]: version["isLatest"] = False
+ entry["metadata"]["versions"].insert(0, copy.deepcopy(marker))
+ entry["sha256"] += "marker-ack"
+ manifest = add_marker(expected, key, self.response("new-marker"))
+ return before, authorities, bucket, key, manifest, target
+
+ def test_stale_oracle_uses_online_ack_authority_and_original_target_data_roles(self):
+ before, authorities, bucket, key, manifest, target = self.scene()
+ oracle = stale_expected_scene(before, authorities, bucket, key, manifest, target)
+ identity = (target["node"], target["disk"], f"{bucket}/{key}/xl.meta")
+ original = files(before)[identity]["metadata"]["versions"]
+ wanted = files(oracle)[identity]["metadata"]["versions"]
+ self.assertEqual(wanted[0]["versionId"], "new-marker")
+ for old, new in zip(original, wanted[1:], strict=True):
+ self.assertEqual({**old, "isLatest": False}, new)
+ self.assertEqual(files(before)[identity]["sha256"], files(oracle)[identity]["sha256"])
+ self.assertEqual(files(authorities)[identity]["metadata"]["versions"], original)
+
+ def test_stale_oracle_rejects_target_writes_wrong_quorum_and_control_changes(self):
+ before, authorities, bucket, key, manifest, target = self.scene()
+ for mutation in ("target", "authority", "data", "format"):
+ changed = copy.deepcopy(authorities)
+ entries = changed[target["node"]]["files"]
+ if mutation == "target":
+ next(entry for entry in entries if entry["disk"] == target["disk"])["sha256"] += "unexpected"
+ elif mutation == "authority":
+ next(entry for entry in entries if entry.get("metadata") and entry["disk"] != target["disk"])["metadata"]["versions"][0]["versionId"] = "foreign"
+ elif mutation == "data": next(entry for entry in entries if not entry.get("metadata"))["sha256"] += "changed"
+ else: changed[target["node"]]["formats"][0]["value"]["id"] += "changed"
+ with self.subTest(mutation=mutation), self.assertRaises(ValueError):
+ stale_expected_scene(before, changed, bucket, key, manifest, target)
+
+ def test_marker_negative_requires_http_headers_and_no_payload_or_unknown(self):
+ good = {"acknowledged": False, "resultUnknown": False, "response": {"ResponseMetadata": {
+ "HTTPStatusCode": 405, "HTTPHeaders": {"x-amz-version-id": "marker", "x-amz-delete-marker": "true"}}}}
+ validate_marker_error(good, 405, "marker")
+ for kind in ("status", "version", "marker", "payload", "transport", "ack"):
+ changed = copy.deepcopy(good)
+ if kind == "status": changed["response"]["ResponseMetadata"]["HTTPStatusCode"] = 404
+ elif kind in {"version", "marker"}: changed["response"]["ResponseMetadata"]["HTTPHeaders"]["x-amz-" + ("version-id" if kind == "version" else "delete-marker")] = "wrong"
+ elif kind == "payload": changed["payload"] = {"size": 0}
+ elif kind == "transport": changed["resultUnknown"] = True
+ else: changed["acknowledged"] = True
+ with self.subTest(kind=kind), self.assertRaises(ValueError): validate_marker_error(changed, 405, "marker")
+
+ def test_permission_probe_checks_read_and_write_with_same_namespace_and_dac_limit(self):
+ value = {"mode": "permission-denied", "dataPath": "/private/data", "disk": 1}
+ output = types.SimpleNamespace(returncode=0, stdout=json.dumps({"uid": 0, "gid": 0, "errno": errno.EACCES}))
+ with patch("scripts.validation_heal.drive_gate.checked_gate", return_value=value), \
+ patch("scripts.validation_heal.drive_gate.process_identity", return_value={"uid": 0, "gid": 0, "capEff": 0}), \
+ patch("pathlib.Path.readlink", side_effect=[Path("mnt:[private]"), Path("mnt:[host]")]), \
+ patch("scripts.validation_heal.drive_gate.subprocess.run", return_value=output) as invoke:
+ result = probe_gate(None, None, 123)
+ self.assertEqual(result["readProbe"]["errno"], errno.EACCES)
+ self.assertEqual(invoke.call_count, 2)
+ for args in invoke.call_args_list:
+ command = args.args[0]
+ self.assertEqual(command.count("python3"), 1)
+ self.assertIn("--mount=/proc/123/ns/mnt", command)
+ self.assertIn("--bounding-set=-dac_override,-dac_read_search", command)
+ self.assertEqual(invoke.call_args.args[0][-1], "/private/data/.rustfs.sys/format.json")
+
+
+class HealReleasedClientTest(unittest.TestCase):
+ def archive(self, root, *, extra=None):
+ import io
+ import tarfile
+ binary = bytearray(64)
+ binary[:6] = b"\x7fELF\x02\x01"
+ binary[18:20] = b"\x3e\x00"
+ archive = root / "client.tar.gz"
+ with tarfile.open(archive, "w:gz") as output:
+ for name, data in [("rc", bytes(binary)), ("LICENSE-MIT", b"MIT"), ("LICENSE-APACHE", b"Apache")]:
+ entry = tarfile.TarInfo(name)
+ entry.size = len(data)
+ output.addfile(entry, io.BytesIO(data))
+ if extra is not None:
+ output.addfile(extra)
+ return archive, bytes(binary)
+
+ def test_exact_archive_and_binary_are_required_before_execution(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ archive, binary = self.archive(root)
+ with patch.object(rc_binary, "ARCHIVE_SHA256", hashlib.sha256(archive.read_bytes()).hexdigest()), \
+ patch.object(rc_binary, "BINARY_SHA256", hashlib.sha256(binary).hexdigest()), \
+ patch.object(rc_binary.subprocess, "run") as execute:
+ target = rc_binary.unpack(archive, root)
+ self.assertEqual(target.read_bytes(), binary)
+ self.assertEqual(target.stat().st_mode & 0o777, 0o700)
+ execute.assert_not_called()
+ with self.assertRaises(FileExistsError):
+ rc_binary.unpack(archive, root)
+
+ def test_archive_or_binary_digest_mismatch_does_not_create_executable(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ archive, _ = self.archive(root)
+ with self.assertRaisesRegex(ValueError, "archive digest"):
+ rc_binary.unpack(archive, root)
+ with patch.object(rc_binary, "ARCHIVE_SHA256", hashlib.sha256(archive.read_bytes()).hexdigest()):
+ with self.assertRaisesRegex(ValueError, "binary digest"):
+ rc_binary.unpack(archive, root)
+ self.assertFalse((root / "rc").exists())
+
+ def test_unexpected_or_link_members_are_rejected(self):
+ import tarfile
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ for name, kind in [("../outside", tarfile.REGTYPE), ("alias", tarfile.SYMTYPE)]:
+ entry = tarfile.TarInfo(name)
+ entry.type = kind
+ entry.linkname = "rc" if kind == tarfile.SYMTYPE else ""
+ archive, binary = self.archive(root, extra=entry)
+ with patch.object(rc_binary, "ARCHIVE_SHA256", hashlib.sha256(archive.read_bytes()).hexdigest()), \
+ patch.object(rc_binary, "BINARY_SHA256", hashlib.sha256(binary).hexdigest()):
+ with self.assertRaisesRegex(ValueError, "unexpected members"):
+ rc_binary.unpack(archive, root)
+ self.assertFalse((root / "rc").exists())
+
+ def test_prepare_records_pinned_identity_and_uses_private_client(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ archive, binary = self.archive(root)
+ raw = archive.read_bytes()
+ destination = root / "selected-client"
+ def invoke(command, **kwargs):
+ if command[0] == "curl":
+ self.assertEqual(command[-1], rc_binary.URL)
+ self.assertIn("--max-filesize", command)
+ Path(command[command.index("--output") + 1]).write_bytes(raw)
+ return types.SimpleNamespace(returncode=0)
+ self.assertEqual(command, [str((destination / "rc").resolve()), "--version"])
+ return types.SimpleNamespace(returncode=0, stdout="rc 0.1.36\n")
+ with patch.object(rc_binary, "ARCHIVE_SHA256", hashlib.sha256(raw).hexdigest()), \
+ patch.object(rc_binary, "BINARY_SHA256", hashlib.sha256(binary).hexdigest()), \
+ patch.object(rc_binary.subprocess, "run", side_effect=invoke) as execute:
+ target = rc_binary.prepare(destination)
+ self.assertEqual(execute.call_count, 2)
+ manifest = json.loads((destination / "manifest.json").read_text())
+ self.assertEqual(manifest["binaryPath"], str(target))
+ self.assertEqual(manifest["binarySha256"], hashlib.sha256(binary).hexdigest())
+ self.assertEqual(manifest["sourceCommit"], rc_binary.SOURCE)
+ self.assertEqual(manifest["versionOutput"], "rc 0.1.36")
+ with self.assertRaises(FileExistsError):
+ rc_binary.prepare(destination)
+
+
+class HealRuntimeStatusTest(unittest.TestCase):
+ def background(self, *, state="idle", missing=False):
+ return {"state": state, "clusterStatusComplete": not missing,
+ "coverage": {"expected": 4, "responded": 3 if missing else 4, "unknown": int(missing),
+ "reasons": ["peer_status_unavailable"] if missing else []},
+ "healQueueLength": 0, "healActiveTasks": 0,
+ "healOperations": {"queueLength": 0, "activeTasks": 0, "retryingTasks": 0}}
+
+ def observation(self, *, state="uninitialized", missing=False):
+ background = self.background(state=state, missing=missing)
+ uncertain = missing or state in {"disabled", "uninitialized"}
+ return {"node1": {"background": {"httpStatus": 200, "response": background},
+ "replacement": {"httpStatus": 200, "response": {"contractVersion": 2, "scope": "cluster_survivor_disks",
+ "cluster": {"records": [], "definitive": not uncertain, "reason": "runtime unavailable" if uncertain else None}}},
+ "rcJson": {"exitCode": 0, "json": {"state": state, "clusterStatusComplete": not missing, "coverage": background["coverage"]}},
+ "rcText": {"exitCode": 0, "stdout": "Heal Status: " + state.capitalize()}}}
+
+ def test_raw_coverage_cannot_hide_peer_loss_retrying_work_or_disabled_state(self):
+ validate_background(self.background(), "enabled-idle", 4)
+ validate_background(self.background(state="degraded", missing=True), "missing-peer", 4)
+ for branch, state in (("disabled", "disabled"), ("uninitialized", "uninitialized")):
+ validate_background(self.background(state=state), branch, 4)
+ for mutation in ("bool", "lost-peer", "false-idle", "retry", "disabled-idle", "reason"):
+ missing = mutation in {"false-idle", "reason"}
+ value = self.background(state="degraded" if missing else "idle", missing=missing)
+ branch = "missing-peer" if missing else "disabled" if mutation == "disabled-idle" else "enabled-idle"
+ if mutation == "bool": value["coverage"]["responded"] = True
+ elif mutation == "lost-peer": value["coverage"]["unknown"] = 1
+ elif mutation == "false-idle": value["state"] = "idle"
+ elif mutation == "retry": value["healOperations"]["retryingTasks"] = 1
+ elif mutation == "reason": value["coverage"]["reasons"] = []
+ with self.subTest(mutation=mutation), self.assertRaises(ValueError): validate_background(value, branch, 4)
+
+ def test_replacement_does_not_convert_disabled_uninitialized_or_partial_to_definitive(self):
+ for branch, state in (("disabled", "disabled"), ("uninitialized", "uninitialized"), ("missing-peer", "degraded")):
+ value = self.observation(state=state, missing=branch == "missing-peer")["node1"]["replacement"]["response"]
+ validate_replacement(value, branch)
+ value["cluster"]["definitive"] = True
+ with self.assertRaises(ValueError): validate_replacement(value, branch)
+
+ def test_rc_fields_and_text_must_preserve_raw_runtime_and_peer_truth(self):
+ for branch, state in (("disabled", "disabled"), ("uninitialized", "uninitialized"), ("missing-peer", "degraded")):
+ value = self.observation(state=state, missing=branch == "missing-peer")["node1"]
+ validate_cli(value["rcJson"], value["rcText"], value["background"]["response"], branch, 4)
+ changed = copy.deepcopy(value)
+ changed["rcJson"]["json"]["state"] = "idle"
+ with self.assertRaises(ValueError): validate_cli(changed["rcJson"], changed["rcText"], changed["background"]["response"], branch, 4)
+ changed = copy.deepcopy(value)
+ changed["rcJson"]["json"]["coverage"] = copy.deepcopy(changed["rcJson"]["json"]["coverage"])
+ changed["rcJson"]["json"]["coverage"]["unknown"] += 1
+ with self.assertRaises(ValueError): validate_cli(changed["rcJson"], changed["rcText"], changed["background"]["response"], branch, 4)
+ with self.assertRaises(ValueError): validate_cli(value["rcJson"], {"exitCode": 0, "stdout": "Heal Status: Idle"}, value["background"]["response"], branch, 4)
+
+ def test_unsafe_convergence_sample_fails_immediately_and_is_not_retried_to_green(self):
+ observed = self.observation(state="degraded", missing=True)
+ observed["node1"]["rcText"]["stdout"] = "Heal Status: Idle"
+ with self.assertRaises(ValueError): reject_unsafe_completion(observed)
+ with patch("scripts.validation_heal.status.observations", return_value=observed) as reads, \
+ patch("scripts.validation_heal.status.time.sleep") as sleep:
+ with self.assertRaises(ValueError): stable_status(None, {"endpoints": {"node1": "url"}}, None, "missing-peer", ["node1"])
+ reads.assert_called_once()
+ sleep.assert_not_called()
+
+ def test_startup_needs_raw_rc_window_bracket_transport_failure_is_not_observation(self):
+ good = self.observation()
+ fleet = types.SimpleNamespace(record=lambda label, value: value)
+ binding = {"endpoints": {"node1": "one", "node2": "two", "node3": "three", "node4": "four"}}
+ with patch("scripts.validation_heal.status.observations", return_value=good), \
+ patch("scripts.validation_heal.status.TimedProbe", return_value=types.SimpleNamespace(admin=lambda *args: good["node1"]["background"])):
+ result = watch_startup(fleet, binding, None, types.SimpleNamespace(is_set=lambda: False))
+ self.assertEqual(result["verdict"], "PASS")
+ self.assertTrue(result["rawBracketedCliWindow"])
+ bad = copy.deepcopy(good)
+ bad["node1"]["background"] = {"httpStatus": None, "error": "connection refused"}
+ with patch("scripts.validation_heal.status.observations", return_value=bad), \
+ patch("scripts.validation_heal.status.time.monotonic", side_effect=[0, 1, 31]), \
+ patch("scripts.validation_heal.status.time.sleep"):
+ result = watch_startup(fleet, binding, None, types.SimpleNamespace(is_set=lambda: False))
+ self.assertEqual(result["verdict"], "NOT_OBSERVED")
+ with patch("scripts.validation_heal.status.observations", return_value=good), \
+ patch("scripts.validation_heal.status.TimedProbe", return_value=types.SimpleNamespace(admin=lambda *args: {"httpStatus": 200, "response": self.background()})), \
+ patch("scripts.validation_heal.status.time.monotonic", side_effect=[0, 1, 31]), \
+ patch("scripts.validation_heal.status.time.sleep"):
+ self.assertEqual(watch_startup(fleet, binding, None, types.SimpleNamespace(is_set=lambda: False))["verdict"], "NOT_OBSERVED")
+
+ def test_cli_private_credentials_are_removed_and_never_enter_process_argv(self):
+ cli = Cli(Path("/usr/bin/rc"), {"endpoints": {"node1": "http://private:9000"}}, "runtime-access", "runtime-secret")
+ root = Path(cli.folder.name)
+ self.assertEqual(stat.S_IMODE((root / "config.toml").stat().st_mode), 0o600)
+ try:
+ with patch("scripts.validation_heal.status.subprocess.run", return_value=types.SimpleNamespace(
+ returncode=0, stdout='{"state":"disabled"}', stderr="")) as invoked:
+ result = cli.read("node1", as_json=True)
+ self.assertEqual(result["json"]["state"], "disabled")
+ self.assertNotIn("runtime-access", repr(invoked.call_args.args))
+ self.assertNotIn("runtime-secret", repr(invoked.call_args.args))
+ self.assertEqual(invoked.call_args.kwargs["env"]["RC_CONFIG_DIR"], str(root))
+ finally: cli.close()
+ self.assertFalse(root.exists())
+ config = runtime_config("disabled")
+ self.assertEqual(config["RUSTFS_HEAL_ENABLED"], "false")
+ self.assertEqual({**config, "RUSTFS_HEAL_ENABLED": "true"}, BACKGROUND_CONFIG)
+
+
+class HealPayloadMismatchTest(unittest.TestCase):
+ def test_length_and_payload_donors_keep_original_metadata_and_exact_same_role(self):
+ branches = VERSION_BRANCHES["heal.metadata-payload-mismatch"]
+ scene, bucket, expected = versioned_scene(["mismatch/" + branch + ".bin" for branch in branches])
+ original = copy.deepcopy(scene)
+ for branch in branches:
+ plan = mismatch_plan(branch, scene, bucket, expected)
+ self.assertEqual(len(plan["expected"]), 3)
+ self.assertEqual(plan["proofs"][0]["versionId"], "v0")
+ self.assertEqual(plan["donorProof"]["versionId"], "v2" if branch == "length-mismatch" else "v1")
+ self.assertEqual(len(plan["faults"]), 3 if plan["negative"] else 1)
+ if plan["negative"]:
+ self.assertEqual(len(plan["retainedShards"]), plan["readQuorum"] - 1)
+ self.assertTrue({shard["index"] for shard in plan["retainedShards"]}.isdisjoint(
+ {shard["index"] for shard in plan["faults"]}))
+ for fault in plan["faults"]:
+ self.assertNotEqual(fault["donorSha256"], fault["sha256"])
+ self.assertNotEqual(fault["donorRelativePath"], fault["relativePath"])
+ self.assertEqual(fault["donorSize"] != fault["size"], branch == "length-mismatch")
+ self.assertEqual(scene, original)
+ broken = copy.deepcopy(scene)
+ for item in broken["node1"]["files"]:
+ if "metadata" in item:
+ for version in item["metadata"]["versions"]:
+ if version["versionId"] == "v1":
+ version["erasure"]["block_size"] //= 2
+ with self.assertRaisesRegex(ValueError, "different EC layout"):
+ mismatch_plan("payload-mismatch", broken, bucket, expected)
+
+ def replacement(self, directory, *, invalid=None):
+ root = Path(directory).resolve()
+ prefix = "rv-heal-mismatch-unit"
+ target = prefix + "/key/old/part.1"
+ donor = prefix + "/key/new/part.1"
+ before, replacement = b"authoritative-old-shard", b"other-generation-shard"
+ (root / "quarantine").mkdir()
+ for relative, value in ((target, before), (donor, replacement)):
+ path = root / "data" / relative
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_bytes(value)
+ sha = hashlib.sha256(replacement).hexdigest()
+ if invalid == "hash": sha = "0" * 64
+ elif invalid == "traversal": donor = prefix + "/key/../new/part.1"
+ elif invalid == "bucket": donor = donor.replace(prefix, "unowned-bucket")
+ elif invalid == "metadata": donor = donor.removesuffix("part.1") + "xl.meta"
+ elif invalid == "symlink":
+ link = root / "data" / donor
+ link.unlink()
+ link.symlink_to(root / "data" / target)
+ elif invalid == "hardlink":
+ import os
+ os.link(root / "data" / donor, root / "second-link")
+ elif invalid == "identical": donor = target
+ owner = {"runId": "manual-mismatch-unit", "bucketPrefix": prefix}
+ with patch.object(heal_fault, "mount_id", return_value=1), heal_fault.directory(root) as fd:
+ if invalid:
+ with self.assertRaises((ValueError, OSError)):
+ heal_fault.apply_fault(fd, owner, target, "C08-test", "replace-part", hashlib.sha256(before).hexdigest(),
+ donor_relative=donor, donor_sha=sha)
+ self.assertEqual((root / "data" / target).read_bytes(), before)
+ self.assertFalse(list((root / "quarantine").iterdir()))
+ return
+ receipt = heal_fault.apply_fault(fd, owner, target, "C08-test", "replace-part", hashlib.sha256(before).hexdigest(),
+ donor_relative=donor, donor_sha=sha)
+ self.assertEqual((root / "data" / target).read_bytes(), replacement)
+ self.assertEqual((root / "data" / donor).read_bytes(), replacement)
+ self.assertEqual(receipt["donor"]["sha256"], sha)
+ self.assertEqual((root / "quarantine" / "C08-test.original").read_bytes(), before)
+ # A later generation is preserved instead of overwritten by rescue.
+ (root / "data" / target).write_bytes(b"new-committed-generation")
+ with self.assertRaisesRegex(ValueError, "changed after fault"):
+ heal_fault.recover_fault(fd, owner, "C08-test")
+ (root / "data" / target).write_bytes(replacement)
+ restored = heal_fault.recover_fault(fd, owner, "C08-test")
+ self.assertFalse(restored["physicalRepairPassAllowed"])
+ self.assertEqual((root / "data" / target).read_bytes(), before)
+
+ def test_replacement_is_recoverable_and_cannot_escape_owned_part_files(self):
+ for invalid in (None, "hash", "traversal", "bucket", "metadata", "symlink", "hardlink", "identical"):
+ with self.subTest(invalid=invalid), tempfile.TemporaryDirectory() as directory:
+ self.replacement(directory, invalid=invalid)
+
+ def test_three_version_fixture_has_real_different_length_and_content_acks(self):
+ observed = []
+ def invoke(operation, params, body=None):
+ observed.append((operation, params, body))
+ return {"VersionId": str(len(observed))} if operation == "put_object" else {}
+ with patch("scripts.validation_heal.versions.payload", side_effect=lambda identity, size: identity.encode()):
+ expected = seed_mismatch(types.SimpleNamespace(call=invoke), "rv-heal-mismatch")
+ self.assertEqual(len(expected), 9)
+ self.assertEqual(sum(entry["isLatest"] for entry in expected), 3)
+ for key in {entry["key"] for entry in expected}:
+ group = [entry for entry in expected if entry["key"] == key]
+ self.assertEqual(group[0]["payload"]["size"], group[1]["payload"]["size"])
+ self.assertNotEqual(group[0]["payload"]["size"], group[2]["payload"]["size"])
+ self.assertNotEqual(group[0]["payload"]["sha256"], group[1]["payload"]["sha256"])
+ with self.assertRaises(ValueError):
+ seed_mismatch(types.SimpleNamespace(call=lambda *args: {"VersionId": "null"}), "rv-heal-mismatch")
+
+
+class HealObjectLockTest(unittest.TestCase):
+ def test_governance_bypass_requires_explicit_owned_version_and_reaches_the_sdk(self):
+ from test_validation_heal_authorization import binding
+ bound = binding()
+ params = {"Bucket": bound["bucketPrefix"], "Key": "locked/governance.bin", "VersionId": "owned-version",
+ "BypassGovernanceRetention": True}
+ with self.assertRaises(ValueError): validate_s3_request(bound, "delete_object", params)
+ validate_s3_request(bound, "delete_object", params, allow_governance_bypass=True)
+ for operation, changed in (("put_object", params), ("delete_objects", params),
+ ("delete_object", {**params, "Bucket": "unowned"}),
+ ("delete_object", {**params, "VersionId": "null"}),
+ ("delete_object", {**params, "BypassGovernanceRetention": 1})):
+ with self.subTest(operation=operation), self.assertRaises(ValueError):
+ validate_s3_request(bound, operation, changed, allow_governance_bypass=True)
+ sdk = unittest.mock.Mock()
+ sdk.delete_object.return_value = {"ResponseMetadata": {"HTTPStatusCode": 204}}
+ probe = Probe(bound)
+ with patch.object(probe, "s3_client", return_value=sdk):
+ self.assertTrue(probe.s3("delete_object", params, allow_governance_bypass=True)["acknowledged"])
+ sdk.delete_object.assert_called_once_with(**params)
+
+ def seed(self, *, fail_after=None):
+ calls, context = [], {}
+ fleet = types.SimpleNamespace(secret_values=set(), record=lambda label, value: value)
+ principal = types.SimpleNamespace(binding={"runId": "gh-12345-lock"}, credentials=lambda: ("owner", "secret"))
+ def invoke(operation, params, body=None):
+ calls.append((operation, copy.deepcopy(params)))
+ if fail_after is not None and operation == "put_object" and len([x for x in calls if x[0] == operation]) > fail_after:
+ raise ValueError("partial protected fixture")
+ if operation == "get_bucket_versioning": return {"Status": "Enabled"}
+ if operation == "get_object_lock_configuration":
+ return {"ObjectLockConfiguration": {"ObjectLockEnabled": "Enabled"},
+ "ResponseMetadata": {"HTTPHeaders": {"date": "Sat, 10 Oct 2026 12:00:00 GMT"}}}
+ if operation == "put_object": return {"VersionId": "version-" + str(len(calls))}
+ return {}
+ recorder = types.SimpleNamespace(probe=principal, call=invoke)
+ with patch("scripts.validation_heal.object_lock.Probe", return_value=principal), \
+ patch("scripts.validation_heal.object_lock.payload", return_value=b"protected"), \
+ patch("scripts.validation_heal.object_lock.iam_exchange") as iam:
+ if fail_after is None:
+ expected = seed_locked(recorder, fleet, "rv-heal-lock", context)
+ else:
+ with self.assertRaisesRegex(ValueError, "partial protected fixture"):
+ seed_locked(recorder, fleet, "rv-heal-lock", context)
+ expected = None
+ return expected, context, calls, iam.call_args_list
+
+ def test_protected_seed_uses_real_server_time_and_keeps_partial_ownership(self):
+ expected, context, calls, iam = self.seed()
+ self.assertEqual(len(expected), 12)
+ self.assertEqual(len({(entry["key"], entry["versionId"]) for entry in expected}), 12)
+ self.assertEqual(context["retainUntil"], datetime(2026, 10, 10, 12, 10, tzinfo=UTC))
+ self.assertEqual(sum(entry["isLatest"] for entry in expected), 4)
+ statements = next(call.kwargs["document"]["Statement"] for call in iam if "document" in call.kwargs)
+ self.assertEqual(statements[-1]["Action"], ["s3:BypassGovernanceRetention"])
+ self.assertEqual(statements[-1]["Effect"], "Deny")
+ for fail_after in (0, 1, 10):
+ _, partial, _, _ = self.seed(fail_after=fail_after)
+ self.assertTrue(partial["bucketAttempted"] and partial["policyCreated"] and partial["attached"])
+ self.assertTrue(partial["user"].startswith("rvheal"))
+
+ def test_protected_plan_freezes_all_versions_on_the_lost_metadata_drive(self):
+ snapshot, bucket, expected = versioned_scene(LOCK_KEYS, (100, 100, 100))
+ for index, branch in enumerate(LOCK_BRANCHES):
+ plan = branch_plan("heal.object-lock-retention", branch, snapshot, bucket, expected, "DO")
+ self.assertEqual({item["key"] for item in plan["expected"]}, {LOCK_KEYS[index]})
+ self.assertEqual(plan["faults"][0]["versionId"], "v0" if index == 3 else "v2")
+ if index < 2:
+ self.assertEqual(plan["faults"][0]["role"], "parity" if index == 1 else "data")
+ else:
+ self.assertEqual(len(plan["proofs"]), 3)
+ self.assertEqual({item["versionId"] for item in plan["proofs"]}, {"v0", "v1", "v2"})
+ self.assertEqual(len({(item["node"], item["disk"]) for item in plan["proofs"]}), 1)
+ self.assertEqual(len({item["sha256"] for item in plan["proofs"]}), 3)
+ self.assertTrue(plan["faults"][0]["relativePath"].endswith("/xl.meta"))
+
+ def test_retention_proof_needs_exact_mode_expiry_and_active_server_time(self):
+ expiry = datetime(2026, 10, 10, 12, 5, tzinfo=UTC)
+ spec = {"ObjectLockMode": "COMPLIANCE", "ObjectLockRetainUntilDate": expiry}
+ good = {"acknowledged": True, "response": {"Retention": {"Mode": "COMPLIANCE", "RetainUntilDate": expiry},
+ "ResponseMetadata": {"HTTPHeaders": {"date": "Sat, 10 Oct 2026 12:00:00 GMT"}}}}
+ validate_protection(good, spec)
+ for invalid in ("mode", "expiry", "expired", "missing-server-time", "failed"):
+ changed = copy.deepcopy(good)
+ if invalid == "mode": changed["response"]["Retention"]["Mode"] = "GOVERNANCE"
+ elif invalid == "expiry": changed["response"]["Retention"]["RetainUntilDate"] -= timedelta(seconds=1)
+ elif invalid == "expired": changed["response"]["ResponseMetadata"]["HTTPHeaders"]["date"] = "Sat, 10 Oct 2026 12:05:00 GMT"
+ elif invalid == "missing-server-time": changed["response"].pop("ResponseMetadata")
+ else: changed["acknowledged"] = False
+ with self.subTest(invalid=invalid), self.assertRaises(ValueError): validate_protection(changed, spec)
+ validate_protection({"acknowledged": True, "response": {"LegalHold": {"Status": "ON"}}}, {"ObjectLockLegalHoldStatus": "ON"})
+ with self.assertRaises(ValueError):
+ validate_protection({"acknowledged": True, "response": {"LegalHold": {"Status": "OFF"}}}, {"ObjectLockLegalHoldStatus": "ON"})
+
+ def test_protected_delete_cannot_pass_as_a_success_or_generic_transport_failure(self):
+ good = {"acknowledged": False, "resultUnknown": False,
+ "response": {"ResponseMetadata": {"HTTPStatusCode": 403}, "Error": {"Code": "AccessDenied"}}}
+ validate_protected_delete(good)
+ for changed in ({**good, "acknowledged": True}, {**good, "payload": {}}, {**good, "resultUnknown": True},
+ {**good, "response": {"ResponseMetadata": {"HTTPStatusCode": 404}, "Error": {"Code": "NoSuchVersion"}}}):
+ with self.assertRaises(ValueError): validate_protected_delete(changed)
+
+ def cleanup(self, *, permanent_denial=False, transport_unknown=False, iam_failure=False):
+ clock, calls, evidence, removed = [0], [], {}, [False]
+ fleet = types.SimpleNamespace(reserved=["node1"], faults=[], stop=lambda: None, start=lambda: None,
+ remote=lambda *args, **kwargs: {}, snapshot=lambda *args, **kwargs: {}, cleanup=unittest.mock.Mock(return_value=True))
+ def record(label, value): evidence.setdefault(label, []).append(value); return value
+ fleet.record = record
+ def invoke(operation, params):
+ calls.append((operation, copy.deepcopy(params)))
+ if operation == "list_object_versions":
+ return {"acknowledged": True, "response": {} if removed[0] else {"Versions": [{"Key": LOCK_KEYS[1], "VersionId": "protected-version"}]}}
+ if operation == "get_object_legal_hold": return {"acknowledged": True, "response": {"LegalHold": {"Status": "OFF"}}}
+ if operation == "get_object_retention": return {"acknowledged": True, "response": {"Retention": {"Mode": "COMPLIANCE"}}}
+ if operation == "delete_object":
+ self.assertNotIn("BypassGovernanceRetention", params)
+ if permanent_denial or transport_unknown or clock[0] < 4:
+ return {"acknowledged": False, "resultUnknown": transport_unknown,
+ "response": {"ResponseMetadata": {"HTTPStatusCode": 403}, "Error": {"Code": "AccessDenied"}}}
+ removed[0] = True
+ return {"acknowledged": True, "response": {"ResponseMetadata": {"HTTPStatusCode": 204}}}
+ if operation == "delete_bucket": return {"acknowledged": True, "response": {}}
+ if operation == "head_bucket": return {"acknowledged": False, "resultUnknown": False,
+ "response": {"ResponseMetadata": {"HTTPStatusCode": 404}}}
+ self.fail("unexpected cleanup API: " + operation)
+ def sleep(seconds): clock[0] += CLEANUP_SECONDS + 1 if permanent_denial else seconds
+ context = {"bucketAttempted": True}
+ if iam_failure: context.update(user="rvhealowned", policy="rvhealpolicy", policyCreated=True, attached=True)
+ with patch("scripts.validation_heal.object_lock.time.monotonic", side_effect=lambda: clock[0]), \
+ patch("scripts.validation_heal.object_lock.time.sleep", side_effect=sleep), \
+ patch("scripts.validation_heal.object_lock.iam_exchange", side_effect=ValueError("IAM cleanup failure") if iam_failure else None):
+ result = cleanup_locked_fleet(types.SimpleNamespace(s3=invoke), fleet, "rv-heal-0123456789abcdef", context)
+ return result, fleet, calls, evidence, clock
+
+ def test_physical_cleanup_waits_for_server_ack_after_real_compliance_expiry(self):
+ result, fleet, calls, evidence, clock = self.cleanup()
+ self.assertTrue(result)
+ fleet.cleanup.assert_called_once()
+ self.assertEqual(clock[0], 4)
+ self.assertEqual(len(evidence["locked-final-delete"]), 3)
+ self.assertTrue(evidence["locked-finalization"][0]["namespaceDeletionAllowed"])
+ self.assertFalse(any(operation == "put_object_retention" for operation, params in calls))
+
+ def test_retained_or_unknown_protection_forbids_namespace_deletion(self):
+ for options in ({"permanent_denial": True}, {"transport_unknown": True}, {"iam_failure": True}):
+ with self.subTest(options=options):
+ result, fleet, calls, evidence, clock = self.cleanup(**options)
+ self.assertFalse(result)
+ fleet.cleanup.assert_not_called()
+ self.assertEqual(evidence["locked-finalization"][0]["state"], "RETAINED")
+ self.assertFalse(evidence["locked-finalization"][0]["namespaceDeletionAllowed"])
+
+
+class HealCursorOracleTest(unittest.TestCase):
+ bucket = "rv-heal-cursor-unit"
+
+ def expected(self):
+ return [{"key": f"objects/{index:04d}.bin", "versionId": "null", "deleteMarker": False,
+ "payload": {"size": CURSOR_SIZE}} for index in range(CURSOR_OBJECTS)]
+
+ def window(self, endpoint="node1"):
+ return Window(self.bucket, self.expected(), "task", endpoint, "deployment/task")
+
+ def status(self, start=0, stop=3, *, minimum=1, truncated=False, finished=False):
+ items = [{"bucket": self.bucket, "object": f"objects/{index:04d}.bin", "versionId": "",
+ "objectSize": CURSOR_SIZE} for index in range(start, stop)]
+ return {"summary": "finished" if finished else "running", "nextSeq": stop + 1, "minSeq": minimum,
+ "items": items, "truncated": truncated, "outcome": {"counters": {"processed": stop}}}
+
+ def consume(self, window, status, cursor):
+ return window.consume(status, cursor, token="task", endpoint=window.endpoint, epoch="deployment/task")
+
+ def test_exclusive_next_sequence_preserves_the_first_item_of_each_poll(self):
+ window = self.window()
+ self.assertEqual(self.consume(window, self.status(), 0)["exclusiveNextCursor"], 3)
+ self.assertEqual(self.consume(window, self.status(3, 6), 3)["exclusiveNextCursor"], 6)
+ self.assertEqual(len(window.seen), 6)
+ self.assertEqual(window.running_windows, 2)
+ with self.assertRaisesRegex(ValueError, "jumped"):
+ self.consume(window, self.status(6, 9), 7)
+
+ def test_truncated_payload_advances_only_over_transmitted_items(self):
+ window, status = self.window(), self.status(0, 8, truncated=True)
+ status["items"] = status["items"][:4]
+ self.assertEqual(self.consume(window, status, 0)["exclusiveNextCursor"], 4)
+ self.assertEqual(self.consume(window, self.status(4, 8, truncated=True), 4)["exclusiveNextCursor"], 8)
+ self.assertEqual(len(window.seen), 8)
+ status["truncated"] = False
+ with self.assertRaisesRegex(ValueError, "silently skip"):
+ self.window().check(status, 0)
+
+ def test_retention_gap_requires_a_real_moved_minimum_and_flag(self):
+ status = self.status(1, CURSOR_OBJECTS, minimum=2, truncated=True, finished=True)
+ proof = self.window().check(status, 0)
+ self.assertTrue(proof["retentionGap"])
+ self.assertEqual(proof["returnedItems"], 1024)
+ changed = copy.deepcopy(status)
+ changed["truncated"] = False
+ with self.assertRaisesRegex(ValueError, "silently omitted"):
+ self.window().check(changed, 0)
+ caught_up = self.status(CURSOR_OBJECTS, CURSOR_OBJECTS, minimum=2, truncated=True, finished=True)
+ self.assertEqual(self.window().check(caught_up, CURSOR_OBJECTS)["returnedItems"], 0)
+ self.assertFalse(self.window().check(caught_up, CURSOR_OBJECTS)["retentionGap"])
+
+ def test_duplicate_backward_wrong_scope_and_epoch_are_not_convergence(self):
+ for kind in ("duplicate", "backward", "bucket", "version", "size", "bounds", "flag", "terminal"):
+ with self.subTest(kind=kind):
+ window = self.window()
+ self.consume(window, self.status(), 0)
+ status = self.status(3, 6)
+ if kind == "duplicate": status["items"][0]["object"] = "objects/0000.bin"
+ if kind == "backward": status.update(nextSeq=3, items=[])
+ if kind == "bucket": status["items"][0]["bucket"] = "unowned"
+ if kind == "version": status["items"][0]["versionId"] = "different-generation"
+ if kind == "size": status["items"][0]["objectSize"] += 1
+ if kind == "bounds": status["nextSeq"] = True
+ if kind == "flag": status["truncated"] = "false"
+ if kind == "terminal": status["summary"] = "stopped"
+ with self.assertRaises(ValueError): self.consume(window, status, 3)
+ for name, value in (("token", "other-task"), ("endpoint", "node2"), ("epoch", "restarted/task")):
+ values = {"token": "task", "endpoint": "node1", "epoch": "deployment/task", name: value}
+ with self.assertRaisesRegex(ValueError, "crossed"):
+ self.window().consume(self.status(), 0, **values)
+
+ def terminal(self):
+ status = self.status(1, CURSOR_OBJECTS, minimum=2, truncated=True, finished=True)
+ receipts = [{"identity": {"bucket": self.bucket, "object": f"objects/{index:04d}.bin", "versionId": None,
+ "poolIndex": 0, "setIndex": 0, "bucketIncarnationId": "incarnation"},
+ "disposition": {"state": "repaired" if index == 1024 else "verified_healthy"}} for index in range(897, 1025)]
+ status["outcome"].update(execution={"state": "completed"}, coverage="complete", objectsTruncated=True,
+ objects=receipts, counters={"processed": CURSOR_OBJECTS, "healed": 1, "unchanged": CURSOR_OBJECTS - 1,
+ "skipped": 0, "failed": 0, "unknown": 0, "attemptFailures": 0, "overflowed": False})
+ return status
+
+ def test_terminal_truncation_cannot_hide_error_or_claim_complete_diagnostics(self):
+ repaired = {("objects/1024.bin", "null")}
+ proof = cursor_terminal(self.terminal(), self.bucket, self.expected(), repaired, self.window())
+ self.assertFalse(proof["diagnosticObjectsAreCompleteManifest"])
+ for kind in ("unknown", "failure", "attempt", "counter", "coverage", "truncation", "identity", "repair", "gap"):
+ status = self.terminal()
+ if kind == "unknown": status["outcome"]["counters"]["unknown"] = 1
+ if kind == "failure": status["outcome"]["counters"]["failed"] = 1
+ if kind == "attempt": status["outcome"]["counters"]["attemptFailures"] = 1
+ if kind == "counter": status["outcome"]["counters"]["processed"] -= 1
+ if kind == "coverage": status["outcome"]["coverage"] = "partial"
+ if kind == "truncation": status["outcome"]["objectsTruncated"] = False
+ if kind == "identity": status["outcome"]["objects"][0]["identity"]["bucketIncarnationId"] = "other"
+ if kind == "repair": status["outcome"]["objects"][-1]["disposition"]["state"] = "verified_healthy"
+ if kind == "gap": status["minSeq"] = 1
+ with self.subTest(kind=kind), self.assertRaises(ValueError):
+ cursor_terminal(status, self.bucket, self.expected(), repaired, self.window())
+
+ def test_fault_group_checks_all_controls_and_all_exact_missing_parts(self):
+ before = HealShardOracleTest().snapshot()
+ target = map_target(before, "rv-heal-0123456789abcdef", "objects/0008.bin", "data", 1)
+ second = {**target, **next(shard for shard in target["cohort"] if shard["disk"] != target["disk"])}
+ targets = [target, second]
+ receipts = [{"relativePath": item["relativePath"], "fault": None,
+ "before": {"size": item["size"], "sha256": item["sha256"]}} for item in targets]
+ after = copy.deepcopy(before)
+ removed = {(item["disk"], item["relativePath"]) for item in targets}
+ after["node1"]["files"] = [entry for entry in after["node1"]["files"] if (entry["disk"], entry["path"]) not in removed]
+ self.assertEqual(validate_missing_batch(before, after, targets, receipts)["exactMissingParts"], 2)
+ after["node1"]["files"][0]["sha256"] = "changed-control"
+ with self.assertRaisesRegex(ValueError, "changed metadata"):
+ validate_missing_batch(before, after, targets, receipts)
+
+ def test_batch_bitrot_verifier_requires_every_exact_part_and_no_duplicates(self):
+ with patch("scripts.validation_heal.fleet_node.owned", return_value=(Path("/state"), [], {"inspectorSha256": "sha"})), \
+ patch("scripts.validation_heal.fleet_node.stopped"), patch("scripts.validation_heal.fleet_node.checked", side_effect=lambda path, boundary: path), \
+ patch("scripts.validation_heal.fleet_node.digest", return_value="sha"), \
+ patch("scripts.validation_heal.fleet_node.verify_part_owned", side_effect=lambda entry, *args: entry) as reads:
+ entry = {"disk": 0, "relativePath": "bucket/key/data/part.1", "versionId": "null", "partNumber": 1}
+ self.assertEqual(node_verify_parts({"runId": "run", "parts": [entry]})["parts"], [entry])
+ self.assertEqual(reads.call_count, 1)
+ for batch in ([], [entry, entry], [entry] * 129, [{**entry, "extra": "unbound"}]):
+ with self.assertRaises(ValueError): node_verify_parts({"runId": "run", "parts": batch})
+
+ def test_cursor_resources_are_explicit_bounded_and_source_branches_preserved(self):
+ self.assertEqual(resource_limits("standard")["maxSnapshotFiles"], 4096)
+ # The live candidate writes a part integrity sidecar on every root.
+ self.assertGreater(resource_limits("cursor")["maxSnapshotFiles"], 1025 * 4 * 3)
+ self.assertEqual(resource_limits("cursor")["maxSnapshotFiles"], 16384)
+ self.assertEqual(resource_limits("cursor")["minimumDriveFreeBytes"], 2 * 1024**3)
+ self.assertEqual(list(CURSOR_BRANCHES), select("J03")[0]["variants"])
+ with self.assertRaises(ValueError): resource_limits("unbounded")
+
+ def test_complete_part_manifest_rejects_omitted_or_wrong_original_bytes(self):
+ cohorts = [{"cohort": [{"node": "node1", "disk": disk, "relativePath": f"bucket/{index}/part.1",
+ "versionId": "null", "partNumber": 1, "sha256": f"sha-{index}-{disk}", "size": 100}
+ for disk in range(4)]} for index in range(CURSOR_OBJECTS)]
+ authority = {(item["disk"], item["relativePath"]): item for cohort in cohorts for item in cohort["cohort"]}
+ def remote(node, action, *, parts):
+ self.assertEqual(action, "verify-parts")
+ self.assertLessEqual(len(parts), 128)
+ return {"complete": True, "parts": [{**part, "sha256": authority[part["disk"], part["relativePath"]]["sha256"],
+ "physicalBitrotVerified": True, "physicalSize": 100} for part in parts]}
+ fleet = types.SimpleNamespace(nodes=["node1"], remote=remote, record=lambda label, value: value)
+ self.assertEqual(verify_all_parts(fleet, cohorts, "proof")["allPartsStreamingVerified"], CURSOR_OBJECTS * 4)
+ good = fleet.remote
+ for kind in ("omitted", "wrong-sha", "checksum", "size"):
+ def invalid(node, action, *, parts):
+ result = good(node, action, parts=parts)
+ if kind == "omitted": result["parts"].pop()
+ if kind == "wrong-sha": result["parts"][0]["sha256"] = "donor"
+ if kind == "checksum": result["parts"][0]["physicalBitrotVerified"] = False
+ if kind == "size": result["parts"][0]["physicalSize"] += 1
+ return result
+ fleet.remote = invalid
+ with self.subTest(kind=kind), self.assertRaises(ValueError): verify_all_parts(fleet, cohorts, "proof")
+
+
+class HealStopOracleTest(unittest.TestCase):
+ def expected(self):
+ return HealCursorOracleTest().expected()
+
+ def cancelled(self):
+ status = HealCursorOracleTest().terminal()
+ objects = status["outcome"]["objects"][-2:]
+ objects[0]["disposition"]["state"] = "cancelled"
+ status.update(summary="stopped")
+ status["outcome"].update(execution={"state": "aborted", "reason": "cancelled"}, coverage="partial", objects=objects)
+ status["outcome"]["counters"].update(processed=4, healed=1, unchanged=2, skipped=1)
+ targets = [{"key": "objects/1023.bin", "versionId": "null"}, {"key": "objects/1024.bin", "versionId": "null"}]
+ return status, targets
+
+ def test_cancelled_receipts_cannot_claim_complete_or_repaired_unresolved_objects(self):
+ status, targets = self.cancelled()
+ proof = validate_cancelled(status, HealCursorOracleTest.bucket, self.expected(), targets)
+ self.assertEqual(proof["coverage"], "partial")
+ for kind in ("completed", "coverage", "unknown", "counter", "full-heal", "cancelled-as-repaired", "identity"):
+ changed = copy.deepcopy(status)
+ if kind == "completed": changed["outcome"]["execution"] = {"state": "completed"}
+ if kind == "coverage": changed["outcome"]["coverage"] = "complete"
+ if kind == "unknown": changed["outcome"]["counters"]["unknown"] = 1
+ if kind == "counter": changed["outcome"]["counters"]["processed"] += 1
+ if kind == "full-heal": changed["outcome"]["counters"]["healed"] = 2
+ if kind == "cancelled-as-repaired": changed["outcome"]["objects"][0]["disposition"]["state"] = "unknown"
+ if kind == "identity": changed["outcome"]["objects"][0]["identity"]["bucket"] = "other"
+ with self.subTest(kind=kind), self.assertRaises(ValueError):
+ validate_cancelled(changed, HealCursorOracleTest.bucket, self.expected(), targets)
+
+ def test_active_window_requires_actual_partial_canonical_progress_and_no_errors(self):
+ status, targets = self.cancelled()
+ status.update(summary="running")
+ status["outcome"]["execution"] = {"state": "running"}
+ self.assertTrue(partial_window(status, targets, repair_required=True))
+ for name, value in (("processed", 0), ("processed", CURSOR_OBJECTS), ("healed", 0), ("healed", 2)):
+ changed = copy.deepcopy(status)
+ changed["outcome"]["counters"][name] = value
+ self.assertFalse(partial_window(changed, targets, repair_required=True))
+ changed = copy.deepcopy(status)
+ changed["outcome"]["counters"]["unknown"] = 1
+ with self.assertRaises(ValueError): partial_window(changed, targets, repair_required=True)
+
+ def test_repeat_stop_requires_idempotent_ack_for_the_exact_retained_token(self):
+ response = {"httpStatus": 200, "query": [["clientToken", "old-token"], ["forceStop", "true"]],
+ "method": "POST", "auth": "signed", "requestBody": "", "path": "/rustfs/admin/v3/heal/bucket/objects/",
+ "response": {"summary": "stopped", "detail": "", "startTime": "2026-10-10T22:16:17.150884876Z",
+ "settings": {**BASE_OPTIONS, "scanMode": 1, "pool": None, "set": None}}}
+ self.assertEqual(validate_retired_stop(response, "old-token")["exactRetiredTaskId"], "old-token")
+ self.assertFalse(validate_retired_stop(response, "old-token")["responseEchoesTaskIdentity"])
+ in_memory = {**response, "query": [("clientToken", "old-token"), ("forceStop", "true")]}
+ self.assertEqual(validate_retired_stop(in_memory, "old-token"), validate_retired_stop(json.loads(json.dumps(in_memory)), "old-token"))
+ for changed in ({**response, "httpStatus": 500}, {**response, "transportError": "timeout"},
+ {**response, "query": [["clientToken", "new-token"], ["forceStop", "true"]]},
+ {**response, "query": [["clientToken", "old-token"], ["forceStop", "true"], ["clientToken", "other"]]},
+ {**response, "query": {"clientToken": "old-token", "forceStop": "true"}},
+ {**response, "response": {**response["response"], "summary": "finished"}},
+ {**response, "response": {**response["response"], "outcome": {"execution": {"state": "completed"}}}},
+ {**response, "response": {**response["response"], "settings": BASE_OPTIONS}},
+ {**response, "response": "InternalErrorHeal task not found: old-token"}):
+ with self.assertRaises(ValueError): validate_retired_stop(changed, "old-token")
+
+ def test_unknown_token_is_not_confused_with_retained_terminal_idempotency(self):
+ response = {"httpStatus": 500, "response": "InternalErrorHeal task not found: old-token"}
+ self.assertEqual(validate_unknown_token(response, "old-token")["exactUnknownTaskId"], "old-token")
+ for changed in ({**response, "httpStatus": 200}, {**response, "transportError": "timeout"},
+ {**response, "response": response["response"].replace("old-token", "new-token")},
+ {**response, "response": response["response"].replace("InternalError", "AccessDenied")}):
+ with self.assertRaises(ValueError): validate_unknown_token(changed, "old-token")
+
+ def test_cancelled_physical_count_must_match_actual_original_shards(self):
+ before = HealShardOracleTest().snapshot()
+ for entry in list(before["node1"]["files"]):
+ if "objects/0008.bin" not in entry["path"]: continue
+ cloned = copy.deepcopy(entry)
+ cloned["path"] = cloned["path"].replace("objects/0008.bin", "objects/0017.bin")
+ if "metadata" in cloned: cloned["metadata"]["key"] = "objects/0017.bin"
+ before["node1"]["files"].append(cloned)
+ target = map_target(before, "rv-heal-0123456789abcdef", "objects/0008.bin", "data", 1)
+ second = map_target(before, "rv-heal-0123456789abcdef", "objects/0017.bin", "data", 1)
+ quiet = copy.deepcopy(before)
+ quiet["node1"]["files"] = [entry for entry in quiet["node1"]["files"] if
+ (entry["disk"], entry["path"]) != (second["disk"], second["relativePath"])]
+ responsibility = {"canonicalCounters": {"healed": 1}, "diagnosticDispositions": [
+ {"key": target["key"], "versionId": "null", "state": "repaired"},
+ {"key": second["key"], "versionId": "null", "state": "cancelled"}]}
+ remaining, proof = cancelled_physical(before, quiet, [target, second], responsibility)
+ self.assertEqual(remaining, [second])
+ self.assertFalse(proof["backupRescueUsed"])
+ with self.assertRaisesRegex(ValueError, "disagrees"):
+ cancelled_physical(before, quiet, [target, second], {"canonicalCounters": {"healed": 0}})
+ wrong_disposition = copy.deepcopy(responsibility)
+ wrong_disposition["diagnosticDispositions"][0]["state"] = "cancelled"
+ with self.assertRaisesRegex(ValueError, "diagnostic disposition"):
+ cancelled_physical(before, quiet, [target, second], wrong_disposition)
+ next(entry for entry in quiet["node1"]["files"] if "/control/" in entry["path"])["sha256"] = "changed-control"
+ with self.assertRaises(ValueError): cancelled_physical(before, quiet, [target, second], responsibility)
+
+ def test_every_stop_source_branch_is_retained(self):
+ self.assertEqual(list(STOP_BRANCHES), select("A06")[0]["variants"])
+
+ def test_task_exit_waits_for_actual_drain_and_never_hides_partial_idle(self):
+ good = {"httpStatus": 200, "response": {"state": "idle", "clusterStatusComplete": True,
+ "coverage": {"expected": 1, "responded": 1, "unknown": 0, "reasons": []},
+ "healQueueLength": 0, "healActiveTasks": 0,
+ "healOperations": {"queueLength": 0, "activeTasks": 0, "retryingTasks": 0}}}
+ active = copy.deepcopy(good)
+ active["response"].update(state="active", healActiveTasks=1)
+ active["response"]["healOperations"]["activeTasks"] = 1
+ fleet = types.SimpleNamespace(record=lambda label, value: value)
+ client = types.SimpleNamespace(admin=types.SimpleNamespace())
+ from unittest.mock import Mock
+ client.admin = Mock(side_effect=[active, good])
+ binding = {"endpoints": {"node1": "url"}}
+ with patch("scripts.validation_heal.stop.Probe", return_value=client), patch("scripts.validation_heal.stop.time.sleep"):
+ self.assertEqual(stop_idle_fleet(fleet, binding)["node1"]["state"], "idle")
+ unsafe = copy.deepcopy(good)
+ unsafe["response"]["clusterStatusComplete"] = False
+ client.admin = Mock(side_effect=[unsafe, good])
+ with patch("scripts.validation_heal.stop.Probe", return_value=client):
+ with self.assertRaisesRegex(ValueError, "partial fleet"):
+ stop_idle_fleet(fleet, binding)
+ self.assertEqual(client.admin.call_count, 1)
+
+
+class HealCancelledRestartOracleTest(unittest.TestCase):
+ token = "12345678-1234-1234-1234-123456789abc"
+
+ def journal(self):
+ status, _ = HealStopOracleTest().cancelled()
+ terminal = {"schema": 1, "task_id": self.token, "status": "Cancelled",
+ "heal_type": {"type": "prefix", "bucket": HealCursorOracleTest.bucket, "prefix": "objects/"}}
+ report = {"schema": 1, "terminal": terminal, "outcome": status["outcome"], "min_seq": 1, "next_seq": 2,
+ "seqed_items": [[1, {"bucket": HealCursorOracleTest.bucket, "object": "objects/0000.bin", "objectSize": CURSOR_SIZE}]]}
+ records = [{"disk": disk, "kind": kind, "present": disk == 0 and kind != "intent",
+ **({"value": terminal if kind == "terminal" else report, "sha256": "original", "size": 100}
+ if disk == 0 and kind != "intent" else {})} for disk in range(4) for kind in ("intent", "terminal", "report")]
+ return {"node1": {"taskId": self.token, "readOnly": True, "records": records}}, status
+
+ def test_actual_cancelled_journal_rejects_runnable_duplicate_or_wrong_epoch_records(self):
+ observed, status = self.journal()
+ expected = HealCursorOracleTest().expected()
+ self.assertTrue(validate_journal(observed, self.token, HealCursorOracleTest.bucket, status, expected)["runnableIntentAbsentOnEveryDrive"])
+ for kind in ("intent", "duplicate", "scope", "status", "schema", "identity", "outcome", "cursor", "owner", "missing", "item-key", "item-version", "item-size", "item-duplicate", "item-empty"):
+ changed = copy.deepcopy(observed)
+ records = changed["node1"]["records"]
+ terminal, report = records[1]["value"], records[2]["value"]
+ if kind == "intent": records[0]["present"] = True
+ if kind == "duplicate": records[4].update(present=True, value=terminal)
+ if kind == "scope": terminal["heal_type"]["bucket"] = "unowned"
+ if kind == "status": terminal["status"] = "Completed"
+ if kind == "schema": terminal["schema"] = True
+ if kind == "identity": terminal["task_id"] = "another-task"
+ if kind == "outcome": report["outcome"]["counters"]["healed"] += 1
+ if kind == "cursor": report["seqed_items"][0][0] = report["next_seq"]
+ if kind == "owner": records[2]["disk"] = 1
+ if kind == "missing": records[2]["present"] = False
+ if kind == "item-key": report["seqed_items"][0][1]["object"] = "objects/unregistered.bin"
+ if kind == "item-version": report["seqed_items"][0][1]["versionId"] = "another-version"
+ if kind == "item-size": report["seqed_items"][0][1]["objectSize"] += 1
+ if kind == "item-duplicate":
+ report["next_seq"] = 3
+ report["seqed_items"].append([2, copy.deepcopy(report["seqed_items"][0][1])])
+ if kind == "item-empty": report["seqed_items"] = []
+ with self.subTest(kind=kind), self.assertRaises(ValueError):
+ validate_journal(changed, self.token, HealCursorOracleTest.bucket, status, expected)
+
+ def test_node_reader_is_read_only_and_only_accepts_exact_task_filenames(self):
+ with tempfile.TemporaryDirectory() as folder:
+ root = Path(folder).resolve()
+ state = root / "state"
+ state.mkdir()
+ (state / "rustfs").write_bytes(b"candidate")
+ roots = [root / f"disk{disk}" for disk in range(4)]
+ for disk in roots: (disk / "data/.rustfs.sys").mkdir(parents=True)
+ system = roots[0] / "data/.rustfs.sys"
+ terminal = system / f"terminal-root-heal-{self.token}.json"
+ terminal.write_text('{"task_id":"' + self.token + '"}')
+ unrelated = system / "terminal-root-heal-unrelated.json"
+ unrelated.write_text('{"unrelated":"must not enter evidence"}')
+ owner = {"binarySha256": hashlib.sha256(b"candidate").hexdigest()}
+ with patch("scripts.validation_heal.fleet_node.owned", return_value=(state, roots, owner)), \
+ patch("scripts.validation_heal.fleet_node.stopped"):
+ observed = node_task_records({"runId": "run", "taskId": self.token, "quiescent": True})
+ self.assertTrue(observed["readOnly"])
+ self.assertEqual(len(observed["records"]), 12)
+ self.assertEqual(sum(entry["present"] for entry in observed["records"]), 1)
+ self.assertNotIn("must not enter evidence", json.dumps(observed))
+ self.assertEqual(terminal.read_text(), '{"task_id":"' + self.token + '"}')
+ for invalid in ("../other", self.token.upper(), "not-a-uuid"):
+ with self.assertRaises(ValueError): node_task_records({"runId": "run", "taskId": invalid, "quiescent": True})
+ terminal.unlink()
+ terminal.symlink_to(unrelated)
+ with self.assertRaises(ValueError): node_task_records({"runId": "run", "taskId": self.token, "quiescent": True})
+
+ def test_restart_preserves_every_source_branch(self):
+ self.assertEqual(list(CANCEL_RESTART_BRANCHES), select("F06")[0]["variants"])
+
+
+class HealOverlapOracleTest(unittest.TestCase):
+ bucket = "rv-heal-overlap-unit"
+
+ def retired_stop(self, path, query, body):
+ return {"httpStatus": 200, "query": query, "method": "POST", "auth": "signed",
+ "requestBody": body.decode(), "path": path,
+ "response": {"summary": "stopped", "detail": "", "startTime": "2026-10-23T02:00:00Z",
+ "settings": {**BASE_OPTIONS, "scanMode": 1, "pool": None, "set": None}}}
+
+ def expected(self):
+ return [{"key": f"objects/a/child/{index:04d}.bin", "versionId": "null", "deleteMarker": False,
+ "payload": {"size": CURSOR_SIZE}} for index in range(overlap.SCOPE_OBJECTS)]
+
+ def damaged(self):
+ return {(self.expected()[index]["key"], "null") for index in overlap.MISSING}
+
+ def items(self):
+ return [{"bucket": self.bucket, "object": entry["key"], "versionId": "", "objectSize": CURSOR_SIZE}
+ for entry in self.expected()]
+
+ def state(self, *, cancelled=False, replacement=False, healed=None, processed=None, min_seq=1):
+ total = processed if processed is not None else 100 if cancelled else overlap.SCOPE_OBJECTS
+ repaired = {index for index in overlap.MISSING if index < total}
+ if replacement: repaired -= {0, 64}
+ return {"summary": "stopped" if cancelled else "running" if processed is not None else "finished",
+ "nextSeq": total + 1, "minSeq": min_seq, "items": self.items()[min_seq - 1:total],
+ "truncated": min_seq > 1, "outcome": {
+ "execution": {"state": "aborted", "reason": "cancelled"} if cancelled else
+ {"state": "running" if processed is not None else "completed"},
+ "coverage": "partial" if cancelled or processed is not None else "complete", "objectsTruncated": total > 128,
+ "objects": [{"identity": {"bucket": self.bucket, "object": self.expected()[index]["key"],
+ "versionId": None, "poolIndex": 0, "setIndex": 0, "bucketIncarnationId": "incarnation"},
+ "disposition": {"state": "repaired" if index in repaired else "verified_healthy"}}
+ for index in range(min(total, 128))],
+ "counters": {"processed": total, "healed": len(repaired) if healed is None else healed,
+ "unchanged": total - (len(repaired) if healed is None else healed),
+ "skipped": 0, "failed": 0, "unknown": 0, "attemptFailures": 0, "overflowed": False}}}
+
+ def proof(self, current, **kwargs):
+ return overlap.scope_proof(current, self.bucket, self.expected(), self.damaged(), **kwargs)
+
+ def test_frozen_policy_rejects_arbitrary_http_and_reason(self):
+ good = {"httpStatus": 409, "response": {"Code": "OperationAborted", "Message": overlap.POLICY["parent-child"]["message"]}}
+ self.assertEqual(overlap.overlap_rejection(good)["code"], "OperationAborted")
+ for changed in ({**good, "httpStatus": 200}, {**good, "transportError": "timeout"},
+ {**good, "response": {"Code": "OperationAborted", "Message": "already_running"}}):
+ with self.assertRaises(ValueError): overlap.overlap_rejection(changed)
+
+ def test_incomplete_or_cross_scope_receipts_and_unknowns_never_pass(self):
+ self.proof(self.state(), healed=5)
+ for kind in ("count", "unknown", "failed", "attempt", "coverage", "diagnostic", "key", "version", "incarnation", "role", "duplicate"):
+ current = self.state()
+ value = current["outcome"]
+ if kind == "count": value["counters"]["healed"] = 4
+ if kind in {"unknown", "failed"}: value["counters"][kind] = 1
+ if kind == "attempt": value["counters"]["attemptFailures"] = 1
+ if kind == "coverage": value["coverage"] = "partial"
+ if kind == "diagnostic": value["objectsTruncated"] = False
+ if kind == "key": value["objects"][0]["identity"]["object"] = "objects/b/0000.bin"
+ if kind == "version": value["objects"][0]["identity"]["versionId"] = "other-version"
+ if kind == "incarnation": value["objects"][0]["identity"]["bucketIncarnationId"] = "old-bucket"
+ if kind == "role": value["objects"][1]["disposition"]["state"] = "repaired"
+ if kind == "duplicate": value["objects"][1] = copy.deepcopy(value["objects"][0])
+ with self.subTest(kind=kind), self.assertRaises(ValueError): self.proof(current, healed=5)
+
+ def test_force_accounting_preserves_partial_truth_and_rejects_double_repair(self):
+ old = self.proof(self.state(cancelled=True), cancelled=True)
+ new = self.proof(self.state(replacement=True), healed=3, replacement=True)
+ self.assertEqual(overlap.force_accounting(old, new, self.damaged())["exactTotalRepairs"], 5)
+ for kind in ("count", "incarnation", "duplicate"):
+ changed = copy.deepcopy(new)
+ if kind == "count": changed["canonicalCounters"]["healed"] = 4
+ if kind == "incarnation": changed["bucketIncarnationId"] = "new-bucket"
+ if kind == "duplicate": changed["diagnosticDispositions"][self.expected()[0]["key"]] = "repaired"
+ with self.subTest(kind=kind), self.assertRaises(ValueError): overlap.force_accounting(old, changed, self.damaged())
+ with self.assertRaises(ValueError): self.proof(self.state(), cancelled=True)
+ with self.assertRaises(ValueError): self.proof(self.state(cancelled=True), healed=2)
+
+ def test_terminal_suffix_checks_every_ack_and_never_infers_a_retention_gap(self):
+ current = self.state()
+ current.update(items=[], truncated=True)
+ pages = []
+ for left, right in ((0, 100), (100, 257)):
+ pages.append({**copy.deepcopy(current), "nextSeq": 258, "minSeq": 1, "truncated": right < 257,
+ "items": [{"bucket": self.bucket, "object": entry["key"], "versionId": "", "objectSize": CURSOR_SIZE}
+ for entry in self.expected()[left:right]]})
+ client = types.SimpleNamespace(admin=lambda *args: {"httpStatus": 200, "response": pages.pop(0)})
+ fleet = types.SimpleNamespace(record=lambda label, value: value)
+ with patch.object(overlap, "status", return_value=current):
+ _, proof = overlap.completed_scope(client, fleet, "/scope/", "task", self.bucket, self.expected(), self.damaged(), healed=5)
+ self.assertEqual(proof["completeItemIdentities"], 257)
+ self.assertFalse(proof["retentionGapClaimed"])
+ for kind in ("duplicate", "gap", "empty", "changed-outcome"):
+ page = {**copy.deepcopy(current), "nextSeq": 258, "minSeq": 1, "items": [
+ {"bucket": self.bucket, "object": entry["key"], "versionId": "", "objectSize": CURSOR_SIZE} for entry in self.expected()]}
+ if kind == "duplicate": page["items"][1] = copy.deepcopy(page["items"][0])
+ if kind == "gap": page["minSeq"] = 2
+ if kind == "empty": page["items"] = []
+ if kind == "changed-outcome": page["outcome"]["counters"]["unknown"] = 1
+ client.admin = lambda *args: {"httpStatus": 200, "response": page}
+ with self.subTest(kind=kind), patch.object(overlap, "status", return_value=current), self.assertRaises(ValueError):
+ overlap.completed_scope(client, fleet, "/scope/", "task", self.bucket, self.expected(), self.damaged(), healed=5)
+
+ def test_saved_running_prefix_completes_a_byte_bounded_terminal_report(self):
+ current = self.state(min_seq=57)
+ client = types.SimpleNamespace(admin=lambda path, query, body: {"httpStatus": 200,
+ "response": {**copy.deepcopy(current), "items": []}})
+ recorded = []
+ fleet = types.SimpleNamespace(record=lambda label, value: recorded.append((label, value)) or value)
+ with patch.object(overlap, "status", return_value=current):
+ _, proof = overlap.completed_scope(client, fleet, "/scope/", "task", self.bucket, self.expected(), self.damaged(),
+ healed=5, observations=(self.state(processed=60),))
+ self.assertEqual(proof["completeItemIdentities"], 257)
+ self.assertFalse(proof["retentionGapClaimed"])
+ ledger = next(value for label, value in recorded if label == "overlap-complete-item-ledger")
+ self.assertEqual((ledger["path"], ledger["clientToken"]), ("/scope/", "task"))
+ self.assertEqual([entry["seq"] for entry in ledger["items"]], list(range(1, 258)))
+ client.admin = lambda *args: {"httpStatus": 200, "response": current}
+ for observations in ((), (self.state(processed=55),)):
+ with self.subTest(prefix=len(observations)), patch.object(overlap, "status", return_value=current), \
+ self.assertRaisesRegex(ValueError, "uncollected early item"):
+ overlap.completed_scope(client, fleet, "/scope/", "task", self.bucket, self.expected(), self.damaged(),
+ healed=5, observations=observations)
+
+ def test_item_ledger_rejects_rewritten_sequences_and_cross_scope_identity(self):
+ wanted = overlap.wanted_pairs(self.expected(), count=overlap.SCOPE_OBJECTS)
+ for kind in ("rewrite", "duplicate", "key", "version", "bucket", "size", "bounds", "silent-prefix", "silent-tail"):
+ ledger = {}
+ overlap.collect_scope_items(self.state(processed=60), self.bucket, wanted, ledger)
+ current = self.state()
+ if kind == "rewrite": current["items"][0]["detail"] = "changed"
+ if kind == "duplicate": current["items"][61] = copy.deepcopy(current["items"][0])
+ if kind == "key": current["items"][0]["object"] = "objects/b/0000.bin"
+ if kind == "version": current["items"][0]["versionId"] = "another-version"
+ if kind == "bucket": current["items"][0]["bucket"] = "another-bucket"
+ if kind == "size": current["items"][0]["objectSize"] += 1
+ if kind == "bounds": current["nextSeq"] = 259
+ if kind == "silent-prefix": current = self.state(min_seq=57); current["truncated"] = False
+ if kind == "silent-tail": current["items"].pop()
+ with self.subTest(kind=kind), self.assertRaises(ValueError):
+ overlap.collect_scope_items(current, self.bucket, wanted, ledger)
+
+ def test_cancelled_skip_requires_the_exact_typed_receipt_and_zero_unknowns(self):
+ current = self.state(cancelled=True)
+ current["outcome"]["objects"][-1]["disposition"]["state"] = "cancelled"
+ current["outcome"]["counters"].update(skipped=1, unchanged=97)
+ self.assertEqual(self.proof(current, cancelled=True)["canonicalCounters"]["skipped"], 1)
+ overlap.monotonic(self.state(processed=23), current, cancelled=True)
+ for kind in ("missing", "unknown", "count", "completed"):
+ changed = copy.deepcopy(current)
+ if kind == "missing": changed["outcome"]["objects"][-1]["disposition"]["state"] = "verified_healthy"
+ if kind == "unknown": changed["outcome"]["counters"]["unknown"] = 1
+ if kind == "count": changed["outcome"]["counters"].update(skipped=2, unchanged=96)
+ if kind == "completed": changed["summary"] = "finished"
+ with self.subTest(kind=kind), self.assertRaises(ValueError): self.proof(changed, cancelled=True)
+ with self.assertRaises(ValueError): overlap.counters(current)
+
+ def test_parallel_scope_is_sampled_during_every_completion_poll(self):
+ current = self.state()
+ client = types.SimpleNamespace(admin=lambda *args: {"httpStatus": 200,
+ "response": {**copy.deepcopy(current), "items": []}})
+ fleet = types.SimpleNamespace(record=lambda label, value: value)
+ peer = unittest.mock.Mock()
+ with patch.object(overlap, "status", side_effect=(self.state(processed=60), current)), patch.object(overlap.time, "sleep"):
+ overlap.completed_scope(client, fleet, "/scope/", "task", self.bucket, self.expected(), self.damaged(),
+ healed=5, sample_other=peer)
+ self.assertEqual(peer.call_count, 2)
+
+ def test_disjoint_and_replacement_keep_observations_bound_to_their_tokens(self):
+ primary = self.expected()
+ other = copy.deepcopy(primary)
+ for entry in other: entry["key"] = entry["key"].replace("objects/a/child/", "objects/b/")
+ targets = [entries[index] for entries in (primary, other) for index in overlap.MISSING]
+ before, after = self.state(processed=23), self.state(processed=40)
+ new_before, new_after = self.state(processed=70), self.state(processed=80)
+ old_final = self.state(cancelled=True)
+ samples = [self.state(processed=90), self.state()]
+ calls = []
+ def complete(probe, fleet, path, token, bucket, expected, damaged, **options):
+ if options.get("sample_other"):
+ options["sample_other"](); options["sample_other"]()
+ calls.append((path, token, list(options["observations"])))
+ state = self.state(replacement=options.get("replacement", False))
+ return state, self.proof(state, healed=options["healed"], replacement=options.get("replacement", False))
+ for branch in ("disjoint", "force-start"):
+ calls.clear()
+ active = []
+ def admit(probe, fleet, path, identities, **options):
+ token = "new" if identities else "old"
+ identities.append((path, token))
+ return token
+ def observe(probe, fleet, path, token, label):
+ if label == "original-after-disjoint-start": return after
+ if label == "overlap-disjoint-parallel-status": return samples[min(len(peer_samples), 1)]
+ if label in {"force-old-initial-cancellation", "force-old-settled"}: return old_final
+ if label == "force-new-after-old-stop": return new_after
+ if label == "force-new-drain": return samples.pop(0)
+ return self.state(replacement=branch == "force-start" and token == "new", cancelled=branch == "force-start" and token == "old")
+ peer_samples = []
+ def record(label, value):
+ if label == "overlap-disjoint-parallel-status": peer_samples.append(value)
+ return value
+ # status() is mocked, so its recording hook is bypassed.
+ def observed(*args):
+ value = observe(*args)
+ if args[-1] == "overlap-disjoint-parallel-status": peer_samples.append(value)
+ return value
+ fleet = types.SimpleNamespace(nodes=["node"], record=record,
+ snapshot=lambda *args: {"node": {"pid": 1, "unit": "unit"}})
+ probe = types.SimpleNamespace(admin=self.retired_stop)
+ samples = [self.state(processed=90), self.state()]
+ with self.subTest(branch=branch), patch.object(overlap, "start", side_effect=admit), \
+ patch.object(overlap, "wait_window", side_effect=(before, new_before)), \
+ patch.object(overlap, "status", side_effect=observed), patch.object(overlap, "completed_scope", side_effect=complete), \
+ patch.object(overlap, "idle_fleet", return_value={}), \
+ patch.object(overlap, "physical_content", return_value={}), patch.object(overlap.time, "sleep"):
+ result = overlap.observe_branch(branch, probe, fleet, {}, self.bucket, primary + other, targets, active, original={})
+ self.assertEqual(active, [])
+ self.assertEqual(calls[-1][1], "new")
+ if branch == "disjoint":
+ self.assertTrue(calls[0][0].endswith("objects/a/"))
+ self.assertTrue(calls[-1][0].endswith("objects/b/"))
+ self.assertEqual(calls[0][2], [before, after])
+ self.assertEqual(calls[-1][2], [new_before] + peer_samples)
+ else:
+ self.assertEqual(result[branch]["oldStopIsolation"]["exactRetiredTaskId"], "old")
+ self.assertEqual(calls[-1][2][:2], [new_before, new_after])
+ self.assertEqual(len(calls[-1][2]), 4)
+
+ def test_force_start_rejects_unknown_or_malformed_retired_stop_before_completion(self):
+ primary = self.expected()
+ targets = [primary[index] for index in overlap.MISSING]
+ for kind in ("unknown", "token", "duplicate-token", "method", "body", "transport",
+ "settings", "summary", "timestamp", "extra-field"):
+ active = []
+ def admit(probe, fleet, path, identities, **options):
+ token = "new" if identities else "old"
+ identities.append((path, token))
+ return token
+ def response(path, query, body):
+ observed = self.retired_stop(path, query, body)
+ if kind == "unknown":
+ observed.update(httpStatus=500, response={"Code": "InternalError",
+ "Message": "Heal task not found: old"})
+ if kind == "token": observed["query"] = [("clientToken", "new"), ("forceStop", "true")]
+ if kind == "duplicate-token": observed["query"] += [("clientToken", "new")]
+ if kind == "method": observed["method"] = "GET"
+ if kind == "body": observed["requestBody"] = "{}"
+ if kind == "transport": observed["transportError"] = "timeout"
+ if kind == "settings": observed["response"]["settings"]["recursive"] = True
+ if kind == "summary": observed["response"]["summary"] = "finished"
+ if kind == "timestamp": observed["response"]["startTime"] = "2026-10-23T02:00:00"
+ if kind == "extra-field": observed["response"]["clientToken"] = "old"
+ return observed
+ fleet = types.SimpleNamespace(record=lambda label, value: value)
+ probe = types.SimpleNamespace(admin=response)
+ with self.subTest(kind=kind), patch.object(overlap, "start", side_effect=admit), \
+ patch.object(overlap, "wait_window", side_effect=(self.state(processed=23), self.state(processed=70))), \
+ patch.object(overlap, "status", return_value=self.state(cancelled=True)) as status_call, \
+ patch.object(overlap, "completed_scope") as completion, \
+ self.assertRaises(ValueError):
+ overlap.observe_branch("force-start", probe, fleet, {}, self.bucket, primary, targets, active, original={})
+ completion.assert_not_called()
+ self.assertEqual(status_call.call_count, 1)
+ self.assertEqual([token for _, token in active], ["old", "new"])
+
+ def test_finished_window_and_same_token_counter_rollback_cannot_pass(self):
+ self.assertFalse(overlap.active_window(self.state()))
+ before = self.state(cancelled=True)
+ after = copy.deepcopy(before)
+ after["outcome"]["counters"].update(processed=99, unchanged=97)
+ with self.assertRaisesRegex(ValueError, "backwards"): overlap.monotonic(before, after)
+ with patch.object(overlap, "status", return_value=self.state()), self.assertRaisesRegex(ValueError, "NOT_OBSERVED"):
+ overlap.wait_window(None, None, "/scope/", "task")
+
+ def test_all_eight_source_instances_and_cleanup_failure_stop_are_real_dispatches(self):
+ with tempfile.TemporaryDirectory() as folder:
+ args = types.SimpleNamespace(evidence=Path(folder))
+ calls = []
+ def topology(params, topology, nodes, **options):
+ calls.append((topology, options["branches"][0], options["missing_indices"]))
+ return {"verdict": "PASS", "cleanup": "PASS"}
+ with patch.object(overlap, "dx_topology", side_effect=topology):
+ for name in ("H1", "H2"):
+ self.assertEqual(overlap.run_topology(args, name, ["node1"])["verdict"], "PASS")
+ self.assertEqual([(name, branch) for name, branch, _ in calls], [(name, branch) for name in ("H1", "H2") for branch in overlap.BRANCHES])
+ self.assertTrue(all(len(indices) == (10 if branch == "disjoint" else 5) for _, branch, indices in calls))
+ with patch.object(overlap, "dx_topology", return_value={"verdict": "FAIL", "cleanup": "FAIL"}) as operation:
+ self.assertEqual(overlap.run_topology(args, "H1", ["node1"])["cleanup"], "FAIL")
+ self.assertEqual(operation.call_count, 1)
+ self.assertEqual(list(overlap.BRANCHES), select("A05")[0]["variants"])
+ self.assertEqual(runtime_config("overlap")["RUSTFS_HEAL_OVERLAP_POLICY"], "merge")
+ # Parameterizing A05 physical fixtures must not weaken J03 retention.
+ with self.assertRaises(ValueError): Window(self.bucket, self.expected(), "task", "node1", "epoch")
+
+
+class HealRootRestartOracleTest(unittest.TestCase):
+ token = "01234567-89ab-4def-8123-456789abcdef"
+
+ def intent(self):
+ return {"schema": 3, "task_id": self.token, "heal_type": {"type": "cluster"}, "priority": "High",
+ "created_at": {"secs_since_epoch": 10, "nanos_since_epoch": 0}, "retry_attempts": 0,
+ "options": {"scan_mode": 2, "remove_corrupted": False, "recreate_missing": False,
+ "update_parity": False, "recursive": True, "dry_run": False, "no_lock": False,
+ "pool_index": None, "set_index": None, "timeout": None}}
+
+ def records(self):
+ entries = [{"disk": disk, "kind": kind, "present": disk == 2 and kind == "intent",
+ "relativePath": ".rustfs.sys/root-heal-" + self.token + ".json", "value": self.intent()}
+ for disk in range(4) for kind in ("intent", "terminal", "report")]
+ return {"node1": {"readOnly": True, "taskId": self.token, "records": entries}}
+
+ def process(self, pid=10, start=50):
+ return {name: value for name, value in zip(fleet_node.PROCESS_FIELDS,
+ (pid, start, "boot", "unit", "/state/rustfs", "sha", "mnt:[1]", "command", "env", {"budget": "300"}), strict=True)} | {"state": "T"}
+
+ def test_root_owner_rejects_duplicate_missing_and_cross_scope_records(self):
+ self.assertEqual(root_restart.root_intent(self.records(), self.token, "node1")["disk"], 2)
+ for kind in ("missing", "duplicate", "scope", "schema", "selector", "read-repair", "retry", "coverage"):
+ observed = self.records()
+ entries = observed["node1"]["records"]
+ entry = next(row for row in entries if row["present"])
+ if kind == "missing": entry["present"] = False
+ if kind == "duplicate": entries[0]["present"] = True
+ if kind == "scope": entry["value"]["heal_type"] = {"type": "prefix", "bucket": "other", "prefix": "objects/"}
+ if kind == "schema": entry["value"]["schema"] = 2
+ if kind == "selector": entry["value"]["options"]["pool_index"] = 0
+ if kind == "read-repair": entry["value"]["options"]["no_lock"] = True
+ if kind == "retry": entry["value"]["retry_attempts"] = 1
+ if kind == "coverage": entries.pop()
+ with self.subTest(kind=kind), self.assertRaises(ValueError): root_restart.root_intent(observed, self.token, "node1")
+
+ def test_checkpoint_budget_cannot_be_removed_refunded_or_move_owner(self):
+ before = root_restart.root_intent(self.records(), self.token, "node1")
+ after = copy.deepcopy(before)
+ after["value"]["options"]["timeout"] = {"secs": 280, "nanos": 7}
+ proof = root_restart.same_owner(before, after)
+ self.assertEqual(proof["checkpointRemainingTimeoutNs"], 280_000_000_007)
+ for kind in ("absent", "refund", "zero", "disk", "identity", "scope", "option", "bool", "nanos"):
+ current = copy.deepcopy(after)
+ if kind == "absent": current["value"]["options"]["timeout"] = None
+ if kind == "refund": current["value"]["options"]["timeout"]["secs"] = 301
+ if kind == "zero": current["value"]["options"]["timeout"] = {"secs": 0, "nanos": 0}
+ if kind == "disk": current["disk"] = 3
+ if kind == "identity": current["value"]["task_id"] = "new-task"
+ if kind == "scope": current["value"]["heal_type"] = {"type": "bucket", "bucket": "other"}
+ if kind == "option": current["value"]["options"]["remove_corrupted"] = True
+ if kind == "bool": current["value"]["options"]["timeout"]["secs"] = True
+ if kind == "nanos": current["value"]["options"]["timeout"]["nanos"] = 1_000_000_000
+ with self.subTest(kind=kind), self.assertRaises(ValueError): root_restart.same_owner(before, current)
+ with self.assertRaises(ValueError): root_restart.same_owner(after, before)
+
+ def test_only_original_coordinator_changes_birth_identity(self):
+ before = {"node1": self.process(), "node2": self.process(11, 51)}
+ after = {"node1": self.process(12, 60), "node2": self.process(11, 51)}
+ self.assertTrue(root_restart.restart_identity(before, after, "node1")["onlyCoordinatorProcessChanged"])
+ for kind in ("no-restart", "peer-restart", "boot", "env", "namespace", "candidate", "missing"):
+ current = copy.deepcopy(after)
+ if kind == "no-restart": current["node1"] = copy.deepcopy(before["node1"])
+ if kind == "peer-restart": current["node2"]["startTicks"] += 1
+ if kind == "boot": current["node1"]["bootId"] = "new-boot"
+ if kind == "env": current["node1"]["environmentFileSha256"] = "new-env"
+ if kind == "namespace": current["node1"]["mountNamespace"] = "mnt:[2]"
+ if kind == "candidate": current["node1"]["binarySha256"] = "new-build"
+ if kind == "missing": del current["node2"]
+ with self.subTest(kind=kind), self.assertRaises(ValueError): root_restart.restart_identity(before, current, "node1")
+
+ def test_pidfd_revalidates_birth_identity_before_any_signal(self):
+ expected = self.process()
+ with patch.object(fleet_node.os, "pidfd_open", return_value=17, create=True), \
+ patch.object(fleet_node.signal, "pidfd_send_signal", create=True) as send, \
+ patch.object(fleet_node.os, "close") as close, patch.object(fleet_node, "process", return_value=expected):
+ fleet_node.pidfd_signal({"expected": expected}, fleet_node.signal.SIGSTOP)
+ send.assert_called_once_with(17, fleet_node.signal.SIGSTOP, None, 0)
+ close.assert_called_once_with(17)
+ changed = {**expected, "startTicks": expected["startTicks"] + 1}
+ with patch.object(fleet_node.os, "pidfd_open", return_value=17, create=True), \
+ patch.object(fleet_node.signal, "pidfd_send_signal", create=True) as send, \
+ patch.object(fleet_node.os, "close") as close, patch.object(fleet_node, "process", return_value=changed), self.assertRaisesRegex(ValueError, "stale"):
+ fleet_node.pidfd_signal({"expected": expected}, fleet_node.signal.SIGSTOP)
+ send.assert_not_called(); close.assert_called_once_with(17)
+ with self.assertRaises(ValueError): fleet_node.process_signal({"signal": "SIGKILL", "expected": expected})
+
+ def test_graceful_exit_requires_actual_exit_zero_and_preserves_identity(self):
+ before = self.process()
+ exited = {"pid": 0, "bootId": "boot", "binarySha256": "sha", "environmentFileSha256": "env",
+ "service": {"ExecMainCode": "1", "ExecMainStatus": "0", "Result": "success"}}
+ for kind in ("good", "signal", "failed", "timeout", "boot", "candidate", "environment"):
+ current = copy.deepcopy(exited)
+ if kind == "signal": current["service"]["ExecMainCode"] = "2"
+ if kind == "failed": current["service"]["ExecMainStatus"] = "1"
+ if kind == "timeout": current["service"]["Result"] = "timeout"
+ if kind == "boot": current["bootId"] = "new-boot"
+ if kind == "candidate": current["binarySha256"] = "new-build"
+ if kind == "environment": current["environmentFileSha256"] = "new-env"
+ with self.subTest(kind=kind), patch.object(fleet_node, "owned", return_value=(None, None, {"runtimeProfile": "root-restart"})), \
+ patch.object(fleet_node, "process", side_effect=[before, current]), patch.object(fleet_node, "pidfd_signal", return_value=before):
+ if kind == "good":
+ self.assertEqual(fleet_node.process_graceful({"runId": "run", "expected": before})["actualExitCode"], 0)
+ else:
+ with self.assertRaises(ValueError): fleet_node.process_graceful({"runId": "run", "expected": before})
+
+ def test_recovery_absence_is_bounded_observation_and_never_completion(self):
+ missing = {"httpStatus": 500, "response": {"Code": "InternalError", "Message": "Heal task not found: " + self.token}}
+ good = {"httpStatus": 200, "response": {"summary": "running"}}
+ client = types.SimpleNamespace(admin=Mock(side_effect=[missing, good]))
+ fleet = types.SimpleNamespace(record=lambda name, value: value)
+ with patch.object(root_restart.time, "sleep"):
+ self.assertEqual(root_restart.recovery_status(client, fleet, self.token)["summary"], "running")
+ client.admin = lambda *args: missing
+ with patch.object(root_restart.time, "monotonic", side_effect=[0, 16]), self.assertRaisesRegex(ValueError, "15-second"):
+ root_restart.recovery_status(client, fleet, self.token)
+ self.assertEqual(list(root_restart.BRANCHES), select("F01")[0]["variants"])
+
+ def test_completed_root_journal_retires_intent_and_preserves_checkpoint_options(self):
+ owner = root_restart.root_intent(self.records(), self.token, "node1")
+ owner["value"]["options"]["timeout"] = {"secs": 280, "nanos": 0}
+ expected = HealCursorOracleTest().expected()
+ bucket = HealCursorOracleTest.bucket
+ current = HealCursorOracleTest().terminal()
+ terminal = {"schema": 1, "task_id": self.token, "heal_type": {"type": "cluster"}, "status": "Completed",
+ "options": owner["value"]["options"]}
+ report = {"schema": 1, "terminal": copy.deepcopy(terminal), "outcome": current["outcome"],
+ "next_seq": 1027, "min_seq": 3, "seqed_items": [[index + 2,
+ {"bucket": bucket, "object": entry["key"], "versionId": "", "objectSize": CURSOR_SIZE}]
+ for index, entry in enumerate(expected[1:], 1)]}
+ observed = self.records()
+ for entry in observed["node1"]["records"]:
+ entry["present"] = entry["disk"] == 2 and entry["kind"] in {"terminal", "report"}
+ if entry["present"]: entry["value"] = terminal if entry["kind"] == "terminal" else report
+ self.assertTrue(root_restart.root_terminal(observed, self.token, owner, current, bucket, expected)["runnableIntentRetiredOnEveryDrive"])
+ for kind in ("intent", "owner", "scope", "state", "refund", "outcome", "duplicate", "wrong-version", "wrong-size"):
+ changed = copy.deepcopy(observed)
+ rows = changed["node1"]["records"]
+ end = next(row for row in rows if row["present"] and row["kind"] == "terminal")
+ durable = next(row for row in rows if row["present"] and row["kind"] == "report")
+ if kind == "intent": rows[0]["present"] = True
+ if kind == "owner": end["disk"] = 3
+ if kind == "scope": end["value"]["heal_type"] = {"type": "bucket", "bucket": bucket}
+ if kind == "state": end["value"]["status"] = "Cancelled"
+ if kind == "refund": end["value"]["options"]["timeout"]["secs"] = 300
+ if kind == "outcome": durable["value"]["outcome"]["counters"]["unknown"] = 1
+ if kind == "duplicate": durable["value"]["seqed_items"][1][1] = copy.deepcopy(durable["value"]["seqed_items"][0][1])
+ if kind == "wrong-version": durable["value"]["seqed_items"][0][1]["versionId"] = "unacknowledged"
+ if kind == "wrong-size": durable["value"]["seqed_items"][0][1]["objectSize"] += 1
+ with self.subTest(kind=kind), self.assertRaises(ValueError): root_restart.root_terminal(changed, self.token, owner, current, bucket, expected)
+
+ def test_journal_reads_mark_the_actual_coordinator_as_stopped(self):
+ calls = []
+ fleet = types.SimpleNamespace(nodes=["node1", "node2", "node3", "node4"], record=lambda name, value: value,
+ remote=lambda node, action, **params: calls.append((node, params["quiescent"])) or {})
+ root_restart.records(fleet, self.token, "checkpoint", stopped_node="node3")
+ self.assertEqual(calls, [("node1", False), ("node2", False), ("node3", True), ("node4", False)])
+
+ def test_cleanup_resumes_only_the_proved_original_process_before_finalization(self):
+ fleet = Fleet(["node1"], Path("/tmp/unit-root-restart"), "run")
+ fleet.frozen_processes = {"node1": self.process()}
+ fleet.reserved = ["node1"]
+ calls = []
+ def remote(node, action, **kwargs):
+ calls.append((action, kwargs))
+ return {"pid": 10} if action == "process" else {}
+ fleet.remote = remote
+ fleet.record = lambda name, value: value
+ self.assertTrue(fleet.cleanup())
+ self.assertEqual([action for action, _ in calls[:2]], ["process", "process-signal"])
+ self.assertEqual(calls[1][1]["signal"], "SIGCONT")
+ self.assertEqual(calls[1][1]["expected"]["startTicks"], 50)
+ self.assertFalse(fleet.frozen_processes)
+ fleet.frozen_processes = {"node1": self.process()}
+ def stale(node, action, **kwargs):
+ calls.append((action, kwargs))
+ if action == "process-signal": raise ValueError("stale process")
+ return {"pid": 10} if action == "process" else {}
+ fleet.remote = stale; calls.clear()
+ self.assertFalse(fleet.cleanup())
+ self.assertFalse(any(action == "cleanup" for action, _ in calls))
+
+
+class HealReadQuorumBoundaryTest(unittest.TestCase):
+ contract = {"recognized": True, "writeQuorumRule": "data-plus-one-if-equal-parity"}
+ bucket = "rv-heal-0123456789abcdef"
+
+ def scene(self, branch="exact-r-read", *, total=4, data=2):
+ original = HealShardOracleTest().snapshot()["node1"]
+ snapshot = {}
+ disk_ids = [f"disk-{index}" for index in range(total)]
+ key = "boundary/" + branch + ".bin"
+ for slot in range(total):
+ node, disk = "node" + str(slot // 4 + 1), slot % 4
+ value = snapshot.setdefault(node, {"formats": [], "files": []})
+ value["formats"].append({"disk": disk, "value": {"format": "xl", "id": "pool",
+ "xl": {"sets": [disk_ids], "this": disk_ids[slot]}}})
+ metadata = copy.deepcopy(original["files"][0]["metadata"])
+ metadata["key"] = key
+ version = metadata["versions"][0]
+ version["role"] = "data" if slot < data else "parity"
+ version["erasure"].update(index=slot + 1, data_blocks=data, parity_blocks=total - data,
+ distribution=list(range(1, total + 1)))
+ value["files"].extend([
+ {"disk": disk, "path": f"{self.bucket}/{key}/xl.meta", "size": 600,
+ "sha256": "metadata-" + str(slot), "metadata": metadata},
+ {"disk": disk, "path": f"{self.bucket}/{key}/{version['dataDir']}/part.1", "size": 82,
+ "sha256": "shard-" + str(slot)}])
+ expected = [{"key": key, "versionId": "null", "deleteMarker": False, "isLatest": True,
+ "payload": {"size": 100, "sha256": "acknowledged"}}]
+ return snapshot, expected
+
+ def plan(self, branch="exact-r-read", **kwargs):
+ snapshot, expected = self.scene(branch, **kwargs)
+ return snapshot, quorum_boundary.boundary_plan(branch, snapshot, self.bucket, expected, self.contract)
+
+ def missing(self, snapshot, targets):
+ changed = copy.deepcopy(snapshot)
+ for target in targets:
+ changed[target["node"]]["files"] = [entry for entry in changed[target["node"]]["files"]
+ if (entry["disk"], entry["path"]) != (target["disk"], target["relativePath"])]
+ return changed
+
+ def test_source_quorum_is_distinct_from_writable_drives_and_host_locks(self):
+ for total, data, write, clients in ((4, 2, 3, 1), (16, 12, 12, 4), (16, 8, 9, 4)):
+ snapshot, plan = self.plan("exact-r-heal-write-limited", total=total, data=data)
+ self.assertEqual((plan["readQuorum"], plan["writeQuorum"], plan["writableTargets"]), (data, write, total))
+ self.assertEqual(plan["lock"]["clientCount"], clients)
+ self.assertEqual(plan["lock"]["exclusiveQuorum"], clients // 2 + 1)
+ self.assertFalse(plan["insufficientWriteOrLockApplicable"])
+ witness = quorum_boundary.source_witness(self.missing(snapshot, plan["faults"]), plan, plan["faults"])
+ self.assertEqual(witness["sameVersionPayloadSources"], data)
+
+ def test_restore_to_w_leaves_real_targets_and_does_not_count_restored_sources_as_heal(self):
+ for total, data, restored, remaining in ((4, 2, 2, 1), (16, 12, 1, 4), (16, 8, 2, 7)):
+ snapshot, plan = self.plan("restored-w", total=total, data=data)
+ self.assertEqual(len(plan["retained"]), data - 1)
+ self.assertEqual(plan["restoreCount"], restored)
+ targets = plan["faults"][restored:]
+ self.assertEqual(len(targets), remaining)
+ after = self.missing(snapshot, targets)
+ self.assertEqual(quorum_boundary.source_witness(after, plan, targets)["sameVersionPayloadSources"], plan["writeQuorum"])
+
+ def test_source_witness_rejects_changed_generation_extra_sources_and_good_shard_mutation(self):
+ snapshot, plan = self.plan()
+ faulted = self.missing(snapshot, plan["faults"])
+ for kind in ("reappeared", "retained-bytes", "metadata", "foreign-target", "duplicate-target"):
+ changed, targets = copy.deepcopy(faulted), copy.deepcopy(plan["faults"])
+ if kind == "reappeared": changed = self.missing(snapshot, targets[1:])
+ elif kind == "retained-bytes": next(entry for entry in changed["node1"]["files"] if entry["path"].endswith("/part.1"))["sha256"] = "corrupt"
+ elif kind == "metadata": changed["node1"]["files"][0]["sha256"] = "another-generation"
+ elif kind == "foreign-target": targets[0]["relativePath"] += ".wrong-version"
+ else: targets.append(copy.deepcopy(targets[0]))
+ with self.subTest(kind=kind), self.assertRaises(ValueError):
+ quorum_boundary.source_witness(changed, plan, targets)
+
+ def test_read_boundary_requires_complete_acknowledged_body_and_records_interference(self):
+ snapshot, plan = self.plan()
+ expected = plan["expected"][0]
+ good = {"acknowledged": True, "resultUnknown": False, "payload": expected["payload"],
+ "response": {"ResponseMetadata": {"HTTPStatusCode": 200}}}
+ quorum_boundary.read_observation(good, expected)
+ for field, value in (("acknowledged", False), ("resultUnknown", True), ("payload", {"size": 0, "sha256": "empty"})):
+ with self.subTest(field=field), self.assertRaises(ValueError):
+ quorum_boundary.read_observation({**good, field: value}, expected)
+ faulted = self.missing(snapshot, plan["faults"])
+ self.assertFalse(quorum_boundary.read_interference(faulted, faulted, plan)["readRepairObserved"])
+ proof = quorum_boundary.read_interference(faulted, snapshot, plan)
+ self.assertTrue(proof["readRepairObserved"])
+ self.assertFalse(proof["manualRepairClaimed"])
+ changed = copy.deepcopy(snapshot)
+ target = plan["faults"][0]
+ next(entry for entry in changed[target["node"]]["files"] if entry["disk"] == target["disk"]
+ and entry["path"] == target["relativePath"])["sha256"] = "bad-reconstruction"
+ with self.assertRaises(ValueError): quorum_boundary.read_interference(faulted, changed, plan)
+
+ def test_recovery_keeps_unresolved_registration_when_backup_oracle_is_rejected(self):
+ registration = ("node1", {"disk": 0, "faultId": "B06-exact-r-read-0"})
+ fleet = Mock(faults=[registration])
+ fleet.remote.return_value = {"physicalRepairPassAllowed": False, "restoredFromBackup": True}
+ fleet.record.side_effect = lambda label, value: value
+ registrations = [registration]
+ quorum_boundary.recover_registered(fleet, registrations, "rescue")
+ self.assertFalse(fleet.faults or registrations)
+ fleet.faults, registrations = [registration], [registration]
+ fleet.remote.return_value = {"physicalRepairPassAllowed": True}
+ with self.assertRaises(ValueError): quorum_boundary.recover_registered(fleet, registrations, "rescue")
+ self.assertEqual(fleet.faults, registrations)
+
+ def test_branch_and_geometry_contract_cannot_be_weakened(self):
+ self.assertEqual(list(quorum_boundary.BRANCHES), select("B06")[0]["variants"])
+ snapshot, expected = self.scene()
+ for contract in ({}, {"recognized": True, "writeQuorumRule": "unknown"}):
+ with self.assertRaises(ValueError): quorum_boundary.boundary_plan("exact-r-read", snapshot, self.bucket, expected, contract)
+ with self.assertRaises(ValueError): quorum_boundary.boundary_plan("exact-r-read", snapshot, self.bucket, expected * 2, self.contract)
+
+
+class HealStaleRejoinTest(unittest.TestCase):
+ def scene(self):
+ before, bucket, expected = versioned_scene(["objects/0001.bin", "control/object.bin"])
+ for key in {entry["key"] for entry in expected}:
+ for nonce, entry in enumerate(entry for entry in expected if entry["key"] == key): entry["payloadNonce"] = nonce
+ target = map_target(before, bucket, "objects/0001.bin", "data", 1, "v0", latest_required=False)
+ manifest = [entry for entry in expected if (entry["key"], entry["versionId"]) != (target["key"], "v0")]
+ authority = copy.deepcopy(before)
+ member = target["node"], target["disk"]
+ for node, value in authority.items():
+ for entry in value["files"]:
+ if (node, entry["disk"]) == member: continue
+ metadata = entry.get("metadata")
+ if metadata and metadata["key"] == target["key"]:
+ metadata["versions"] = [version for version in metadata["versions"] if version["versionId"] != "v0"]
+ entry["sha256"] += "-deleted-v0"
+ value["files"] = [entry for entry in value["files"] if (node, entry["disk"]) == member
+ or not entry["path"].startswith(f"{bucket}/{target['key']}/12345678-1234-1234-1234-000000000000/")]
+ witness = stale_rejoin.authority_witness(before, authority, bucket, target["key"], manifest, target)
+ after = copy.deepcopy(authority)
+ for entry in after[member[0]]["files"]:
+ if entry["disk"] == member[1] and entry.get("metadata", {}).get("key") == target["key"]:
+ entry["metadata"]["versions"] = [version for version in entry["metadata"]["versions"] if version["versionId"] != "v0"]
+ entry["sha256"] += "-deleted-v0"
+ return before, authority, after, bucket, manifest, target, witness
+
+ def test_same_disk_stale_generation_requires_full_online_ack_authority(self):
+ before, authority, after, bucket, expected, target, witness = self.scene()
+ self.assertEqual((witness["readQuorum"], witness["writeQuorum"], witness["onlineAuthorities"]), (2, 3, 3))
+ cohorts, proof = stale_rejoin.physical_rejoin(authority, after, bucket, target["key"], expected, target, witness)
+ self.assertEqual({cohort["versionId"] for cohort in cohorts}, {"v1", "v2"})
+ self.assertFalse(proof["backupRestoreUsed"])
+ for kind in ("offline-write", "lost-online-version", "no-stale-generation", "control", "format"):
+ changed = copy.deepcopy(authority)
+ if kind == "offline-write": next(entry for entry in changed[target["node"]]["files"] if entry["disk"] == target["disk"])["sha256"] = "changed"
+ elif kind == "lost-online-version":
+ next(entry for entry in changed["node1"]["files"] if entry["disk"] != target["disk"]
+ and entry.get("metadata", {}).get("key") == target["key"])["metadata"]["versions"].pop()
+ elif kind == "no-stale-generation": changed = copy.deepcopy(after)
+ elif kind == "control": next(entry for entry in changed["node1"]["files"] if "/control/" in entry["path"])["sha256"] = "changed"
+ else: changed["node1"]["formats"][0]["value"]["id"] = "foreign"
+ with self.subTest(kind=kind), self.assertRaises(ValueError):
+ stale_rejoin.authority_witness(before, changed, bucket, target["key"], expected, target)
+
+ def test_physical_convergence_cannot_change_retained_shards_or_quorum_controls(self):
+ _, authority, after, bucket, expected, target, witness = self.scene()
+ for kind in ("retained-target-shard", "quorum-shard", "control", "resurrected-metadata", "extra-file"):
+ changed = copy.deepcopy(after)
+ if kind in {"retained-target-shard", "quorum-shard"}:
+ entry = next(entry for entry in changed["node1"]["files"] if entry["path"].endswith("/part.1")
+ and target["key"] in entry["path"] and "000000000001" in entry["path"]
+ and (entry["disk"] == target["disk"]) == (kind == "retained-target-shard"))
+ entry["sha256"] = "corrupt"
+ elif kind == "control": next(entry for entry in changed["node1"]["files"] if "/control/" in entry["path"])["sha256"] = "changed"
+ elif kind == "resurrected-metadata":
+ entry = next(entry for entry in changed["node1"]["files"] if entry["disk"] == target["disk"] and entry.get("metadata", {}).get("key") == target["key"])
+ entry["metadata"] = copy.deepcopy(next(entry for entry in authority["node1"]["files"] if entry["disk"] == target["disk"] and entry.get("metadata", {}).get("key") == target["key"])["metadata"])
+ else: changed["node1"]["files"].append({"disk": 0, "path": f"{bucket}/unrelated/new.bin", "size": 1, "sha256": "extra"})
+ with self.subTest(kind=kind), self.assertRaises(ValueError):
+ stale_rejoin.physical_rejoin(authority, changed, bucket, target["key"], expected, target, witness)
+
+ def test_historical_delete_keeps_original_payload_nonce_and_exact_ack(self):
+ _, _, _, bucket, expected_after, target, _ = self.scene()
+ _, _, expected = versioned_scene([target["key"]])
+ for nonce, entry in enumerate(expected): entry["payloadNonce"] = nonce
+ recorder = Mock()
+ recorder.call.return_value = {"VersionId": "v0", "ResponseMetadata": {"HTTPStatusCode": 204}}
+ manifest, ack = stale_rejoin.transition(recorder, bucket, "delete-historical-version", target["key"], expected)
+ self.assertEqual([(entry["versionId"], entry["payloadNonce"]) for entry in manifest], [("v1", 1), ("v2", 2)])
+ self.assertEqual(ack["deletedVersionId"], "v0")
+ self.assertFalse(ack["backupRestorationAllowed"])
+ for changed in ({"VersionId": "v1", "ResponseMetadata": {"HTTPStatusCode": 204}},
+ {"VersionId": "v0", "DeleteMarker": True, "ResponseMetadata": {"HTTPStatusCode": 204}}):
+ recorder.call.return_value = changed
+ with self.assertRaises(ValueError): stale_rejoin.transition(recorder, bucket, "delete-historical-version", target["key"], expected)
+
+ def test_overwrite_uses_new_nonce_and_marker_preserves_all_acknowledged_versions(self):
+ key, bucket = "objects/0001.bin", "rv-heal-0123456789abcdef"
+ old = [{"key": key, "versionId": "null", "deleteMarker": False, "isLatest": True,
+ "payload": {"size": 100, "sha256": "old"}, "payloadNonce": 0}]
+ recorder = Mock()
+ recorder.call.return_value = {"ResponseMetadata": {"HTTPStatusCode": 200}}
+ manifest, ack = stale_rejoin.transition(recorder, bucket, "unversioned-overwrite", key, old)
+ self.assertEqual(manifest[0]["payloadNonce"], 1)
+ self.assertEqual(manifest[0]["payload"]["sha256"], hashlib.sha256(stale_rejoin.payload(f"{bucket}/{key}/1", 100)).hexdigest())
+ self.assertEqual(old[0]["payload"]["sha256"], "old")
+ before, _, expected = versioned_scene([key])
+ recorder.call.return_value = {"VersionId": "new-marker", "DeleteMarker": True, "ResponseMetadata": {"HTTPStatusCode": 204}}
+ manifest, ack = stale_rejoin.transition(recorder, bucket, "delete-marker", key, expected)
+ self.assertEqual(len(manifest), 4)
+ self.assertTrue(manifest[-1]["isLatest"] and manifest[-1]["deleteMarker"])
+ self.assertTrue(all(not entry["isLatest"] for entry in manifest[:-1]))
+
+ def test_stale_receipts_reject_unknown_resurrection_and_false_convergence(self):
+ _, _, _, bucket, expected, target, _ = self.scene()
+ selected = [entry for entry in expected if entry["key"] == target["key"]]
+ state = HealHealthyOracleTest().receipt("repaired")
+ template = state["outcome"]["objects"][0]
+ receipts = []
+ for entry in selected:
+ receipt = copy.deepcopy(template)
+ receipt["identity"].update(bucket=bucket, object=target["key"], versionId=entry["versionId"])
+ receipt["disposition"] = {"state": "repaired" if entry["versionId"] == "v1" else "verified_healthy"}
+ receipts.append(receipt)
+ absent = copy.deepcopy(template)
+ absent["identity"].update(bucket=bucket, object=target["key"], versionId="v0")
+ absent["disposition"] = {"state": "authoritatively_absent"}
+ state["outcome"]["objects"] = [*receipts, absent]
+ state["outcome"]["counters"].update(processed=3, healed=1, unchanged=2)
+ self.assertTrue(stale_rejoin.responsibility(state, bucket, target["key"], expected, deleted="v0")["completeRetainedAndOptionalDeletedVersionReceipts"])
+ for kind in ("unknown", "live-deleted", "no-repair", "missing-retained", "incarnation", "wrong-count"):
+ changed = copy.deepcopy(state)
+ if kind == "unknown": changed["outcome"]["objects"][0]["disposition"]["state"] = "unknown"
+ elif kind == "live-deleted": changed["outcome"]["objects"][-1]["disposition"]["state"] = "verified_healthy"
+ elif kind == "no-repair": changed["outcome"]["objects"][0]["disposition"]["state"] = "verified_healthy"
+ elif kind == "missing-retained": changed["outcome"]["objects"].pop(1)
+ elif kind == "incarnation": changed["outcome"]["objects"][1]["identity"]["bucketIncarnationId"] = "foreign"
+ else: changed["outcome"]["counters"]["healed"] += 1
+ with self.subTest(kind=kind), self.assertRaises(ValueError):
+ stale_rejoin.responsibility(changed, bucket, target["key"], expected, deleted="v0")
+
+ def test_stale_runtime_and_source_branches_are_explicit(self):
+ self.assertEqual(list(stale_rejoin.BRANCHES), select("C06")[0]["variants"])
+ config = runtime_config("stale-rejoin")
+ self.assertEqual(config["RUSTFS_HEAL_DANGLING_DELETE_GRACE_SECS"], "0")
+ self.assertEqual(config["RUSTFS_HEAL_MRF_ENABLE"], "false")
+
+ def test_range_oracle_uses_ack_nonce_after_historical_deletion(self):
+ from scripts.validation_heal.versions import verify_version_s3
+ bucket, key = "rv-heal-0123456789abcdef", "objects/0001.bin"
+ expected = [{"key": key, "versionId": f"v{nonce}", "deleteMarker": False, "isLatest": nonce == 2,
+ "payload": {"size": 1024**2 + 47}, "payloadNonce": nonce} for nonce in (1, 2)]
+ baseline = {"entries": [{**entry, "properties": {}} for entry in expected]}
+ probe, fleet = Mock(), Mock()
+ probe.inventory.return_value = baseline
+ probe.s3.return_value = {"acknowledged": True, "response": {}}
+ fleet.record.side_effect = lambda label, value: value
+ with patch("scripts.validation_heal.versions.compare_inventories", return_value={}), \
+ patch("scripts.validation_heal.versions.payload", side_effect=lambda label, size: label.encode()) as generator, \
+ patch("scripts.validation_heal.versions.verify_ranges", return_value=[]) as ranges:
+ verify_version_s3(probe, fleet, bucket, expected, baseline, "after-deletion")
+ self.assertEqual([call.args[0] for call in generator.call_args_list], [f"{bucket}/{key}/1", f"{bucket}/{key}/2"])
+ self.assertEqual([call.kwargs["expected_body"] for call in ranges.call_args_list],
+ [f"{bucket}/{key}/1".encode(), f"{bucket}/{key}/2".encode()])
+
+ def test_nonce_bound_ack_reaches_inventory_checks_without_weakening_public_manifest(self):
+ from scripts.validation_heal.heal_workload import verify_seed, FixtureStateMismatch
+ entry = {"key": "objects/0001.bin", "versionId": "v1", "deleteMarker": False, "isLatest": True,
+ "payload": {"size": 100, "sha256": "original-ack"}}
+ expected, inventory = [{**entry, "payloadNonce": 1}], {"entries": [entry]}
+ self.assertTrue(verify_seed(expected, inventory)["seedAckAndFullGetAgree"])
+ for field, value in (("payload", {"size": 100, "sha256": "wrong"}), ("versionId", "v2"),
+ ("deleteMarker", True), ("isLatest", False)):
+ with self.subTest(field=field), self.assertRaises(FixtureStateMismatch):
+ verify_seed(expected, {"entries": [{**entry, field: value}]})
+ for nonce in (-1, True, "1"):
+ with self.subTest(nonce=nonce), self.assertRaises(ValueError):
+ verify_seed([{**entry, "payloadNonce": nonce}], inventory)
+
+
+class HealMultiLossMemberReadinessTest(unittest.TestCase):
+ def scene(self, total):
+ snapshot, expected = HealReadQuorumBoundaryTest().scene(total=total, data=2 if total == 4 else 12)
+ target = map_target(snapshot, HealReadQuorumBoundaryTest.bucket, expected[0]["key"], "data", 1)
+ observed = HealDiskLossTest().admin_info(target)
+ drives = observed["response"]["info"]["servers"][0]["drives"]
+ observed["response"]["info"]["servers"] = [{"drives": drives[index:index + 4]}
+ for index in range(0, total, 4)]
+ return snapshot, observed
+
+ def test_complete_formatted_members_are_required_even_with_read_quorum(self):
+ for total in (4, 16):
+ snapshot, online = self.scene(total)
+ self.assertTrue(validate_all_members_online(online, snapshot)["allFormattedMembersOnline"])
+ changed = copy.deepcopy(online)
+ drives = changed["response"]["info"]["servers"][0]["drives"]
+ for disk in drives[:1 if total == 4 else 4]:
+ disk.update(state="disk not found", runtimeState="offline", uuid="")
+ self.assertFalse(validate_all_members_online(changed, snapshot)["allFormattedMembersOnline"])
+ for field, value in (("runtimeState", "offline"), ("runtimeState", "unknown"),
+ ("state", "initializing"), ("uuid", None)):
+ snapshot, changed = self.scene(16)
+ changed["response"]["info"]["servers"][0]["drives"][0][field] = value
+ with self.subTest(field=field, value=value):
+ self.assertFalse(validate_all_members_online(changed, snapshot)["allFormattedMembersOnline"])
+
+ def test_malformed_or_foreign_member_evidence_fails_without_polling(self):
+ for kind in ("http", "omitted", "duplicate-slot", "foreign-uuid", "duplicate-uuid",
+ "duplicate-endpoint", "wrong-pool", "bool-slot"):
+ snapshot, changed = self.scene(16)
+ servers = changed["response"]["info"]["servers"]
+ drives = servers[0]["drives"]
+ if kind == "http": changed["httpStatus"] = 503
+ elif kind == "omitted": servers.pop()
+ elif kind == "duplicate-slot": drives[1]["disk_index"] = drives[0]["disk_index"]
+ elif kind == "foreign-uuid": drives[0]["uuid"] = "foreign"
+ elif kind == "duplicate-uuid": drives[1]["uuid"] = drives[0]["uuid"]
+ elif kind == "duplicate-endpoint": drives[1]["endpoint"] = drives[0]["endpoint"]
+ elif kind == "wrong-pool": drives[0]["pool_index"] = 1
+ else: drives[0]["disk_index"] = False
+ probe, fleet, reader = Mock(), Mock(), Mock()
+ probe.binding = {"endpoints": {"node1": "http://rustfs-node1:9000"}}
+ fleet.record.side_effect = lambda label, value: value
+ reader.admin.return_value = changed
+ with self.subTest(kind=kind), patch("scripts.validation_heal.versions.Probe", return_value=reader), \
+ patch("scripts.validation_heal.versions.time.sleep") as sleep, self.assertRaises(ValueError):
+ wait_all_members_online(probe, fleet, snapshot, "before-task")
+ reader.admin.assert_called_once_with("/rustfs/admin/v3/info", [], b"", method="GET")
+ sleep.assert_not_called()
+
+ def test_polling_records_each_endpoint_and_never_admits_a_heal(self):
+ snapshot, online = self.scene(16)
+ offline = copy.deepcopy(online)
+ for disk in offline["response"]["info"]["servers"][1]["drives"]:
+ disk.update(state="offline", runtimeState="offline")
+ probe, fleet = Mock(), Mock()
+ probe.binding = {"endpoints": {f"node{index}": f"http://rustfs-node{index}:9000" for index in range(1, 5)}}
+ fleet.record.side_effect = lambda label, value: value
+ readers = {endpoint: Mock() for endpoint in probe.binding["endpoints"]}
+ for endpoint, reader in readers.items():
+ reader.admin.side_effect = [offline if endpoint == "node2" else online, online]
+ with patch("scripts.validation_heal.versions.Probe", side_effect=lambda binding, endpoint: readers[endpoint]), \
+ patch("scripts.validation_heal.versions.time.sleep") as sleep, \
+ patch("scripts.validation_heal.versions.run_task") as task:
+ result = wait_all_members_online(probe, fleet, snapshot, "before-task")
+ self.assertTrue(result["allEndpointViewsOnline"])
+ self.assertEqual(set(result["views"]), set(readers))
+ for reader in readers.values():
+ self.assertEqual(reader.admin.call_count, 2)
+ self.assertTrue(all(call.args == ("/rustfs/admin/v3/info", [], b"")
+ and call.kwargs == {"method": "GET"} for call in reader.admin.call_args_list))
+ self.assertEqual(sum("proof" in call.args[0] for call in fleet.record.call_args_list), 2)
+ sleep.assert_called_once()
+ task.assert_not_called()
+ fleet.remote.assert_not_called()
+
+ def test_persistent_offline_and_late_online_observations_expire(self):
+ for late_online in (False, True):
+ snapshot, observed = self.scene(4)
+ if not late_online:
+ observed["response"]["info"]["servers"][0]["drives"][0].update(state="offline")
+ probe, fleet, reader = Mock(), Mock(), Mock()
+ probe.binding = {"endpoints": {"node1": "http://rustfs-node1:9000"}}
+ fleet.record.side_effect = lambda label, value: value
+ reader.admin.return_value = observed
+ with self.subTest(late_online=late_online), \
+ patch("scripts.validation_heal.versions.Probe", return_value=reader), \
+ patch("scripts.validation_heal.versions.time.monotonic", side_effect=[0, 0, 91]), \
+ patch("scripts.validation_heal.versions.time.sleep") as sleep, \
+ self.assertRaisesRegex(ValueError, "NOT_OBSERVED.*90 seconds"):
+ wait_all_members_online(probe, fleet, snapshot, "before-task")
+ reader.admin.assert_called_once()
+ sleep.assert_not_called()
+
+ def test_runner_checks_members_before_seed_fault_and_the_single_task(self):
+ from scripts.validation_heal import versions
+ snapshot, _ = self.scene(4)
+ scenarios = [(rejected, ["instance"]) for rejected in
+ ("before-seed", "before-fault-instance", "before-task-instance", None)]
+ scenarios.append(("before-task-instance", ["instance", "successor"]))
+ for rejected, names in scenarios:
+ with self.subTest(rejected=rejected), tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ candidate = root / "candidate.json"
+ candidate.write_text(json.dumps({"source_sha": "a" * 40, "package_sha256": "b" * 64}))
+ rc = root / "rc"
+ rc.write_bytes(b"rc")
+ args = types.SimpleNamespace(evidence=root / "evidence", candidate=candidate,
+ case_id="heal.multiple-shard-loss", tool_directory=root, testing_commit="c" * 40,
+ binary_sha="d" * 64, rc_bin=rc)
+ fleet, probe = Mock(), Mock()
+ events = []
+ fleet.drive_gates = set()
+ fleet.faults = []
+ service = {"running": False}
+ fleet.deploy.side_effect = lambda *args: service.update(running=True)
+ fleet.stop.side_effect = lambda: service.update(running=False)
+ fleet.start.side_effect = lambda: service.update(running=True)
+ fleet.record.side_effect = lambda label, value: value
+ fleet.snapshot.return_value = snapshot
+ fleet.cleanup.return_value = True
+ plan = {"scope": {}, "recursive": True, "expected": [], "proofs": [], "faults": [],
+ "requiredRepairs": set(), "metadataRepairs": set()}
+ def ready(probe, fleet, actual, label):
+ self.assertTrue(service["running"], "member observation requires the recovered service")
+ events.append(label)
+ if label == rejected:
+ raise ValueError("NOT_OBSERVED: startup members remain offline")
+ def seeded(*args):
+ events.append("seed")
+ return []
+ def fault_plan(*args):
+ events.append("fault-plan")
+ return plan
+ def task(*args, **kwargs):
+ events.append("task")
+ raise ValueError("stop after the single admitted task")
+ with patch.dict("os.environ", {"GITHUB_RUN_ID": "123", "GITHUB_RUN_ATTEMPT": "1"}), \
+ patch.object(versions, "Fleet", return_value=fleet), \
+ patch.object(versions, "Probe", return_value=probe), \
+ patch.object(versions, "verify_inspector", return_value={}), \
+ patch.object(versions, "Recorder"), \
+ patch.object(versions, "seed", side_effect=seeded), \
+ patch.object(versions, "verify_seed", return_value={}), \
+ patch.object(versions, "multi_loss_instances", return_value=[{"name": name} for name in names]), \
+ patch.object(versions, "multi_loss_plan", side_effect=fault_plan), \
+ patch.object(versions, "wait_all_members_online", side_effect=ready), \
+ patch.object(versions, "physical_content", return_value={}), \
+ patch.object(versions, "run_task", side_effect=task) as repair:
+ result = versions.run_topology(args, "H1", ["vm000"], "DV")
+ wanted = ["before-seed", "seed", "before-fault-instance", "fault-plan", "before-task-instance", "task"]
+ if len(names) == 2:
+ wanted = wanted[:-1] + ["before-fault-successor", "fault-plan", "before-task-successor", "task"]
+ elif rejected is not None:
+ wanted = wanted[:wanted.index(rejected) + 1]
+ self.assertEqual(events, wanted)
+ self.assertEqual(repair.call_count, int(rejected is None or len(names) == 2))
+ if len(names) == 2:
+ self.assertEqual(result["branches"]["instance"]["verdict"], "FAIL")
+ self.assertEqual(result["verdict"], "FAIL")
+ self.assertEqual(result["cleanup"], "PASS")
+
+
+class HealDiskLossTest(unittest.TestCase):
+ def test_original_member_return_preserves_every_peer_process_identity(self):
+ identities = {node: {name: node + ":" + name for name in fleet_node.PROCESS_FIELDS}
+ for node in ("node1", "node2")}
+ for changed in (None, *fleet_node.PROCESS_FIELDS, "restart", "unbound"):
+ released = False
+ def remote(node, action):
+ nonlocal released
+ if action == "drive-gate-release-live":
+ released = True
+ return {"sameOriginalProcess": changed != "unbound", "serviceRestarted": changed == "restart"}
+ self.assertEqual(action, "process")
+ value = copy.deepcopy(identities[node])
+ if released and node == "node1" and changed in fleet_node.PROCESS_FIELDS:
+ value[changed] = "changed"
+ return value
+ fleet = types.SimpleNamespace(nodes=list(identities), drive_gates={"node2"}, remote=remote,
+ record=Mock(side_effect=lambda label, value: value))
+ with self.subTest(changed=changed):
+ if changed is None:
+ stale_rejoin.return_original_member(fleet, "node2", "disk-loss")
+ self.assertEqual(fleet.drive_gates, set())
+ self.assertEqual(fleet.record.call_args.args[0], "disk-loss-live-rejoin-process-proof")
+ else:
+ with self.assertRaises(ValueError):
+ stale_rejoin.return_original_member(fleet, "node2", "disk-loss")
+ self.assertNotIn("disk-loss-live-rejoin-process-proof", [call.args[0] for call in fleet.record.call_args_list])
+
+ def test_offline_ack_rejoin_cannot_restart_or_admit_manual_work_after_early_repair(self):
+ original, authority, repaired, bucket, target, new, _ = self.scene()
+ online = self.admin_info(target)
+ offline = copy.deepcopy(online)
+ offline["response"]["info"]["servers"][0]["drives"][target["setSlot"]].update(state="drive access denied")
+ for failure in (None, "unstable", "early-repair"):
+ events = []
+ def snapshot(label, **options):
+ if label.startswith("disk-loss-original-"): return original
+ self.assertFalse(options.get("quiescent", False), "offline ACKs must remain in their original running fleet")
+ if failure == "unstable" and label == "disk-loss-online-authorities-stable": return repaired
+ if failure == "early-repair" and label == "disk-loss-before-manual": return repaired
+ return authority
+ def remote(node, action, **options):
+ if action == "drive-gate": return {"dataPath": "/owned-root"}
+ if action == "drive-gate-probe":
+ return {"dataPath": "/owned-root", "readProbe": {"errno": 13}, "errno": 13, "dacBypassRemoved": True}
+ if action == "drive-gate-release-live":
+ events.append("live-return")
+ return {"sameOriginalProcess": True, "serviceRestarted": False}
+ self.assertEqual(action, "process")
+ return {name: node + ":" + name for name in fleet_node.PROCESS_FIELDS}
+ def ack(*args):
+ events.append("ack")
+ return new, new
+ fleet = types.SimpleNamespace(nodes=["node1"], drive_gates=set(), remote=remote,
+ record=lambda label, value: value, snapshot=snapshot,
+ start=lambda: events.append("start"), stop=lambda: events.append("stop"))
+ recorder = types.SimpleNamespace(probe=types.SimpleNamespace(
+ admin=Mock(side_effect=[online, offline, offline]), inventory=lambda *args, **kw: {}))
+ with self.subTest(failure=failure), patch.object(disk_loss, "map_target", return_value=target), \
+ patch.object(disk_loss, "offline_ack", side_effect=ack), \
+ patch.object(disk_loss, "verify_seed", return_value={}), \
+ patch.object(disk_loss, "run_task", side_effect=RuntimeError("manual boundary reached")) as run:
+ if failure is None:
+ with self.assertRaisesRegex(RuntimeError, "manual boundary reached"):
+ disk_loss.run_branch(recorder, fleet, bucket, "offline-ack-repair", [], [], [],
+ HealReadQuorumBoundaryTest.contract)
+ self.assertEqual(run.call_count, 1)
+ else:
+ with self.assertRaisesRegex(ValueError, "NOT_OBSERVED"):
+ disk_loss.run_branch(recorder, fleet, bucket, "offline-ack-repair", [], [], [],
+ HealReadQuorumBoundaryTest.contract)
+ run.assert_not_called()
+ self.assertNotIn("stop", events[events.index("ack"):])
+ self.assertNotIn("start", events[events.index("ack"):])
+
+ def scene(self, total=4, data=2):
+ original, old = HealReadQuorumBoundaryTest().scene(total=total, data=data)
+ bucket, old_key, new_key = HealReadQuorumBoundaryTest.bucket, old[0]["key"], "disk-loss/offline-ack-repair/new-00.bin"
+ repaired = copy.deepcopy(original)
+ for node, value in repaired.items():
+ additions = []
+ for entry in value["files"]:
+ item = copy.deepcopy(entry)
+ item["path"] = item["path"].replace(old_key, new_key)
+ if item.get("metadata"):
+ item["metadata"]["key"] = new_key
+ item["metadata"]["versions"][0]["usesLegacyChecksum"] = False
+ item["sha256"] += "-new-ack"
+ additions.append(item)
+ value["files"] += additions
+ new = [{**old[0], "key": new_key}]
+ target = map_target(original, bucket, old_key, "data", 1)
+ target = {**target, **target["cohort"][-1]}
+ member = target["node"], target["disk"]
+ authority = copy.deepcopy(repaired)
+ authority[member[0]]["files"] = [entry for entry in authority[member[0]]["files"]
+ if entry["disk"] != member[1] or not entry["path"].startswith(f"{bucket}/{new_key}/")]
+ witness = disk_loss.authority_witness(original, authority, bucket, target, new)
+ return original, authority, repaired, bucket, target, new, witness
+
+ def admin_info(self, target):
+ return {"httpStatus": 200, "response": {"info": {"servers": [{"drives": [
+ {"pool_index": 0, "set_index": 0, "disk_index": shard["setSlot"], "uuid": shard["diskUUID"],
+ "endpoint": f"http://{shard['node']}:9000/data/{shard['disk']}", "state": "ok", "runtimeState": "online"}
+ for shard in target["cohort"]]}]}}}
+
+ def test_actual_single_member_quorum_and_repair_preserve_all_original_bytes(self):
+ for total, data in ((4, 2), (16, 12), (16, 8)):
+ original, authority, repaired, bucket, target, new, witness = self.scene(total, data)
+ plan = disk_loss.quorum_plan(target, HealReadQuorumBoundaryTest.contract)
+ self.assertEqual(plan["onlineSameSetDrives"], total - 1)
+ self.assertEqual(plan["writeQuorum"], data + int(data == total - data))
+ cohorts, proof = disk_loss.repaired_physical(authority, repaired, bucket, target, new, witness)
+ self.assertEqual(len(cohorts), 1)
+ self.assertTrue(proof["allDegradedACKTargetPartsCreated"])
+ self.assertFalse(proof["backupRestorationUsed"])
+
+ def test_repaired_protected_ack_requires_one_exact_target_integrity_sidecar(self):
+ original, authority, repaired, bucket, target, new, _ = self.scene()
+ for scene in (authority, repaired):
+ for value in scene.values():
+ for item in value["files"]:
+ metadata = item.get("metadata", {})
+ if metadata.get("key") == new[0]["key"]:
+ metadata["versions"][0]["parts"][0]["integrityDescriptorPresent"] = True
+ witness = disk_loss.authority_witness(original, authority, bucket, target, new)
+ cohort = map_target(repaired, bucket, new[0]["key"], "data", 1)
+ member = next(shard for shard in cohort["cohort"] if shard["node"] == target["node"] and shard["disk"] == target["disk"])
+ sidecar = {"disk": target["disk"], "path": member["relativePath"] + ".integrity.12345678-1234-1234-1234-123456789abc",
+ "size": 1408, "sha256": "actual-sidecar-hash"}
+ repaired[target["node"]]["files"].append(sidecar)
+ disk_loss.repaired_physical(authority, repaired, bucket, target, new, witness)
+ for kind in ("missing", "duplicate", "foreign-generation", "nil", "empty", "oversize", "different-member"):
+ changed = copy.deepcopy(repaired)
+ row = changed[target["node"]]["files"][-1]
+ if kind == "missing": changed[target["node"]]["files"].pop()
+ elif kind == "duplicate": changed[target["node"]]["files"].append({**row, "path": row["path"][:-1] + "d"})
+ elif kind == "foreign-generation": row["path"] = row["path"].replace(cohort["dataDir"], "other")
+ elif kind == "nil": row["path"] = member["relativePath"] + ".integrity.00000000-0000-0000-0000-000000000000"
+ elif kind == "empty": row["size"] = 0
+ elif kind == "oversize": row["size"] = 65537
+ else: row["disk"] = (target["disk"] + 1) % 4
+ with self.subTest(kind=kind), self.assertRaises(ValueError):
+ disk_loss.repaired_physical(authority, changed, bucket, target, new, witness)
+
+ def test_offline_ack_authority_rejects_target_writes_loss_of_controls_and_donor_versions(self):
+ original, authority, repaired, bucket, target, new, witness = self.scene()
+ for kind in ("offline-write", "control", "donor-version", "format", "outside"):
+ changed = copy.deepcopy(authority)
+ if kind == "offline-write": changed = copy.deepcopy(repaired)
+ elif kind == "control": changed["node1"]["files"][0]["sha256"] = "changed-original"
+ elif kind == "donor-version":
+ next(entry for entry in changed["node1"]["files"] if entry.get("metadata", {}).get("key") == new[0]["key"])["metadata"]["versions"][0]["versionId"] = "foreign"
+ elif kind == "format": changed["node1"]["formats"][0]["value"]["xl"]["this"] = "new-disk"
+ else: changed["node1"]["files"].append({"disk": 0, "path": "other/key/part.1", "size": 82, "sha256": "outside"})
+ with self.subTest(kind=kind), self.assertRaises(ValueError):
+ disk_loss.authority_witness(original, changed, bucket, target, new)
+
+ def test_ready_or_get_only_and_changed_surviving_shards_cannot_prove_repair(self):
+ original, authority, repaired, bucket, target, new, witness = self.scene()
+ for kind in ("no-repair", "donor", "missing-part", "foreign-generation", "extra"):
+ changed = copy.deepcopy(repaired)
+ if kind == "no-repair": changed = copy.deepcopy(authority)
+ elif kind == "donor":
+ next(entry for entry in changed["node1"]["files"] if entry["disk"] != target["disk"]
+ and entry["path"].startswith(f"{bucket}/{new[0]['key']}/") and entry["path"].endswith("part.1"))["sha256"] = "changed"
+ elif kind == "missing-part": changed["node1"]["files"] = [entry for entry in changed["node1"]["files"]
+ if not (entry["disk"] == target["disk"] and entry["path"].startswith(f"{bucket}/{new[0]['key']}/") and entry["path"].endswith("part.1"))]
+ elif kind == "foreign-generation":
+ next(entry for entry in changed["node1"]["files"] if entry["disk"] == target["disk"]
+ and entry.get("metadata", {}).get("key") == new[0]["key"])["metadata"]["versions"][0]["dataDir"] = "foreign"
+ else: changed["node1"]["files"].append({"disk": target["disk"], "path": f"{bucket}/{new[0]['key']}/orphan", "size": 82, "sha256": "extra"})
+ with self.subTest(kind=kind), self.assertRaises(ValueError):
+ disk_loss.repaired_physical(authority, changed, bucket, target, new, witness)
+
+ def test_offline_admin_requires_exact_slot_endpoint_uuid_and_complete_healthy_peers(self):
+ _, _, _, _, target, _, _ = self.scene()
+ online = self.admin_info(target)
+ baseline = disk_loss.member_state(online, target, 4)
+ offline = copy.deepcopy(online)
+ selected = offline["response"]["info"]["servers"][0]["drives"][target["setSlot"]]
+ selected.update(state="drive access denied", runtimeState="online")
+ self.assertTrue(disk_loss.member_state(offline, target, 4, baseline)["exactOriginalMemberOffline"])
+ for kind in ("wrong-slot", "online", "unknown", "initializing", "unknown-error", "new-uuid",
+ "new-endpoint", "peer-offline", "peer-uuid", "omitted", "duplicate"):
+ changed = copy.deepcopy(offline)
+ drives = changed["response"]["info"]["servers"][0]["drives"]
+ if kind == "wrong-slot": drives[target["setSlot"]]["disk_index"] = 0
+ elif kind == "online": drives[target["setSlot"]].update(state="ok", runtimeState="online")
+ elif kind == "unknown": drives[target["setSlot"]].update(state="unknown", runtimeState="unknown")
+ elif kind == "initializing": drives[target["setSlot"]].update(state="initializing", runtimeState=None)
+ elif kind == "unknown-error": drives[target["setSlot"]].update(state="unexpected access error", runtimeState="online")
+ elif kind == "new-uuid": drives[target["setSlot"]]["uuid"] = "another-disk"
+ elif kind == "new-endpoint": drives[target["setSlot"]]["endpoint"] = "http://another-node:9000/data"
+ elif kind == "peer-offline": drives[0].update(state="offline", runtimeState="offline")
+ elif kind == "peer-uuid": drives[0]["uuid"] = "another-disk"
+ elif kind == "omitted": drives.pop()
+ else: drives.append(copy.deepcopy(drives[0]))
+ with self.subTest(kind=kind), self.assertRaises(ValueError):
+ disk_loss.member_state(changed, target, 4, baseline)
+
+ def test_explicit_drive_access_denial_is_offline_despite_cached_runtime_state(self):
+ _, _, _, _, target, _, _ = self.scene()
+ online = self.admin_info(target)
+ baseline = disk_loss.member_state(online, target, 4)
+ for runtime in ("online", None):
+ for uuid in (target["diskUUID"], ""):
+ observed = copy.deepcopy(online)
+ selected = observed["response"]["info"]["servers"][0]["drives"][target["setSlot"]]
+ selected.update(state="drive access denied", uuid=uuid)
+ if runtime is None:
+ selected.pop("runtimeState")
+ else:
+ selected["runtimeState"] = runtime
+ with self.subTest(runtime=runtime, uuid=uuid):
+ proof = disk_loss.member_state(observed, target, 4, baseline)
+ self.assertTrue(proof["exactOriginalMemberOffline"])
+ self.assertEqual(proof["state"], "drive access denied")
+ self.assertEqual(proof["reportedUUID"], uuid)
+ self.assertEqual(proof["runtimeState"], runtime)
+ with self.assertRaisesRegex(ValueError, "not positively online"):
+ disk_loss.member_state(observed, target, 4)
+
+ def test_degraded_unique_puts_keep_each_ack_and_branches_match_source(self):
+ self.assertEqual(list(disk_loss.BRANCHES), select("B01")[0]["variants"])
+ bucket = HealReadQuorumBoundaryTest.bucket
+ observed = []
+ def invoke(operation, params, body):
+ observed.append((params["Key"], body))
+ return {"ResponseMetadata": {"HTTPStatusCode": 200}}
+ def get(operation, params):
+ body = next(body for key, body in observed if key == params["Key"])
+ return {"acknowledged": True, "payload": {"size": len(body), "sha256": hashlib.sha256(body).hexdigest()}}
+ recorder = types.SimpleNamespace(call=invoke, probe=types.SimpleNamespace(s3=get))
+ fleet = types.SimpleNamespace(record=lambda label, value: value)
+ expected = []
+ for branch, count in zip(disk_loss.BRANCHES, (1, 6)):
+ expected, new = disk_loss.offline_ack(recorder, fleet, bucket, branch, expected)
+ self.assertEqual(len(new), count)
+ self.assertEqual(len(expected), 7)
+ with self.assertRaisesRegex(ValueError, "already has"):
+ disk_loss.offline_ack(recorder, fleet, bucket, disk_loss.BRANCHES[0], expected)
+
+
+class HealWholeDriveReplacementTest(unittest.TestCase):
+ def test_partial_target_window_requires_actual_committed_shards_and_pending_versions(self):
+ scene, bucket, expected = versioned_scene(["first", "second"])
+ proofs = [map_target(scene, bucket, key, "data", 1, "v0", latest_required=False) for key in ("first", "second")]
+ partial = copy.deepcopy(scene)
+ missing = proofs[1]
+ partial[missing["node"]]["files"] = [item for item in partial[missing["node"]]["files"]
+ if not (item["disk"] == missing["disk"] and item["path"] == missing["relativePath"])]
+ progress = replacement.target_progress(partial, proofs)
+ self.assertTrue(progress["partial"])
+ self.assertEqual(progress["completeDataVersions"], [("first", "v0")])
+ self.assertFalse(replacement.target_progress(scene, proofs)["partial"])
+ for kind in ("bytes", "version-authority", "role"):
+ changed = copy.deepcopy(partial)
+ target = proofs[0]
+ if kind == "bytes":
+ next(item for item in changed[target["node"]]["files"] if item["disk"] == target["disk"]
+ and item["path"] == target["relativePath"])["sha256"] = "wrong"
+ else:
+ version = next(item for item in changed[target["node"]]["files"] if item["disk"] == target["disk"]
+ and item["path"] == f"{bucket}/first/xl.meta")["metadata"]["versions"][0]
+ if kind == "version-authority": version["dataDir"] = "foreign-generation"
+ else: version["role"] = "wrong"
+ with self.subTest(kind=kind), self.assertRaises(ValueError): replacement.target_progress(changed, proofs)
+
+ def test_target_restart_keeps_other_peers_and_rechecks_private_namespace(self):
+ before = {f"node{index}": HealRootRestartOracleTest().process(10 + index, 50 + index) for index in range(1, 5)}
+ after = copy.deepcopy(before)
+ after["node4"].update(pid=29, startTicks=99, mountNamespace="mnt:[new-private]")
+ self.assertTrue(replacement.target_restart_identity(before, after, "node4")["onlyOriginalTargetProcessRestarted"])
+ for node, field in (("node1", "pid"), ("node4", "binarySha256"), ("node4", "bootId"), ("node4", "settings")):
+ changed = copy.deepcopy(after); changed[node][field] = "changed"
+ with self.subTest(node=node, field=field), self.assertRaises(ValueError): replacement.target_restart_identity(before, changed, "node4")
+
+ def test_target_crash_requires_exact_signal_nine_and_original_birth_identity(self):
+ before = HealRootRestartOracleTest().process()
+ exited = {"pid": 0, "bootId": "boot", "binarySha256": "sha", "environmentFileSha256": "env",
+ "service": {"ExecMainCode": "2", "ExecMainStatus": "9", "Result": "signal"}}
+ for kind in ("good", "graceful", "other-signal", "timeout", "boot", "candidate", "environment"):
+ current = copy.deepcopy(exited)
+ if kind == "graceful": current["service"]["ExecMainCode"] = "1"
+ elif kind == "other-signal": current["service"]["ExecMainStatus"] = "15"
+ elif kind == "timeout": current["service"]["Result"] = "timeout"
+ elif kind == "boot": current["bootId"] = "new-boot"
+ elif kind == "candidate": current["binarySha256"] = "new-build"
+ elif kind == "environment": current["environmentFileSha256"] = "new-env"
+ with self.subTest(kind=kind), patch.object(fleet_node, "owned", return_value=(None, None, {"runtimeProfile": "replacement"})), \
+ patch.object(fleet_node, "process", side_effect=[before, current]), patch.object(fleet_node, "pidfd_signal", return_value=before) as send:
+ if kind == "good":
+ proof = fleet_node.process_crash({"runId": "run", "expected": before})
+ self.assertEqual(proof["actualSignal"], 9)
+ self.assertFalse(proof["platformEventIssued"])
+ self.assertEqual(send.call_args.args[1], fleet_node.signal.SIGKILL)
+ else:
+ with self.assertRaises(ValueError): fleet_node.process_crash({"runId": "run", "expected": before})
+ with patch.object(fleet_node, "owned", return_value=(None, None, {"runtimeProfile": "manual"})), \
+ patch.object(fleet_node, "pidfd_signal") as send, self.assertRaises(ValueError):
+ fleet_node.process_crash({"runId": "run", "expected": before})
+ send.assert_not_called()
+
+ def test_fast_completed_replacement_cannot_certify_an_unobserved_interruption(self):
+ endpoint = "/private/new/data"
+ row = {"taskId": "12345678-1234-4234-8234-123456789abc", "generation": "12345678-1234-4234-8234-123456789abc",
+ "setDiskId": "pool_0_set_0", "targetSlots": [endpoint], "state": "completed", "reason": None, "verifiedAt": 101}
+ with patch("scripts.validation_heal.replacement.replacement_records", return_value={"node1": [row]}), \
+ patch("scripts.validation_heal.replacement.time.time", return_value=102), self.assertRaisesRegex(ValueError, "NOT_OBSERVED"):
+ replacement.automatic_completion(None, None, endpoint, 100, interruption="sigkill")
+ self.assertEqual(list(replacement.GRACEFUL_BRANCHES), select("F02")[0]["variants"])
+ self.assertEqual(list(replacement.CRASH_BRANCHES), select("F03")[0]["variants"])
+
+ def test_target_interruption_needs_fresh_source_and_a_distinct_actual_coordinator(self):
+ token = "12345678-1234-4234-8234-123456789abc"
+ rows = [{"timestamp": "2026-10-10T10:00:01Z", "target": "rustfs::heal::manager",
+ "request_id": token, "source": "auto_heal", "result": "accepted", "message": "Heal queue request accepted"},
+ {"timestamp": "2026-10-10T10:00:02Z", "target": "rustfs::heal::task", "task_id": token,
+ "set_disk_id": "pool_0_set_0", "stage": "start", "message": "Heal erasure set started"}]
+ self.assertEqual(replacement.coordinator({"node1": rows, "node4": []}, token, "node4"), "node1")
+ for kind in ("same-target", "duplicate", "source", "task", "order", "missing"):
+ events = {"node1": copy.deepcopy(rows), "node4": []}
+ if kind == "same-target": events = {"node4": rows}
+ elif kind == "duplicate": events["node2"] = rows
+ elif kind == "source": events["node1"][0]["source"] = "admin"
+ elif kind == "task": events["node1"][1]["task_id"] = "other-task"
+ elif kind == "order": events["node1"][1]["timestamp"] = "2026-10-10T10:00:00Z"
+ else: events["node1"].pop()
+ with self.subTest(kind=kind), self.assertRaisesRegex(ValueError, "NOT_OBSERVED"):
+ replacement.coordinator(events, token, "node4")
+ config = runtime_config("replacement-target")
+ self.assertEqual(config["RUSTFS_HEAL_AUTO_HEAL_ENABLE"], "false")
+ self.assertEqual(config["RUSTFS_HEAL_ENABLED"], "true")
+
+ def test_live_permission_rejoin_restores_same_inode_and_requires_same_process(self):
+ for changed_process in (False, True):
+ with self.subTest(changed_process=changed_process), tempfile.TemporaryDirectory() as folder:
+ state, root = self.fixture(folder)
+ identity = HealRootRestartOracleTest().process()
+ after = {**identity, "startTicks": identity["startTicks"] + int(changed_process)}
+ with self.root_owned():
+ apply_gate(state, (root,), 0, "permission-denied")
+ activate_gate(state, (root,))
+ original_inode = (root / "data").stat().st_ino
+ with patch.object(fleet_node, "owned", return_value=(state, (root,), {})), \
+ patch.object(fleet_node, "process", side_effect=[identity, after]), \
+ patch.object(fleet_node, "stopped") as stopped_service:
+ if changed_process:
+ with self.assertRaisesRegex(ValueError, "process identity"):
+ fleet_node.drive_gate_release_live({"runId": "owned-run"})
+ else:
+ proof = fleet_node.drive_gate_release_live({"runId": "owned-run"})
+ self.assertTrue(proof["sameOriginalProcess"])
+ self.assertFalse(proof["serviceRestarted"])
+ stopped_service.assert_not_called()
+ self.assertEqual((root / "data").stat().st_ino, original_inode)
+ self.assertEqual(stat.S_IMODE((root / "data").stat().st_mode), 0o700)
+ self.assertIsNone(checked_gate(state, (root,)))
+
+ def test_live_rejoin_rejects_unactivated_gate_without_mutating_root(self):
+ with tempfile.TemporaryDirectory() as folder:
+ state, root = self.fixture(folder)
+ with self.root_owned():
+ apply_gate(state, (root,), 0, "permission-denied")
+ with patch.object(fleet_node, "owned", return_value=(state, (root,), {})), \
+ patch.object(fleet_node, "process") as process, self.assertRaisesRegex(ValueError, "active permission"):
+ fleet_node.drive_gate_release_live({"runId": "owned-run"})
+ process.assert_not_called()
+ self.assertEqual(checked_gate(state, (root,))["phase"], "armed")
+ self.assertEqual(stat.S_IMODE((root / "data").stat().st_mode), 0o700)
+
+ def replaced_scene(self):
+ before = HealShardOracleTest().snapshot()
+ member = "node1", 3
+ after = copy.deepcopy(before)
+ for entry in after["node1"]["formats"]:
+ entry["value"]["xl"]["sets"][0][3] = "new-member"
+ if entry["disk"] == 3: entry["value"]["xl"]["this"] = "new-member"
+ return before, after, member
+
+ def test_new_disk_requires_same_pool_set_slot_and_distinct_uuid(self):
+ before, after, member = self.replaced_scene()
+ self.assertEqual(replacement.format_identity(before, after, member)["newDiskUUID"], "new-member")
+ for kind in ("same-uuid", "new-cluster", "wrong-slot", "changed-survivor"):
+ changed = copy.deepcopy(after)
+ if kind == "same-uuid": changed = copy.deepcopy(before)
+ elif kind == "new-cluster":
+ for entry in changed["node1"]["formats"]: entry["value"]["id"] = "another-cluster"
+ elif kind == "wrong-slot": changed["node1"]["formats"][3]["value"]["xl"]["this"] = "0"
+ else: changed["node1"]["formats"][0]["value"]["version"] = "future"
+ with self.subTest(kind=kind), self.assertRaises(ValueError): replacement.format_identity(before, changed, member)
+
+ def test_rebuild_requires_all_same_role_original_parts_and_unchanged_survivors(self):
+ before, after, member = self.replaced_scene()
+ self.assertFalse(replacement.physical_rebuild(before, after, member)["backupRestorationUsed"])
+ for kind in ("missing-target", "bad-target", "donor", "version", "orphan"):
+ changed = copy.deepcopy(after)
+ target = next(entry for entry in changed["node1"]["files"] if entry["disk"] == member[1]
+ and entry["path"].endswith("part.1"))
+ if kind == "missing-target": changed["node1"]["files"].remove(target)
+ elif kind == "bad-target": target["sha256"] = "wrong-shard"
+ elif kind == "donor": changed["node1"]["files"][0]["sha256"] = "changed-donor"
+ elif kind == "version":
+ next(entry for entry in changed["node1"]["files"] if entry["disk"] == member[1]
+ and entry.get("metadata"))["metadata"]["versions"][0]["versionId"] = "foreign-version"
+ else: changed["node1"]["files"].append({"disk": member[1], "path": "run/key/orphan", "size": 82, "sha256": "orphan"})
+ with self.subTest(kind=kind), self.assertRaises(ValueError): replacement.physical_rebuild(before, changed, member)
+
+ def fixture(self, folder):
+ state, root = Path(folder) / "state", Path(folder) / "root"
+ state.mkdir(mode=0o700)
+ (root / "data/.rustfs.sys").mkdir(parents=True, mode=0o700)
+ (root / "data").chmod(0o700)
+ (root / "quarantine").mkdir(mode=0o700)
+ (root / "data/.rustfs.sys/format.json").write_bytes(b'{"original":"format"}')
+ (root / "data/ack-part").write_bytes(b"original-ack-shard")
+ return state, root
+
+ def root_owned(self):
+ original = Path.lstat
+ def observed(path):
+ value = original(path)
+ return types.SimpleNamespace(st_mode=value.st_mode, st_uid=0, st_nlink=value.st_nlink,
+ st_size=value.st_size, st_dev=value.st_dev, st_ino=value.st_ino)
+ return patch.object(Path, "lstat", observed)
+
+ def test_replacement_keeps_whole_original_and_rejects_drift_or_second_begin(self):
+ with tempfile.TemporaryDirectory() as folder:
+ state, root = self.fixture(folder)
+ with self.root_owned():
+ before = replace_drive.tree_identity(root / "data")
+ proof = replace_drive.begin(state, (root,), 0, replace_drive.digest(root / "data/.rustfs.sys/format.json"))
+ self.assertTrue(proof["newDataRootEmpty"])
+ self.assertEqual(replace_drive.tree_identity(root / "quarantine/old-disk"), before)
+ self.assertEqual((root / "quarantine/old-disk/ack-part").read_bytes(), b"original-ack-shard")
+ self.assertTrue(replace_drive.attest(state, (root,))["originalArchiveUnchanged"])
+ self.assertIn("--property=InaccessiblePaths=" + str(root / "quarantine"), replace_drive.start_properties(state, (root,)))
+ with self.assertRaises(ValueError): replace_drive.begin(state, (root,), 0, proof["originalFormatSha256"])
+ (root / "quarantine/old-disk/ack-part").write_bytes(b"changed-old-backup")
+ with self.assertRaisesRegex(ValueError, "drifted"): replace_drive.attest(state, (root,))
+
+ def test_partial_begin_retains_recovery_intent_and_never_copies_original_back(self):
+ for boundary in ("rename", "mkdir"):
+ with self.subTest(boundary=boundary), tempfile.TemporaryDirectory() as folder:
+ state, root = self.fixture(folder)
+ with self.root_owned():
+ digest = replace_drive.digest(root / "data/.rustfs.sys/format.json")
+ if boundary == "rename": failure = patch.object(replace_drive.os, "rename", side_effect=OSError("interrupted rename"))
+ else: failure = patch.object(Path, "mkdir", side_effect=OSError("interrupted mkdir"))
+ with failure, self.assertRaises(OSError): replace_drive.begin(state, (root,), 0, digest)
+ proof = replace_drive.attest(state, (root,))
+ self.assertTrue(proof["originalArchiveUnchanged"])
+ self.assertEqual(proof["originalStillAtDataRoot"], boundary == "rename")
+ original = root / ("data" if boundary == "rename" else "quarantine/old-disk")
+ self.assertEqual((original / "ack-part").read_bytes(), b"original-ack-shard")
+
+ def test_archive_rejects_links_and_wrong_format_before_mutation(self):
+ for kind in ("symlink", "hardlink", "format"):
+ with self.subTest(kind=kind), tempfile.TemporaryDirectory() as folder:
+ state, root = self.fixture(folder)
+ if kind == "symlink": (root / "data/link").symlink_to(root / "data/ack-part")
+ elif kind == "hardlink": __import__("os").link(root / "data/ack-part", root / "data/another")
+ with self.root_owned(), self.assertRaises(ValueError): replace_drive.begin(state, (root,), 0, "0" * 64)
+ self.assertFalse((state / "disk-replacement.json").exists())
+ self.assertFalse((root / "quarantine/old-disk").exists())
+ self.assertEqual((root / "data/ack-part").read_bytes(), b"original-ack-shard")
+
+ def test_replacement_format_wait_never_substitutes_service_ready_for_member_readiness(self):
+ absent = {"readOnly": True, "originalArchiveUnchanged": True,
+ "formats": [{"disk": index, "present": index != 3} for index in range(4)]}
+ present = copy.deepcopy(absent); present["formats"][3]["present"] = True
+ fleet = types.SimpleNamespace(record=lambda label, value: value, remote=lambda *args: absent)
+ with patch("scripts.validation_heal.replacement.time.monotonic", side_effect=[0, 91]), self.assertRaisesRegex(ValueError, "90-second"):
+ replacement.wait_format(fleet, "node1", 3)
+ rows = [absent, present]
+ fleet.remote = lambda *args: rows.pop(0)
+ with patch("scripts.validation_heal.replacement.time.sleep"), patch("scripts.validation_heal.replacement.time.monotonic", return_value=0):
+ self.assertTrue(replacement.wait_format(fleet, "node1", 3)["formats"][3]["present"])
+ fleet.remote = lambda *args: {**present, "originalArchiveUnchanged": False}
+ with self.assertRaisesRegex(ValueError, "archive witness"): replacement.wait_format(fleet, "node1", 3)
+
+ def test_automatic_replacement_generation_rejects_unknown_wrong_target_and_stale_completion(self):
+ endpoint = "http://rustfs-node4:9000/private/new/data"
+ row = {"taskId": "12345678-1234-4234-8234-123456789abc", "generation": "12345678-1234-4234-8234-123456789abc",
+ "setDiskId": "pool_0_set_0", "targetSlots": [endpoint], "state": "completed", "reason": None, "verifiedAt": 101}
+ with patch("scripts.validation_heal.replacement.time.time", return_value=102):
+ binding = replacement.generation([row], endpoint, 100, completed=True)
+ for kind in ("state", "reason", "stale", "target", "set", "generation", "task", "missing", "duplicate"):
+ with self.subTest(kind=kind):
+ bad = copy.deepcopy(row)
+ rows = [bad]
+ if kind == "state": bad["state"] = "unknown"
+ elif kind == "reason": bad["reason"] = "incomplete"
+ elif kind == "stale": bad["verifiedAt"] = 99
+ elif kind == "target": bad["targetSlots"] = [endpoint + "-different"]
+ elif kind == "set": bad["setDiskId"] = "pool_1_set_0"
+ elif kind == "generation": bad["generation"] = "other"
+ elif kind == "task": bad["taskId"] = bad["generation"] = "ffffffff-ffff-4fff-8fff-ffffffffffff"
+ elif kind == "missing": rows = []
+ else: rows *= 2
+ with self.assertRaises(ValueError): replacement.generation(rows, endpoint, 100, original=binding, completed=True)
+ running = {**row, "state": "running", "verifiedAt": None}
+ replacement.generation([running], endpoint, 100, original=binding)
+ with self.assertRaisesRegex(ValueError, "not completed"): replacement.generation([running], endpoint, 100, completed=True)
+
+ def test_automatic_replacement_requires_every_peer_and_stable_completed_generation(self):
+ endpoint = "/private/new/data"
+ row = {"taskId": "12345678-1234-4234-8234-123456789abc", "generation": "12345678-1234-4234-8234-123456789abc",
+ "setDiskId": "pool_0_set_0", "targetSlots": [endpoint], "state": "completed", "reason": None, "verifiedAt": 101}
+ complete = {"node1": [row], "node2": [copy.deepcopy(row)]}
+ with patch("scripts.validation_heal.replacement.replacement_records", side_effect=[{"node1": [row], "node2": []}, complete, complete, complete]), \
+ patch("scripts.validation_heal.replacement.time.sleep"), \
+ patch("scripts.validation_heal.replacement.time.monotonic", return_value=0), \
+ patch("scripts.validation_heal.replacement.time.time", return_value=102):
+ proof = replacement.automatic_completion(None, None, endpoint, 100)
+ self.assertTrue(proof["completionObservedOnEveryPeer"])
+ self.assertEqual(proof["adminHealMutations"], 0)
+ self.assertEqual(proof["observations"], 2)
+ changed = copy.deepcopy(complete); changed["node2"][0]["verifiedAt"] = 102
+ with patch("scripts.validation_heal.replacement.replacement_records", return_value=changed), \
+ patch("scripts.validation_heal.replacement.time.time", return_value=102), self.assertRaisesRegex(ValueError, "differs across peers"):
+ replacement.automatic_completion(None, None, endpoint, 100)
+
+ def test_automatic_replacement_profile_enables_only_its_registered_source(self):
+ config = runtime_config("replacement")
+ self.assertEqual(config["RUSTFS_HEAL_AUTO_HEAL_ENABLE"], "true")
+ self.assertEqual(config["RUSTFS_HEAL_MRF_ENABLE"], "false")
+ self.assertEqual(config["RUSTFS_SCANNER_ENABLED"], "false")
+ self.assertEqual(config["RUSTFS_HEAL_INTERVAL_SECS"], "10")
+ self.assertEqual(list(replacement.AUTOMATIC_BRANCHES), select("E06")[0]["variants"])
+
+ def test_remote_replacement_module_initializes_and_all_source_branches_remain(self):
+ import sys
+ self.assertEqual(list(replacement.BRANCHES), select("B03")[0]["variants"])
+ prior = sys.modules.get("drive_gate")
+ observed = []
+ def invoke(args, **kwargs):
+ source = kwargs["input"].rsplit("\nmain(", 1)[0]
+ scope = {"__name__": "private-node-script"}
+ exec(source, scope)
+ self.assertEqual(scope["replacement_module"]().DENY_DAC_CAPS, (1 << 1) | (1 << 2))
+ self.assertTrue(callable(scope["replacement_module"]().begin))
+ observed.append(source)
+ return types.SimpleNamespace(returncode=0, stdout='{"compiled":true}', stderr="")
+ try:
+ with tempfile.TemporaryDirectory() as folder, patch("scripts.validation_heal.healthy.subprocess.run", side_effect=invoke):
+ fleet = Fleet(["node1"], Path(folder), "gh-12345-1-h1")
+ for action in ("start", "cleanup", "replace-drive", "replacement-state", "replacement-probe"):
+ self.assertTrue(fleet.remote("node1", action)["compiled"])
+ finally:
+ if prior is None: sys.modules.pop("drive_gate", None)
+ else: sys.modules["drive_gate"] = prior
+ self.assertEqual(len(observed), 5)
+
+
+class HealPackageFinalizationTest(unittest.TestCase):
+ def run_finalizer(self, mode, *, installed=True):
+ import os
+ import subprocess
+ source = Path('scripts/validation_heal/run_case.sh').read_text()
+ function = 'finalize_package() {' + source.split('finalize_package() {', 1)[1].split('\n}\n', 1)[0] + '\n}\n'
+ with tempfile.TemporaryDirectory() as tmp:
+ root = Path(tmp)
+ for node in ('n0', 'n1', 'n2', 'n3'):
+ if installed: (root / node).touch()
+ query = root / 'dpkg-query'
+ query.write_text('''#!/usr/bin/env python3
+import os,pathlib,sys
+if sys.argv[1:]==['-W','-f=','rustfs']: sys.exit(0)
+if sys.argv[1:] != ['-W','-f=${db:Status-Status}','rustfs']: sys.exit(2)
+if os.environ['MODE']=='query-error': sys.exit(42)
+if (pathlib.Path(os.environ['SCENE'])/os.environ['NODE']).exists(): print('installed');sys.exit(0)
+sys.exit(1)
+''')
+ purge = root / 'dpkg'
+ purge.write_text('''#!/usr/bin/env python3
+import os,pathlib,sys
+assert sys.argv[1:]==['-P','rustfs']
+p=pathlib.Path(os.environ['SCENE'])
+with (p/'purges').open('a') as out: out.write(os.environ['NODE']+'\\n')
+if os.environ['MODE']=='purge-error':sys.exit(42)
+if os.environ['MODE']!='left-installed':(p/os.environ['NODE']).unlink()
+''')
+ sudo = root / 'sudo'
+ sudo.write_text('#!/bin/sh\n[ "$1" = -n ] || exit 1\nshift\nexec "$@"\n')
+ systemctl = root / 'systemctl'
+ systemctl.write_text('#!/bin/sh\nexit 3\n')
+ for p in (query, purge, sudo, systemctl): p.chmod(0o700)
+ shell = '''set -euo pipefail
+NODES=(n0 n1 n2 n3); SSH_OPTS=(-o BatchMode=yes); SSH_USER=test
+stop_existing() { return 0; }
+ssh() { local arg node=""; for arg in "$@"; do case "$arg" in test@*) node="${arg##*@}" ;; esac; done; NODE="$node" bash -c "${@: -1}"; }
+''' + function + '\nfinalize_package\n'
+ env = dict(os.environ, PATH=str(root) + os.pathsep + os.environ['PATH'], SCENE=str(root), MODE=mode)
+ env.pop('BASH_ENV', None)
+ result = subprocess.run(['bash', '-c', shell], env=env, capture_output=True, text=True)
+ purges = (root / 'purges').read_text().splitlines() if (root / 'purges').exists() else []
+ return result, purges, [n for n in ('n0','n1','n2','n3') if (root / n).exists()]
+
+ def test_actual_nested_shell_passes_literal_dpkg_format_and_purges_all_four_packages(self):
+ result, purges, remaining = self.run_finalizer('success')
+ self.assertEqual(result.returncode, 0, result.stderr)
+ self.assertEqual(purges, ['n0','n1','n2','n3'])
+ self.assertEqual(remaining, [])
+ result, purges, remaining = self.run_finalizer('success', installed=False)
+ self.assertEqual(result.returncode, 0, result.stderr)
+ self.assertEqual(purges, [])
+
+ def test_query_errors_failed_purge_and_still_installed_packages_cannot_report_clean(self):
+ for mode in ('query-error', 'purge-error', 'left-installed'):
+ with self.subTest(mode=mode):
+ result, _, remaining = self.run_finalizer(mode)
+ self.assertNotEqual(result.returncode, 0)
+ self.assertEqual(remaining, ['n0','n1','n2','n3'])
+
+
+class HealAdminResponseBudgetTest(unittest.TestCase):
+ def test_complete_reviewed_8mib_response_and_strict_oversize_and_content_length(self):
+ from io import BytesIO
+ from test_validation_heal_authorization import binding
+ from scripts.validation_heal.heal_probe import ADMIN_RESPONSE_LIMIT
+ self.assertEqual(ADMIN_RESPONSE_LIMIT, 8 * 1024**2)
+ class Response(BytesIO):
+ code = 200
+ def __init__(self, raw, length=None):
+ super().__init__(raw)
+ self.headers = {"Content-Length": str(len(raw) if length is None else length)}
+ probe = Probe(binding())
+ raw = b'{"complete":"' + b'x' * (ADMIN_RESPONSE_LIMIT - len(b'{"complete":""}')) + b'"}'
+ with patch.object(probe, "credentials", return_value=("test-access", "test-secret")), \
+ patch.object(probe, "_open_admin", side_effect=lambda outgoing: Response(raw)):
+ observed = probe.admin("/rustfs/admin/v3/heal/", [], b"")
+ self.assertEqual(observed["responseBytes"], ADMIN_RESPONSE_LIMIT)
+ self.assertEqual(observed["responseBudgetBytes"], ADMIN_RESPONSE_LIMIT)
+ self.assertEqual(len(observed["response"]["complete"]), ADMIN_RESPONSE_LIMIT - len(b'{"complete":""}'))
+ for changed, length in ((raw + b" ", None), (b"{}", 3)):
+ with self.subTest(length=length), patch.object(probe, "credentials", return_value=("test-access", "test-secret")), \
+ patch.object(probe, "_open_admin", side_effect=lambda outgoing: Response(changed, length)), self.assertRaises(ValueError):
+ probe.admin("/rustfs/admin/v3/heal/", [], b"")
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test_validation_heal_authority.py b/test_validation_heal_authority.py
new file mode 100644
index 00000000..79ac4b35
--- /dev/null
+++ b/test_validation_heal_authority.py
@@ -0,0 +1,502 @@
+import copy
+import json
+import os
+from pathlib import Path
+import subprocess
+import sys
+import tempfile
+import types
+import unittest
+import uuid
+from unittest.mock import patch
+
+from scripts.validation_heal import generation_fault as generation, heal_fault as fault
+from scripts.validation_heal import authority, authority_runner as runner, native_regressions as native
+from scripts.validation_heal.healthy import BASE_OPTIONS, Fleet
+from scripts.validation_heal.matrix import AUTOMATIC_CASES, select
+import test_validation_heal as oracle
+
+
+class AuthorityCensusTests(unittest.TestCase):
+ bucket = 'rv-heal-0123456789abcdef'
+
+ def scene(self, total=4, branch='disjoint-version-quorums'):
+ first, _ = oracle.HealReadQuorumBoundaryTest().scene(total=total, data=total - 1)
+ for value in first.values():
+ for entry in value['files']:
+ entry['path'] = entry['path'].replace('boundary/exact-r-read.bin', 'conflict.bin')
+ if 'metadata' in entry:
+ entry['metadata']['key'] = 'conflict.bin'
+ version = entry['metadata']['versions'][0]
+ version['versionId'] = str(uuid.UUID(int=11)) if branch == authority.BRANCHES[0] else None
+ second = copy.deepcopy(first)
+ for value in second.values():
+ for entry in value['files']:
+ entry['sha256'] += '-generation-b'
+ if 'metadata' in entry:
+ version = entry['metadata']['versions'][0]
+ old_dir = version['dataDir']
+ version['dataDir'] = str(uuid.UUID(int=2))
+ version['versionId'] = str(uuid.UUID(int=12)) if branch == authority.BRANCHES[0] else None
+ else:
+ entry['path'] = entry['path'].replace(old_dir, str(uuid.UUID(int=2)))
+ mixed = copy.deepcopy(second)
+ members = sorted((node, row['disk']) for node, value in first.items() for row in value['formats'])
+ for node, disk in members[:total // 2]:
+ mixed[node]['files'] = [row for row in mixed[node]['files'] if row['disk'] != disk]
+ mixed[node]['files'].extend(copy.deepcopy([row for row in first[node]['files'] if row['disk'] == disk]))
+ return first, second, mixed
+
+ def check(self, first, second, mixed, total=4, branch='disjoint-version-quorums'):
+ return authority.validate_split(mixed, first, second, self.bucket, 'conflict.bin', 'H1' if total == 4 else 'H2', branch)
+
+ def test_both_original_topologies_and_real_identity_branches_have_no_authority_quorum(self):
+ for total in (4, 16):
+ for branch in authority.BRANCHES[:2]:
+ with self.subTest(total=total, branch=branch):
+ proof = self.check(*self.scene(total, branch), total, branch)
+ self.assertEqual(proof['R'], total - 1)
+ self.assertEqual([row['members'] for row in proof['groups']], [total // 2] * 2)
+ self.assertFalse(proof['crossVersionUnionClaimedAsAuthority'])
+ self.assertFalse(proof['bucketIncarnationSplitClaimed'])
+
+ def test_hidden_live_history_in_other_group_cannot_be_ignored_by_latest_only_census(self):
+ first, second, mixed = self.scene()
+ for disk in (2, 3):
+ row = next(row for row in mixed['node1']['files'] if row['disk'] == disk and 'metadata' in row)
+ old = copy.deepcopy(next(row for row in first['node1']['files'] if row['disk'] == disk and 'metadata' in row)['metadata']['versions'][0])
+ old['isLatest'] = False
+ row['metadata']['versions'].append(old)
+ with self.assertRaisesRegex(ValueError, 'sub-quorum'):
+ self.check(first, second, mixed)
+
+ def test_changed_bytes_uuid_slot_role_geometry_and_missing_members_are_not_split_proofs(self):
+ for kind in ('bytes', 'format', 'slot', 'role', 'geometry', 'missing', 'duplicate', 'same-generation', 'wrong-half'):
+ first, second, mixed = self.scene()
+ value = mixed['node1']
+ version = next(row['metadata']['versions'][0] for row in value['files'] if row['disk'] == 0 and 'metadata' in row)
+ if kind == 'bytes': value['files'][0]['sha256'] = 'changed'
+ elif kind == 'format': value['formats'][0]['value']['xl']['this'] = 'foreign'
+ elif kind == 'slot': version['erasure']['index'] = 2
+ elif kind == 'role': version['role'] = 'parity'
+ elif kind == 'geometry': version['erasure']['data_blocks'] = 2
+ elif kind == 'missing': value['files'] = [row for row in value['files'] if row['disk'] != 0]
+ elif kind == 'duplicate': value['files'].append(copy.deepcopy(value['files'][0]))
+ elif kind == 'same-generation': second = copy.deepcopy(first)
+ else:
+ value['files'] = [row for row in value['files'] if row['disk'] != 0]
+ value['files'].extend(copy.deepcopy([row for row in second['node1']['files'] if row['disk'] == 0]))
+ with self.subTest(kind=kind), self.assertRaises(ValueError): self.check(first, second, mixed)
+
+ def test_generation_sources_must_be_independent_and_branch_identity_exact(self):
+ first, second, mixed = self.scene()
+ frozen = copy.deepcopy((first, second))
+ self.check(first, second, mixed)
+ self.assertEqual((first, second), frozen)
+ for branch in ('native-authority-fence', 'disjoint-incarnations'):
+ with self.subTest(branch=branch), self.assertRaises(ValueError): self.check(first, second, mixed, branch=branch)
+ with self.assertRaises(ValueError): self.check(second, second, mixed)
+
+
+def negative_status(aborted=False):
+ objects = [] if aborted else [{'identity': {'bucket': 'owned', 'object': 'conflict.bin', 'kind': 'object',
+ 'versionId': 'null', 'bucketIncarnationId': 'incarnation', 'poolIndex': 0, 'setIndex': 0}, 'disposition': {'state': 'failed'}}]
+ return {'summary': 'stopped', 'truncated': False, 'settings': BASE_OPTIONS,
+ 'outcome': {'execution': {'state': 'aborted', 'reason': 'untraversable'} if aborted else {'state': 'completed_with_errors'},
+ 'coverage': 'partial' if aborted else 'complete', 'objectsTruncated': False, 'objects': objects,
+ 'counters': {'processed': len(objects), 'healed': 0, 'unchanged': 0, 'skipped': 0, 'failed': len(objects),
+ 'unknown': 0, 'attemptFailures': len(objects), 'overflowed': False}}}
+
+
+class AuthorityResponsibilityTests(unittest.TestCase):
+ def test_explicit_failed_object_and_typed_untraversable_abort_preserve_negative_responsibility(self):
+ for aborted in (False, True):
+ value = negative_status(aborted)
+ proof = authority.validate_negative_task(value, 'owned', BASE_OPTIONS)
+ self.assertTrue(proof['safeExplicitRefusal'])
+ self.assertFalse(proof['runtimeCommitPauseClaimed'])
+
+ def test_failed_and_aborted_terminals_cannot_be_presented_as_finished_or_running(self):
+ for aborted in (False, True):
+ for summary in ('finished', 'running', 'unknown'):
+ value = negative_status(aborted)
+ value['summary'] = summary
+ with self.subTest(aborted=aborted, summary=summary), self.assertRaises(ValueError):
+ authority.validate_negative_task(value, 'owned', BASE_OPTIONS)
+
+ def test_false_health_missing_identity_duplicate_counters_and_false_complete_abort_fail(self):
+ for kind in ('healthy', 'healed', 'foreign', 'pool', 'incarnation', 'duplicate', 'counters', 'empty', 'options', 'overflow', 'abort-coverage'):
+ value = negative_status(kind == 'abort-coverage')
+ outcome = value['outcome']
+ if kind == 'healthy': outcome['objects'][0]['disposition']['state'] = 'verified_healthy'
+ elif kind == 'healed': outcome['counters']['healed'] = 1
+ elif kind == 'foreign': outcome['objects'][0]['identity']['object'] = 'other.bin'
+ elif kind == 'pool': outcome['objects'][0]['identity']['poolIndex'] = True
+ elif kind == 'incarnation': outcome['objects'][0]['identity']['bucketIncarnationId'] = ''
+ elif kind == 'duplicate': outcome['objects'] *= 2; outcome['counters'].update(processed=2, failed=2, attemptFailures=2)
+ elif kind == 'counters': outcome['counters']['failed'] = 0
+ elif kind == 'empty': outcome['objects'] = []; outcome['counters'].update(processed=0, failed=0, attemptFailures=0)
+ elif kind == 'options': value['settings'] = {**BASE_OPTIONS, 'remove': True}
+ elif kind == 'overflow': outcome['counters']['overflowed'] = True
+ else: outcome['coverage'] = 'complete'
+ with self.subTest(kind=kind), self.assertRaises(ValueError): authority.validate_negative_task(value, 'owned', BASE_OPTIONS)
+
+ def test_peer_propagation_can_converge_for_partial_abort_without_claiming_full_coverage(self):
+ value = negative_status(True)
+ fleet = types.SimpleNamespace(record=lambda label, value: value)
+ probe = types.SimpleNamespace(binding={'endpoints': {'node1': 'url'}}, admin=unittest.mock.Mock(side_effect=[
+ {'httpStatus': 200, 'response': {'clientToken': 'token'}}, {'httpStatus': 200, 'response': value}]))
+ active = []
+ with patch.object(runner, 'Probe') as remote, patch.object(runner.time, 'sleep'):
+ remote.return_value.admin.side_effect = [{'httpStatus': 200, 'response': {'summary': 'running'}},
+ {'httpStatus': 200, 'response': copy.deepcopy(value)}]
+ proof = runner.negative_heal(probe, fleet, 'owned', BASE_OPTIONS, active,
+ {'unrecoverableGet': {'httpStatus': 503, 'code': 'SlowDownRead'}})
+ self.assertEqual(proof['coverage'], 'partial')
+ self.assertFalse(active)
+
+ def test_missing_peer_status_and_wrong_public_error_cannot_be_success(self):
+ fleet = types.SimpleNamespace(record=lambda label, value: value)
+ contract = {'unrecoverableGet': {'httpStatus': 503, 'code': 'SlowDownRead'}}
+ probe = types.SimpleNamespace(binding={'endpoints': {'node1': 'url'}}, admin=unittest.mock.Mock(side_effect=[
+ {'httpStatus': 200, 'response': {'clientToken': 'token'}}, {'httpStatus': 200, 'response': negative_status()}]))
+ active = []
+ with patch.object(runner, 'Probe') as remote, self.assertRaisesRegex(ValueError, '404/unknown'):
+ remote.return_value.admin.return_value = {'httpStatus': 404, 'response': {}}
+ runner.negative_heal(probe, fleet, 'owned', BASE_OPTIONS, active, contract)
+ self.assertEqual(len(active), 1)
+ probe.admin = lambda *args: {'httpStatus': 503, 'response': {'Code': 'SlowDown'}}
+ with self.assertRaises(ValueError): runner.negative_heal(probe, fleet, 'owned', BASE_OPTIONS, [], contract)
+
+
+class AuthorityNativeEvidenceTests(unittest.TestCase):
+ def fixture(self, directory):
+ tools, evidence = directory / 'native-tools', directory / 'native-evidence'
+ tools.mkdir(); evidence.mkdir()
+ binary = tools / 'ecstore-native-tests'
+ binary.write_bytes(b'fixed-test-executable')
+ manifest = {'schemaVersion': 1, 'candidateCommit': native.CANDIDATE, 'candidateSourceTree': authority.SOURCE_TREE,
+ 'candidateCargoLockSha256': authority.CARGO_LOCK, 'sourceFiles': {native.SOURCE: native.SOURCE_SHA},
+ 'sourceUnmodified': True, 'optimized': True, 'collectorSha256': native.sha(Path(native.__file__)),
+ 'regressions': native.REGRESSIONS, 'fleetPassClaimed': False, 'binaryName': binary.name,
+ 'binarySha256': native.sha(binary), 'runId': '123', 'runAttempt': '1'}
+ (tools / 'native-regressions.json').write_text(json.dumps(manifest))
+ run = {'id': 123, 'run_attempt': 1, 'status': 'completed', 'conclusion': 'success',
+ 'path': '.github/workflows/rustfs-functional-chain.yml'}
+ (directory / 'tool-run.json').write_text(json.dumps(run))
+ listing = ''.join(spec['test'] + ': test\n' for spec in native.REGRESSIONS.values())
+ (tools / 'test-inventory.log').write_text(listing)
+ (evidence / 'runtime-inventory.log').write_text(listing)
+ results = []
+ for key, spec in native.REGRESSIONS.items():
+ row = {'regression': key, 'test': spec['test'], 'verdict': 'PASS', 'candidateCommit': native.CANDIDATE,
+ 'binarySha256': manifest['binarySha256'], 'fleetPassClaimed': False, 'emittedBarrierLogsClaimed': False,
+ 'command': [authority.HOSTED_BINARY, spec['test'], '--exact', '--nocapture', '--test-threads=1'],
+ 'sourceAssertions': spec['sourceAssertions'], 'fenceKind': spec['fenceKind'], 'exitCode': 0}
+ (evidence / (key + '.json')).write_text(json.dumps(row))
+ (evidence / (key + '.log')).write_text(f"running 1 test\ntest {spec['test']} ... ok\ntest result: ok. 1 passed; 0 failed; 0 ignored;\n")
+ results.append(row)
+ summary = {'complete': True, 'fleetPassClaimed': False, 'canonicalCasePassClaimed': False,
+ 'sourceUnmodified': True, 'results': results}
+ (evidence / 'native-summary.json').write_text(json.dumps(summary))
+ return manifest, summary, run
+
+ def verify(self, directory):
+ return authority.verify_native(directory, {'source_sha': native.CANDIDATE})
+
+ def test_all_six_original_tests_are_checked_and_only_two_authority_guards_are_mapped(self):
+ with tempfile.TemporaryDirectory() as folder:
+ directory = Path(folder)
+ self.fixture(directory)
+ proof = self.verify(directory)
+ self.assertEqual({row['regression'] for row in proof['results']}, set(authority.NATIVE_KEYS))
+ self.assertEqual(proof['toolsBuildRun'], 123)
+ self.assertFalse(proof['runtimeCommitPauseClaimed'])
+ self.assertFalse(proof['fleetPassClaimed'])
+
+ def test_source_artifact_attempt_zero_test_forged_complete_and_non_c07_row_failures_reject(self):
+ for kind in ('source', 'tree', 'lock', 'collector', 'binary', 'run', 'attempt', 'summary', 'duplicate', 'row', 'filtered', 'listing', 'path'):
+ with self.subTest(kind=kind), tempfile.TemporaryDirectory() as folder:
+ directory = Path(folder)
+ manifest, summary, run = self.fixture(directory)
+ evidence = directory / 'native-evidence'
+ key = next(iter(native.REGRESSIONS))
+ if kind == 'source': manifest['sourceUnmodified'] = False
+ elif kind == 'tree': manifest['candidateSourceTree'] = '0' * 40
+ elif kind == 'lock': manifest['candidateCargoLockSha256'] = '0' * 64
+ elif kind == 'collector': manifest['collectorSha256'] = '0' * 64
+ elif kind == 'binary': (directory / 'native-tools/ecstore-native-tests').write_bytes(b'changed')
+ elif kind == 'run': run['conclusion'] = 'failure'
+ elif kind == 'attempt': run['run_attempt'] = 2
+ elif kind == 'summary': summary['results'][0]['verdict'] = 'FAIL'
+ elif kind == 'duplicate': summary['results'][-1] = summary['results'][0]
+ elif kind == 'row': (evidence / (key + '.json')).write_text('{}')
+ elif kind == 'filtered': (evidence / (key + '.log')).write_text('running 0 tests\ntest result: ok. 0 passed; 0 failed; 0 ignored;\n')
+ elif kind == 'listing': (evidence / 'runtime-inventory.log').write_text('0 tests\n')
+ else:
+ summary['results'][0]['command'][0] = '/tmp/download/ecstore-native-tests'
+ (evidence / (key + '.json')).write_text(json.dumps(summary['results'][0]))
+ (directory / 'native-tools/native-regressions.json').write_text(json.dumps(manifest))
+ (directory / 'tool-run.json').write_text(json.dumps(run))
+ (evidence / 'native-summary.json').write_text(json.dumps(summary))
+ with self.assertRaises(ValueError): self.verify(directory)
+
+
+class AuthorityRecoveryTests(unittest.TestCase):
+ def test_failed_generation_recovery_retains_namespace_and_does_not_run_cleanup(self):
+ with tempfile.TemporaryDirectory() as folder:
+ fleet = Fleet(['vm000'], Path(folder), 'gh-123-1-h1', runtime_profile='authority-split')
+ fleet.reserved = ['node1']
+ fleet.generation_faults.append(('node1', {'disk': 0, 'bucket': 'owned', 'key': 'conflict.bin'}))
+ with patch.object(fleet, 'stop'), patch.object(fleet, 'remote', side_effect=ValueError('partial archive')) as remote:
+ self.assertFalse(fleet.cleanup())
+ self.assertTrue(fleet.generation_faults)
+ self.assertFalse(any(call.args[1] == 'cleanup' for call in remote.call_args_list))
+
+ def test_wrong_put_ack_keeps_durable_request_pending_and_never_deletes_namespace(self):
+ with tempfile.TemporaryDirectory() as folder:
+ directory = Path(folder)
+ candidate = directory / 'candidate.json'
+ candidate.write_text(json.dumps({'source_sha': native.CANDIDATE, 'package_sha256': 'a' * 64}))
+ binary = directory / 'rc'; binary.write_bytes(b'rc')
+ args = types.SimpleNamespace(evidence=directory / 'evidence', candidate=candidate, rc_bin=binary,
+ tool_directory=directory, testing_commit='b' * 40, binary_sha='c' * 64)
+ with patch.dict(os.environ, {'GITHUB_RUN_ID': '123', 'GITHUB_RUN_ATTEMPT': '1'}), \
+ patch.object(runner, 'verify_inspector', return_value={}), patch.object(runner, 'verify_quorum_contract', return_value={}), \
+ patch.object(runner, 'Fleet') as fleet, patch.object(runner, 'Recorder') as recorder:
+ fleet.return_value.record.side_effect = lambda label, value: value
+ recorder.return_value.call.side_effect = [{}, {'VersionId': 'foreign-version'}]
+ result = runner.run_branch(args, 'H1', ['vm000'], 'disjoint-incarnations')
+ self.assertEqual(result['verdict'], 'FAIL')
+ self.assertEqual(result['cleanup'], 'FAIL')
+ fleet.return_value.cleanup.assert_not_called()
+ fleet.return_value.stop.assert_called_once()
+ self.assertEqual(recorder.return_value.call.call_count, 2)
+
+ def test_native_proof_uses_no_fleet_and_cannot_invent_h2_native_members(self):
+ with tempfile.TemporaryDirectory() as folder:
+ directory = Path(folder)
+ AuthorityNativeEvidenceTests().fixture(directory)
+ candidate = directory / 'candidate.json'; candidate.write_text(json.dumps({'source_sha': native.CANDIDATE}))
+ args = types.SimpleNamespace(evidence=directory / 'evidence', candidate=candidate, native_directory=directory)
+ with patch.object(runner, 'Fleet') as fleet:
+ result = runner.run_branch(args, 'H2', ['vm000', 'vm001', 'vm002', 'vm003'], 'native-authority-fence')
+ fleet.assert_not_called()
+ self.assertEqual(result['actualNativeUnitMembers'], 4)
+ self.assertFalse(result['fleetDeploymentPerformed'])
+ self.assertEqual(result['verdict'], 'PASS')
+
+ def test_registered_source_branches_and_both_dispatch_paths_remain_complete(self):
+ self.assertEqual(select(authority.CASE_ID)[0]['variants'], list(authority.BRANCHES))
+ self.assertIn(authority.CASE_ID, AUTOMATIC_CASES)
+ self.assertIn(authority.CASE_ID + ') driver=authority_runner.py', Path('scripts/validation_heal/run_case.sh').read_text())
+ self.assertIn(authority.CASE_ID, Path('scripts/run_validation_case.sh').read_text())
+
+
+class GenerationFaultTests(unittest.TestCase):
+ bucket = 'rv-heal-0123456789abcdef'
+ data_dir = str(uuid.UUID(int=1))
+
+ def setUp(self):
+ self.temporary = tempfile.TemporaryDirectory()
+ self.root = Path(self.temporary.name).resolve()
+ self.object = self.root / 'data' / self.bucket / 'conflict.bin'
+ self.object.mkdir(parents=True)
+ (self.root / 'quarantine').mkdir()
+ self.owner = {'runId': 'gh-123-1-h1', 'bucket': self.bucket, 'runtimeProfile': 'authority-split'}
+ self.request = {'disk': 0, 'bucket': self.bucket, 'key': 'conflict.bin'}
+ self.mount = patch.object(fault, 'mount_id', return_value=7)
+ self.mount.start()
+ self.seed('a')
+
+ def tearDown(self):
+ self.mount.stop()
+ self.temporary.cleanup()
+
+ def seed(self, label):
+ (self.object / 'xl.meta').write_bytes(('XL2 ' + label).encode())
+ (self.object / self.data_dir).mkdir(exist_ok=True)
+ (self.object / self.data_dir / 'part.1').write_bytes((label * 123).encode())
+
+ def capture(self, label):
+ with fault.directory(self.root) as root, fault.parent(root, ['data', self.bucket, 'conflict.bin']) as obj:
+ manifest = generation.tree(obj, fault)[1]
+ return generation.capture(root, self.owner, {**self.request, 'generation': label, 'expectedFiles': manifest}, fault)
+
+ def pair(self):
+ a = self.capture('a')
+ self.seed('b')
+ b = self.capture('b')
+ return a, b
+
+ def install(self, b):
+ with fault.directory(self.root) as root:
+ return generation.install(root, self.owner, {**self.request, 'expectedSceneSha256': b['treeSha256']}, fault)
+
+ def recover(self):
+ actual_stat = os.stat
+ def root_owned(path, **kwargs):
+ value = actual_stat(path, **kwargs)
+ return types.SimpleNamespace(st_mode=value.st_mode, st_uid=0, st_dev=value.st_dev)
+ with fault.directory(self.root) as root, patch.object(generation.os, 'stat', side_effect=root_owned):
+ return generation.recover(root, self.owner, self.request, fault)
+
+ def test_complete_old_and_new_trees_are_preserved_then_only_last_ack_is_restored(self):
+ a, b = self.pair()
+ receipt = self.install(b)
+ self.assertEqual(receipt['injectedTreeSha256'], a['treeSha256'])
+ self.assertEqual((self.object / 'xl.meta').read_bytes(), b'XL2 a')
+ self.assertEqual((self.root / 'quarantine' / 'C07-pre-split' / 'xl.meta').read_bytes(), b'XL2 b')
+ result = self.recover()
+ self.assertEqual(result['state'], 'CLEAR')
+ self.assertFalse(result['productRepairClaimed'])
+ self.assertEqual((self.object / 'xl.meta').read_bytes(), b'XL2 b')
+ self.assertEqual((self.root / 'quarantine' / 'C07-failed-current' / 'xl.meta').read_bytes(), b'XL2 a')
+ self.assertEqual(self.recover(), result)
+
+ def test_changed_live_bytes_or_archive_authority_refuse_before_first_rename(self):
+ _, b = self.pair()
+ self.seed('drift')
+ with self.assertRaises(ValueError): self.install(b)
+ self.assertFalse((self.root / 'quarantine' / 'C07-replay-intent.json').exists())
+ self.seed('b')
+ (self.root / 'quarantine' / 'C07-a' / 'object' / 'xl.meta').write_bytes(b'foreign')
+ with self.assertRaises(ValueError): self.install(b)
+ self.assertFalse((self.root / 'quarantine' / 'C07-replay-intent.json').exists())
+
+ def test_hardlinks_symlinks_foreign_leaves_and_unknown_directories_cannot_enter_archive(self):
+ for kind in ('hardlink', 'symlink', 'leaf', 'directory'):
+ with self.subTest(kind=kind):
+ target = self.object / self.data_dir / 'part.1'
+ original = target.read_bytes()
+ if kind == 'hardlink':
+ alias = self.root / 'outside'
+ os.link(target, alias)
+ elif kind == 'symlink':
+ alias = self.root / 'outside'
+ alias.write_bytes(original)
+ target.unlink()
+ target.symlink_to(alias)
+ elif kind == 'leaf':
+ alias = self.object / self.data_dir / 'credential.json'
+ alias.write_bytes(b'foreign')
+ else:
+ alias = self.object / 'foreign-directory'
+ alias.mkdir()
+ with self.assertRaises((ValueError, OSError)): self.capture('a')
+ if kind == 'symlink':
+ target.unlink()
+ target.write_bytes(original)
+ if alias.is_dir(): alias.rmdir()
+ else: alias.unlink()
+
+ def test_capture_rejects_an_incomplete_caller_manifest_and_duplicate_archive(self):
+ with fault.directory(self.root) as root:
+ with self.assertRaises(ValueError): generation.capture(root, self.owner,
+ {**self.request, 'generation': 'a', 'expectedFiles': []}, fault)
+ self.assertFalse((self.root / 'quarantine' / 'C07-a').exists())
+ self.capture('a')
+ with self.assertRaises(FileExistsError): self.capture('a')
+
+ def test_full_protected_generation_preserves_exact_candidate_integrity_sidecar(self):
+ name = 'part.1.integrity.' + str(uuid.UUID(int=77))
+ sidecar = self.object / self.data_dir / name
+ sidecar.write_bytes(b'candidate-shard-integrity-a')
+ a = self.capture('a')
+ self.seed('b'); sidecar.unlink()
+ newest_sidecar = self.object / self.data_dir / ('part.1.integrity.' + str(uuid.UUID(int=78)))
+ newest_sidecar.write_bytes(b'candidate-shard-integrity-b')
+ b = self.capture('b')
+ self.assertTrue(any(row['path'].endswith(name) for row in a['files']))
+ self.install(b)
+ self.assertEqual(sidecar.read_bytes(), b'candidate-shard-integrity-a')
+ self.assertFalse(newest_sidecar.exists())
+ self.recover()
+ self.assertEqual(newest_sidecar.read_bytes(), b'candidate-shard-integrity-b')
+ self.assertFalse(sidecar.exists())
+
+ def test_commitment_without_payload_duplicate_nonce_and_nil_nonce_are_rejected(self):
+ for name in ('part.2.integrity.' + str(uuid.UUID(int=77)), 'part.1.integrity.' + str(uuid.UUID(int=0)),
+ 'part.1.integrity.' + str(uuid.UUID(int=78))):
+ with self.subTest(name=name):
+ target = self.object / self.data_dir / name
+ target.write_bytes(b'commitment')
+ first = self.object / self.data_dir / ('part.1.integrity.' + str(uuid.UUID(int=77)))
+ if name.endswith(str(uuid.UUID(int=78))): first.write_bytes(b'original-commitment')
+ with self.assertRaises(ValueError): self.capture('a')
+ target.unlink()
+ if first.exists(): first.unlink()
+
+ def test_bucket_key_profile_and_device_drift_cannot_restore_an_unrelated_generation(self):
+ _, b = self.pair()
+ self.install(b)
+ for field, value in (('bucket', 'rv-heal-fedcba9876543210'), ('key', '../conflict.bin'), ('disk', 1)):
+ with self.subTest(field=field), fault.directory(self.root) as root, self.assertRaises((ValueError, FileNotFoundError)):
+ generation.recover(root, self.owner, {**self.request, field: value}, fault)
+ with fault.directory(self.root) as root, self.assertRaises(ValueError):
+ generation.recover(root, {**self.owner, 'runtimeProfile': 'manual'}, self.request, fault)
+ with patch.object(fault, 'mount_id', return_value=8), fault.directory(self.root) as root, self.assertRaises(ValueError):
+ generation.recover(root, self.owner, self.request, fault)
+
+ def test_partial_recovery_remains_dirty_and_does_not_remove_the_failed_scene(self):
+ _, b = self.pair()
+ self.install(b)
+ failed = self.root / 'quarantine' / 'C07-failed-current'
+ self.object.rename(failed)
+ self.object.mkdir()
+ (self.object / 'xl.meta').write_bytes(b'partial')
+ with self.assertRaises(ValueError): self.recover()
+ self.assertEqual((failed / 'xl.meta').read_bytes(), b'XL2 a')
+ self.assertFalse((self.root / 'quarantine' / 'C07-replay-recovered.json').exists())
+
+
+class GenerationLinuxTests(unittest.TestCase):
+ @unittest.skipUnless(sys.platform == 'linux', 'requires original Linux mount descriptors and root-owned recovery')
+ def test_real_descriptors_restore_last_ack_and_reject_nested_bind_mount(self):
+ program = '''import json,subprocess,tempfile,uuid
+from pathlib import Path
+from scripts.validation_heal import generation_fault as generation,heal_fault as fault
+with tempfile.TemporaryDirectory() as folder:
+ root=Path(folder).resolve()
+ bucket='rv-heal-0123456789abcdef'
+ obj=root/'data'/bucket/'conflict.bin'
+ obj.mkdir(parents=True)
+ (root/'quarantine').mkdir()
+ data_dir=str(uuid.UUID(int=1))
+ (obj/data_dir).mkdir()
+ owner={'runId':'gh-123-1-h1','bucket':bucket,'runtimeProfile':'authority-split'}
+ request={'disk':0,'bucket':bucket,'key':'conflict.bin'}
+ captured={}
+ for label in ('a','b'):
+ (obj/'xl.meta').write_bytes(('XL2 '+label).encode())
+ (obj/data_dir/'part.1').write_bytes(label.encode()*512)
+ (obj/data_dir/('part.1.integrity.'+str(uuid.UUID(int=77)))).write_bytes(label.encode()*32)
+ with fault.directory(root) as fd,fault.parent(fd,['data',bucket,'conflict.bin']) as child:
+ manifest=generation.tree(child,fault)[1]
+ captured[label]=generation.capture(fd,owner,{**request,'generation':label,'expectedFiles':manifest},fault)
+ with fault.directory(root) as fd:
+ generation.install(fd,owner,{**request,'expectedSceneSha256':captured['b']['treeSha256']},fault)
+ assert (obj/'xl.meta').read_bytes()==b'XL2 a'
+ restored=generation.recover(fd,owner,request,fault)
+ assert restored['state']=='CLEAR' and restored['originalLastACKGenerationRestored']
+ assert (obj/'xl.meta').read_bytes()==b'XL2 b'
+ assert generation.recover(fd,owner,request,fault)==restored
+ outside=root/'outside'
+ outside.mkdir()
+ subprocess.run(['mount','--bind',str(outside),str(obj)],check=True)
+ try:
+ with fault.directory(root) as fd:
+ try: generation.capture(fd,owner,{**request,'generation':'a','expectedFiles':[]},fault)
+ except ValueError as error: assert 'mount' in str(error)
+ else: raise AssertionError('nested bind mount was accepted')
+ finally: subprocess.run(['umount',str(obj)],check=True)
+print(json.dumps({'realMountDescriptors':True,'lastACKRestored':True,'nestedBindRefused':True}))
+'''
+ result = subprocess.run(['sudo', '-n', 'unshare', '-m', sys.executable, '-B', '-c', program],
+ capture_output=True, text=True, timeout=60)
+ self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
+ self.assertTrue(json.loads(result.stdout)['nestedBindRefused'])
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/test_validation_heal_authorization.py b/test_validation_heal_authorization.py
new file mode 100644
index 00000000..5d6d885b
--- /dev/null
+++ b/test_validation_heal_authorization.py
@@ -0,0 +1,230 @@
+"""Exercise signing, denial isolation and temporary-principal finalization."""
+
+from datetime import UTC, datetime, timedelta
+import hashlib
+import hmac
+import io
+import json
+from pathlib import Path
+import tempfile
+import unittest
+from unittest.mock import patch
+
+from botocore.awsrequest import AWSRequest
+
+from scripts.validation_heal.authorization import DENIALS, authorization_requests, iam_exchange, validate_denial
+from scripts.validation_heal.heal_probe import Probe
+from scripts.validation_heal.heal_signing import sign_admin_request
+from scripts.validation_heal.healthy import BASE_OPTIONS, Fleet
+from scripts.validation_heal.matrix import select
+
+
+def binding():
+ return {"schemaVersion": 1, "runId": "manual-auth-test", "jumpHostOnly": True,
+ "exclusiveHealService": True, "leaseExpiresAt": (datetime.now(UTC) + timedelta(hours=1)).isoformat(),
+ "bucketPrefix": "rv-heal-auth-test", "endpoints": {"node1": "http://127.0.0.1:19000"},
+ "defaultEndpoint": "node1", "candidateCommit": "a" * 40, "harnessCommit": "b" * 40,
+ "candidatePackageSha256": "c" * 64, "candidateBinarySha256": "d" * 64,
+ "rcBinarySha256": "e" * 64, "maxInventoryObjects": 128, "maxInventoryBytes": 1024**2}
+
+
+class Response(io.BytesIO):
+ code = 200
+ headers = {}
+
+
+class AuthorizationBoundaryTest(unittest.TestCase):
+ def test_s3_complete_get_sets_known_result_and_partial_body_cannot_pass(self):
+ from unittest.mock import Mock
+ from scripts.validation_heal.quorum_boundary import read_observation
+ probe, client = Probe(binding(), credential_pair=("test-access", "test-secret")), Mock()
+ body = b"complete-acknowledged-plaintext"
+ expected = {"payload": {"size": len(body), "sha256": hashlib.sha256(body).hexdigest()}}
+ def response(length):
+ return {"Body": io.BytesIO(body), "ContentLength": length,
+ "ResponseMetadata": {"HTTPStatusCode": 200}}
+ client.get_object.side_effect = [response(len(body)), response(len(body) + 1)]
+ with patch.object(probe, "s3_client", return_value=client):
+ observed = probe.s3("get_object", {"Bucket": binding()["bucketPrefix"], "Key": "unique.bin"})
+ self.assertIs(observed["resultUnknown"], False)
+ read_observation(observed, expected)
+ partial = probe.s3("get_object", {"Bucket": binding()["bucketPrefix"], "Key": "unique.bin"})
+ self.assertIs(partial["acknowledged"], False)
+ with self.assertRaises(ValueError): read_observation(partial, expected)
+
+ def test_server_info_is_exact_read_only_evidence_route(self):
+ probe = Probe(binding(), credential_pair=("test-access", "test-secret"))
+ with patch.object(probe, "_open_admin", return_value=Response(b'{"info":{"servers":[]}}')) as opened:
+ self.assertEqual(probe.admin("/rustfs/admin/v3/info", [], b"", method="GET")["httpStatus"], 200)
+ self.assertEqual(opened.call_args.args[0].get_method(), "GET")
+ for method, query, body, auth in (("POST", [], b"", "signed"), ("GET", [("x", "y")], b"", "signed"),
+ ("GET", [], b"{}", "signed"), ("GET", [], b"", "anonymous")):
+ with self.subTest(method=method, query=query, body=body, auth=auth), self.assertRaises(ValueError):
+ probe.admin("/rustfs/admin/v3/info", query, body, method=method, auth=auth)
+
+ def test_old_timestamp_recomputes_valid_signature_and_credential_scope(self):
+ timestamp, access, secret = "20000102T030405Z", "isolated-access", "isolated-secret"
+ body = b'{"dryRun":true}'
+ digest = hashlib.sha256(body).hexdigest()
+ outgoing = AWSRequest(method="POST", url="http://127.0.0.1:19000/rustfs/admin/v3/heal/rv-heal-auth-test?clientToken=token",
+ data=body, headers={"Content-Type": "application/json", "x-amz-content-sha256": digest})
+ sign_admin_request(outgoing, access, secret, timestamp=timestamp)
+ # Independent SigV4 calculation for this fixed wire request. The old
+ # date participates in canonical headers, credential scope and HMAC.
+ canonical = "\n".join(("POST", "/rustfs/admin/v3/heal/rv-heal-auth-test", "clientToken=token",
+ "content-type:application/json\nhost:127.0.0.1:19000\nx-amz-content-sha256:" + digest + "\nx-amz-date:" + timestamp + "\n",
+ "content-type;host;x-amz-content-sha256;x-amz-date", digest))
+ scope = "20000102/us-east-1/s3/aws4_request"
+ string_to_sign = "\n".join(("AWS4-HMAC-SHA256", timestamp, scope, hashlib.sha256(canonical.encode()).hexdigest()))
+ key = ("AWS4" + secret).encode()
+ for value in ("20000102", "us-east-1", "s3", "aws4_request"):
+ key = hmac.new(key, value.encode(), hashlib.sha256).digest()
+ signature = hmac.new(key, string_to_sign.encode(), hashlib.sha256).hexdigest()
+ self.assertEqual(outgoing.headers["X-Amz-Date"], timestamp)
+ self.assertIn("Credential=" + access + "/" + scope, outgoing.headers["Authorization"])
+ self.assertTrue(outgoing.headers["Authorization"].endswith("Signature=" + signature))
+ tampered = canonical.replace(timestamp, "20000103T030405Z")
+ self.assertNotEqual(hashlib.sha256(canonical.encode()).hexdigest(), hashlib.sha256(tampered.encode()).hexdigest())
+
+ def test_probe_uses_isolated_principal_and_expired_client_clock(self):
+ probe = Probe(binding(), credential_pair=("client-access", "client-secret"))
+ sent = []
+ def opened(outgoing):
+ sent.append(outgoing)
+ return Response(b'{"Code":"RequestTimeTooSkewed"}')
+ with patch.dict("os.environ", {"HEAL_ACCESS_KEY": "root-access", "HEAL_SECRET_KEY": "root-secret", "HEAL_SESSION_TOKEN": "root-token"}), \
+ patch.object(probe, "_open_admin", side_effect=opened):
+ observed = probe.admin("/rustfs/admin/v3/heal/rv-heal-auth-test", [], b"{}", auth="expired-signature")
+ signed_at = datetime.strptime(observed["isolatedClientSigningTimestamp"], "%Y%m%dT%H%M%SZ").replace(tzinfo=UTC)
+ self.assertAlmostEqual((datetime.now(UTC) - signed_at).total_seconds(), 86400, delta=3)
+ headers = dict(sent[0].header_items())
+ self.assertIn("Credential=client-access/", headers["Authorization"])
+ self.assertNotIn("X-amz-security-token", headers)
+ self.assertNotIn("root-access", json.dumps(headers))
+
+ def test_denial_requires_exact_code_and_no_nested_json_or_xml_disclosure(self):
+ for mode, (status, code) in DENIALS.items():
+ good = {"httpStatus": status, "response": {"Code": code}}
+ self.assertTrue(validate_denial(good, mode)["noTaskDisclosure"])
+ for poisoned in ({**good, "httpStatus": 200}, {**good, "response": {"Code": "InternalError"}},
+ {**good, "response": {"Code": code, "detail": [{"objects": []}]}},
+ {**good, "response": {"Code": code, "clientToken": "admitted"}},
+ {**good, "response": f'{code}'},
+ {"response": {"Code": code}, "transportError": "timeout"}):
+ with self.subTest(mode=mode, response=poisoned), self.assertRaises(ValueError):
+ validate_denial(poisoned, mode)
+
+ def test_unsigned_route_refusal_is_not_the_unreachable_handler_error(self):
+ self.assertEqual(DENIALS["anonymous"], (403, "AccessDenied"))
+ validate_denial({"httpStatus": 403, "response": 'AccessDeniedSignature is required'}, "anonymous")
+ with self.assertRaises(ValueError):
+ validate_denial({"httpStatus": 400, "response": {"Code": "InvalidRequest"}}, "anonymous")
+
+ def test_iam_evidence_redacts_credentials_without_modifying_oracle_input(self):
+ with tempfile.TemporaryDirectory() as directory:
+ fleet = Fleet([], Path(directory), "manual-auth-test")
+ fleet.secret_values.update({"root-access", "root-secret", "user-secret"})
+ probe = Probe(binding(), credential_pair=("root-access", "root-secret"))
+ with patch.object(probe, "_open_admin", return_value=Response(b'{"message":"user-secret root-access root-secret"}')):
+ result = iam_exchange(probe, fleet, "add-user", "rvhealowneduser", secret="user-secret")
+ self.assertIn("user-secret", result["response"])
+ archive = (Path(directory) / "00001-iam-add-user.json").read_text()
+ for secret in fleet.secret_values:
+ self.assertNotIn(secret, archive)
+ self.assertFalse(json.loads(archive)["requestBodyArchived"])
+ with self.assertRaisesRegex(ValueError, "namespace"):
+ iam_exchange(probe, fleet, "remove-user", "unrelateduser")
+ original = {"user-secret": {"details": ["user-secret"]}}
+ self.assertIs(fleet.record("secret-key", original), original)
+ self.assertNotIn("user-secret", (Path(directory) / "00002-secret-key.json").read_text())
+
+
+class PrincipalLifecycleTest(unittest.TestCase):
+ def execute(self, directory, *, failed_operation=None, negative_transport=False, revocation_broken=False):
+ users, iam_calls, negative_calls = set(), [], []
+ active = []
+ fleet = Fleet([], Path(directory), "manual-auth-test")
+ scene = {"node1": {"files": [], "formats": []}}
+ class Client:
+ def __init__(self, bound, *, credential_pair=None):
+ self.binding = bound
+ self.credential_pair = credential_pair
+ def credentials(self):
+ return self.credential_pair or ("root-access", "root-secret")
+ def s3(self, operation, params):
+ return {"acknowledged": False, "resultUnknown": False,
+ "response": {"ResponseMetadata": {"HTTPStatusCode": 403}, "Error": {"Code": "AccessDenied"}}}
+ def admin(self, path, query, body, auth="signed"):
+ if self.credential_pair and self.credential_pair[0] not in users:
+ return {"httpStatus": 403, "response": {"Code": "AccessDenied" if revocation_broken else "InvalidAccessKeyId"}}
+ denied = self.credential_pair and self.credential_pair[0].endswith("denied")
+ if auth != "signed" or denied:
+ mode = "denied-user" if denied else auth
+ operation = "stop" if dict(query).get("forceStop") else "status" if query else "start"
+ negative_calls.append((mode, operation))
+ if negative_transport and (mode, operation) == ("bad-signature", "status"):
+ raise OSError("isolated transport failure")
+ status, code = DENIALS[mode]
+ return {"httpStatus": status, "response": {"Code": code}}
+ if not query:
+ return {"httpStatus": 200, "response": {"clientToken": "allowed-token" if self.credential_pair else "root-token"}}
+ return {"httpStatus": 200, "response": {"summary": "finished"}}
+ def inventory(self, *args, **kwargs):
+ return {}
+ def exchange(probe, recorded, operation, user, **kwargs):
+ iam_calls.append((operation, user))
+ if operation == "add-policy":
+ self.assertEqual(kwargs["document"], {"Version": "2012-10-17", "Statement": [{"Effect": "Allow", "Action": ["admin:Heal"]}]})
+ if operation == "add-user": users.add(user)
+ if operation == failed_operation: raise ValueError("injected IAM failure: " + operation)
+ if operation == "remove-user": users.discard(user)
+ self.addCleanup(lambda: self.assertFalse(active, "tokens must be finalized even on IAM failure"))
+ with patch("scripts.validation_heal.authorization.Probe", Client), \
+ patch("scripts.validation_heal.authorization.iam_exchange", side_effect=exchange), \
+ patch.object(fleet, "snapshot", return_value=scene), \
+ patch("scripts.validation_heal.authorization.compare_inventories", return_value={"unchanged": True}):
+ try:
+ branches = authorization_requests(Client(binding()), fleet, "rv-heal-auth-test", scene, {}, active, BASE_OPTIONS)
+ return branches, iam_calls, negative_calls
+ finally:
+ self.last_calls, self.last_negatives = iam_calls, negative_calls
+
+ def test_every_source_principal_controls_start_status_stop_and_is_removed(self):
+ with tempfile.TemporaryDirectory() as directory:
+ branches, iam_calls, negatives = self.execute(directory)
+ self.assertEqual(set(branches), set(select("A08")[0]["variants"]))
+ self.assertTrue(all(branch["verdict"] == "PASS" for branch in branches.values()))
+ self.assertEqual(set(negatives), {(mode, operation) for mode in DENIALS for operation in ("start", "status", "stop")})
+ self.assertEqual([operation for operation, _ in iam_calls].count("remove-user"), 2)
+ self.assertEqual([operation for operation, _ in iam_calls][-1], "remove-policy")
+
+ def test_failed_or_ambiguous_admission_keeps_owned_principal_for_cleanup(self):
+ with tempfile.TemporaryDirectory() as directory:
+ with self.assertRaisesRegex(ValueError, "injected IAM failure"):
+ self.execute(directory, failed_operation="add-user")
+ self.assertEqual([operation for operation, _ in self.last_calls], ["add-user", "remove-user"])
+
+ def test_cleanup_error_cannot_pass_and_other_cleanup_operations_continue(self):
+ with tempfile.TemporaryDirectory() as directory:
+ with self.assertRaisesRegex(ValueError, "finalization failed"):
+ self.execute(directory, failed_operation="detach")
+ self.assertEqual([operation for operation, _ in self.last_calls][-3:], ["remove-user", "remove-user", "remove-policy"])
+ result = next(Path(directory).glob("*-iam-finalization.json"))
+ self.assertEqual(json.loads(result.read_text())["cleanup"], "FAIL")
+
+ def test_one_denial_transport_failure_preserves_all_branches_and_never_passes(self):
+ with tempfile.TemporaryDirectory() as directory:
+ with self.assertRaisesRegex(ValueError, "every start/status/stop"):
+ self.execute(directory, negative_transport=True)
+ self.assertEqual(len(self.last_negatives), 12)
+ result = next(Path(directory).glob("*-authorization-negative-branches.json"))
+ self.assertEqual(json.loads(result.read_text())["bad-signature"]["verdict"], "FAIL")
+
+ def test_removed_credentials_must_return_the_exact_revocation_error(self):
+ with tempfile.TemporaryDirectory() as directory:
+ with self.assertRaisesRegex(ValueError, "removed temporary credentials"):
+ self.execute(directory, revocation_broken=True)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test_validation_heal_bucket_metadata.py b/test_validation_heal_bucket_metadata.py
new file mode 100644
index 00000000..a8e9347d
--- /dev/null
+++ b/test_validation_heal_bucket_metadata.py
@@ -0,0 +1,224 @@
+import copy
+import hashlib
+import json
+from pathlib import Path
+import tempfile
+from types import SimpleNamespace
+import unittest
+from unittest.mock import patch
+
+from scripts.validation_heal import bucket_metadata as executor, bucket_records as backend, heal_fault as fault
+from scripts.validation_heal import fleet_node as node
+from scripts.validation_heal.matrix import AUTOMATIC_CASES, select
+
+
+class BucketMetadataTests(unittest.TestCase):
+ bucket = "rv-heal-0123456789abcdef"
+ value = b"XL2 " + b"\x01\x00\x03\x00" + b"same-run-config-envelope" * 10
+
+ def test_system_allowlist_cannot_escape_or_widen_generic_faults(self):
+ owned = backend.bucket_path(self.bucket, self.bucket) + "/xl.meta"
+ self.assertEqual(backend.record_parts(owned, self.bucket, self.bucket)[-1], "xl.meta")
+ for path, bucket in ((owned.replace("buckets", "config/iam"), self.bucket),
+ (owned + "/../xl.meta", self.bucket),
+ (owned.replace(".metadata.bin", ".bucket-incarnation"), self.bucket),
+ (owned.replace("xl.meta", "part.1"), self.bucket), (owned, "foreign")):
+ with self.subTest(path=path, bucket=bucket), self.assertRaises(ValueError):
+ backend.record_parts(path, bucket, self.bucket)
+ with self.assertRaises(ValueError):
+ fault.target_parts(owned, self.bucket)
+
+ def test_unknown_envelope_preserves_every_byte_besides_reviewed_major(self):
+ changed = backend.changed_bytes(self.value, "future-xl-schema")
+ self.assertEqual(changed[:4], b"XL2 ")
+ self.assertEqual(changed[4:6], b"\x02\x00")
+ self.assertEqual(changed[6:], self.value[6:])
+ for original in (b"", b"not-XL2!", changed):
+ with self.assertRaises(ValueError):
+ backend.changed_bytes(original, "future-xl-schema")
+
+ def scene(self, root, kind):
+ relative = backend.bucket_path(self.bucket, self.bucket) + "/xl.meta"
+ path = root / "data" / relative
+ path.parent.mkdir(parents=True)
+ (root / "quarantine").mkdir()
+ path.write_bytes(self.value)
+ owner = {"runId": "test-run", "bucketPrefix": self.bucket}
+ request = {"bucket": self.bucket, "faultId": "D11-negative-0", "relativePath": relative,
+ "expectedSha256": fault.digest(self.value), "kind": kind}
+ return path, owner, request
+
+ @unittest.skipUnless(Path('/proc/self/fdinfo').is_dir(), 'Linux fd/mount guard required')
+ def test_faults_keep_originals_and_reject_unrecognized_post_fault_writes(self):
+ for kind in ("missing", "truncate", "future-xl-schema"):
+ with self.subTest(kind=kind), tempfile.TemporaryDirectory() as temp:
+ root = Path(temp).resolve()
+ path, owner, request = self.scene(root, kind)
+ with fault.directory(root) as fd:
+ receipt = backend.apply(fd, owner, fault, request)
+ self.assertEqual((root / "quarantine" / receipt["backup"]).read_bytes(), self.value)
+ self.assertTrue((root / "quarantine" / (request["faultId"] + ".intent.json")).exists())
+ path.write_bytes(b"new acknowledged config generation")
+ with self.assertRaisesRegex(ValueError, "post-fault rewrite"):
+ backend.recover(fd, owner, fault, request)
+ self.assertEqual(path.read_bytes(), b"new acknowledged config generation")
+ changed = backend.changed_bytes(self.value, kind)
+ if changed is None:
+ path.unlink()
+ else:
+ path.write_bytes(changed)
+ restored = backend.recover(fd, owner, fault, request)
+ self.assertEqual(path.read_bytes(), self.value)
+ self.assertFalse(restored["physicalRepairPassAllowed"])
+
+ @unittest.skipUnless(Path('/proc/self/fdinfo').is_dir(), 'Linux fd/mount guard required')
+ def test_symlink_and_hardlink_record_targets_are_rejected_before_mutation(self):
+ for kind in ("symlink", "hardlink"):
+ with self.subTest(kind=kind), tempfile.TemporaryDirectory() as temp:
+ root = Path(temp).resolve()
+ path, owner, request = self.scene(root, "truncate")
+ other = root / "unrelated"
+ other.write_bytes(self.value)
+ path.unlink()
+ if kind == "symlink":
+ path.symlink_to(other)
+ else:
+ path.hardlink_to(other)
+ with fault.directory(root) as fd, self.assertRaises((ValueError, OSError)):
+ backend.apply(fd, owner, fault, request)
+ self.assertEqual(other.read_bytes(), self.value)
+ self.assertFalse(list((root / "quarantine").iterdir()))
+
+ def test_empty_default_success_and_transport_error_are_not_unknown_schema_refusal(self):
+ good = {"acknowledged": False, "resultUnknown": False, "response": {
+ "ResponseMetadata": {"HTTPStatusCode": 500}, "Error": {"Code": "InternalError"}}}
+ executor.refusal(good)
+ for observed in ({"acknowledged": True, "resultUnknown": False, "response": {}},
+ {"acknowledged": False, "resultUnknown": True, "response": None},
+ {**good, "response": {"ResponseMetadata": {"HTTPStatusCode": 404}, "Error": {"Code": "NoSuchBucket"}}}):
+ with self.assertRaises(ValueError):
+ executor.refusal(observed)
+ with self.assertRaises(ValueError):
+ executor.configuration({"acknowledged": True, "resultUnknown": False, "response": {"ResponseMetadata": {}}},
+ "get_bucket_versioning")
+
+ def test_duplicate_files_or_sensitive_iam_block_the_oracle(self):
+ row = {"disk": 0, "path": "exact-record", "size": 5, "sha256": "a" * 64}
+ sample = {"node": {"credentialsRead": False, "files": [row],
+ "iamNamespace": [{"disk": 0, "present": True, "traversed": False, "formatOnlyRead": True,
+ "sensitiveSubtrees": dict.fromkeys(backend.IAM_SENSITIVE_SUBTREES, False)}]}}
+ executor.system_files(sample)
+ for kind in ("duplicate", "iam", "traversal"):
+ broken = copy.deepcopy(sample)
+ if kind == "duplicate":
+ broken["node"]["files"].append(row)
+ elif kind == "iam":
+ broken["node"]["iamNamespace"][0]["sensitiveSubtrees"]["users"] = True
+ else:
+ broken["node"]["iamNamespace"][0]["traversed"] = True
+ with self.assertRaises(ValueError):
+ executor.system_files(broken)
+
+ def observe_format(self, root, *, corrupt=False):
+ path = root / "data/.rustfs.sys" / backend.IAM_FORMAT_KEY / "xl.meta"
+ calls, reads = [], []
+
+ def digest(file):
+ reads.append(file)
+ return hashlib.sha256(file.read_bytes()).hexdigest()
+
+ def command(*args, **kwargs):
+ calls.append(args)
+ if corrupt:
+ return SimpleNamespace(returncode=1, stdout="")
+ if args[1] == "--verify-inline":
+ value = {"physicalBitrotVerified": True, "bucket": ".rustfs.sys", "key": backend.IAM_FORMAT_KEY}
+ else:
+ value = {"bucket": ".rustfs.sys", "key": backend.IAM_FORMAT_KEY,
+ "versions": [{"deleted": False, "versionId": "null", "inline": {"length": 42}}]}
+ return SimpleNamespace(returncode=0, stdout=json.dumps(value))
+
+ result = backend.snapshot((root,), {"bucketPrefix": self.bucket},
+ {"buckets": [self.bucket, self.bucket + "-control"],
+ "metadataFaults": {"0:" + str(path.relative_to(root / "data")):
+ {"size": path.stat().st_size, "sha256": hashlib.sha256(path.read_bytes()).hexdigest()}}},
+ node.checked, digest, Path('/inspector'), command)
+ return result, reads, calls
+
+ def test_startup_format_is_read_only_frozen_and_verified_without_payload_output(self):
+ with tempfile.TemporaryDirectory() as temp:
+ root = Path(temp).resolve()
+ path = root / "data/.rustfs.sys" / backend.IAM_FORMAT_KEY / "xl.meta"
+ path.parent.mkdir(parents=True)
+ path.write_bytes(self.value)
+ observed, reads, calls = self.observe_format(root)
+ self.assertEqual(reads, [path])
+ self.assertEqual([call[-1] for call in calls], [backend.IAM_FORMAT_KEY, "null"])
+ self.assertEqual(observed["files"][0]["recordClass"], "iam-format")
+ self.assertTrue(observed["iamNamespace"][0]["present"])
+ self.assertNotIn("same-run-config-envelope", json.dumps(observed))
+ before = executor.system_files({"node": observed})
+ path.write_bytes(self.value + b"changed")
+ after, _, _ = self.observe_format(root)
+ self.assertNotEqual(before, executor.system_files({"node": after}))
+ with self.assertRaises(ValueError):
+ backend.record_parts(str(path.relative_to(root / "data")), self.bucket, self.bucket)
+
+ def test_sensitive_subtree_presence_blocks_before_any_walk_or_file_read(self):
+ for subtree in backend.IAM_SENSITIVE_SUBTREES:
+ with self.subTest(subtree=subtree), tempfile.TemporaryDirectory() as temp:
+ root = Path(temp).resolve()
+ folder = root / "data/.rustfs.sys/config/iam" / subtree
+ folder.mkdir(parents=True)
+ (folder / "never-read").write_bytes(b"sensitive")
+ with patch.object(backend.os, "walk", side_effect=AssertionError("IAM walk")), \
+ self.assertRaisesRegex(ValueError, "sensitive IAM subtree"):
+ backend.snapshot((root,), {"bucketPrefix": self.bucket},
+ {"buckets": [self.bucket, self.bucket + "-control"]}, node.checked,
+ lambda _: self.fail("credential read"), Path('/inspector'), lambda *_: self.fail("decode"))
+
+ def test_iam_format_decode_failure_cannot_use_bucket_fault_exception(self):
+ with tempfile.TemporaryDirectory() as temp:
+ root = Path(temp).resolve()
+ path = root / "data/.rustfs.sys" / backend.IAM_FORMAT_KEY / "xl.meta"
+ path.parent.mkdir(parents=True)
+ path.write_bytes(self.value)
+ with self.assertRaisesRegex(ValueError, "decoding failed"):
+ self.observe_format(root, corrupt=True)
+
+ def test_remote_node_script_compiles_with_dedicated_backend(self):
+ directory = Path(__file__).parent / "scripts/validation_heal"
+ source = (directory / "fleet_node.py").read_text()
+ for name in ("heal_fault", "bucket_records"):
+ source += f"\nimport types\n{name} = types.ModuleType('{name}')\nexec(" + repr((directory / (name + ".py")).read_text()) + f", {name}.__dict__)\n"
+ compile(source, "remote-D11", "exec")
+ self.assertIn(executor.CASE_ID, AUTOMATIC_CASES)
+ self.assertEqual(select(executor.CASE_ID)[0]["variants"], list(executor.BRANCHES))
+
+ def test_system_snapshot_receives_the_real_service_bucket_prefix(self):
+ owner = {"bucket": self.bucket, "inspectorSha256": "a" * 64}
+ with patch.object(node, "owned", return_value=(Path('/owned'), (), owner)), \
+ patch.object(node, "stopped"), patch.object(node, "checked", side_effect=lambda path, _: path), \
+ patch.object(node, "digest", return_value="a" * 64), patch.object(node, "fault_module"), \
+ patch.object(node, "bucket_records", backend, create=True), patch.object(backend, "snapshot", return_value={}) as observe:
+ node.bucket_record_operation({"runId": "test", "action": "bucket-record-snapshot"})
+ self.assertEqual(observe.call_args.args[1]["bucketPrefix"], self.bucket)
+
+ @unittest.skipUnless(Path('/proc/self/fdinfo').is_dir(), 'Linux fd/mount guard required')
+ def test_first_system_fault_creates_a_checked_mount_and_lease_marker(self):
+ with tempfile.TemporaryDirectory() as temp:
+ root = Path(temp).resolve()
+ path, _, request = self.scene(root, "missing")
+ owner = {"runId": "test-run", "bucket": self.bucket}
+ with patch.object(node, "owned", return_value=(root, (root,), owner)), patch.object(node, "stopped"), \
+ patch.object(node, "fault_root", return_value=(root, owner)), patch.object(node, "fault_module", return_value=fault), \
+ patch.object(node, "bucket_records", backend, create=True):
+ node.bucket_record_operation({**request, "runId": "test-run", "action": "bucket-record-apply"})
+ self.assertFalse(path.exists())
+ with fault.directory(root) as fd:
+ marker = fault.checked_owner(fd, "test-run")
+ self.assertEqual(marker["bucketPrefix"], self.bucket)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test_validation_heal_caller_abort.py b/test_validation_heal_caller_abort.py
new file mode 100644
index 00000000..aa871f0d
--- /dev/null
+++ b/test_validation_heal_caller_abort.py
@@ -0,0 +1,262 @@
+"""Real TCP evidence for the I05 client subset; no server-lock PASS claim."""
+
+from datetime import datetime, timedelta, timezone
+import hashlib
+import http.server
+import json
+from pathlib import Path
+import threading
+import tempfile
+import unittest
+from unittest.mock import patch
+
+from scripts.validation_heal import caller_abort
+from scripts.validation_heal.heal_probe import Probe
+
+
+VERSION = '00000000-0000-4000-8000-000000000001'
+
+
+class Handler(http.server.BaseHTTPRequestHandler):
+ def log_message(self, *args):
+ pass
+
+ def do_GET(self):
+ self.connection.settimeout(3)
+ self.server.requests.append((self.path, dict(self.headers)))
+ try:
+ self.server.eof.append(self.rfile.read(1))
+ finally:
+ self.server.disconnected.set()
+
+
+class CallerAbortTests(unittest.TestCase):
+ def setUp(self):
+ self.tmp = tempfile.TemporaryDirectory()
+ self.root = Path(self.tmp.name)
+ self.server = http.server.ThreadingHTTPServer(('127.0.0.1', 0), Handler)
+ self.server.requests = []
+ self.server.eof = []
+ self.server.disconnected = threading.Event()
+ self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)
+ self.thread.start()
+ self.binding = {
+ 'schemaVersion': 1, 'runId': 'gh-12345-1-h1',
+ 'leaseExpiresAt': (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat(),
+ 'jumpHostOnly': True, 'exclusiveHealService': True, 'candidateCommit': 'a' * 40,
+ 'harnessCommit': 'b' * 40, 'candidatePackageSha256': 'c' * 64,
+ 'candidateBinarySha256': 'd' * 64, 'rcBinarySha256': 'e' * 64,
+ 'bucketPrefix': 'rv-heal-0123456789abcdef', 'defaultEndpoint': 'node1',
+ 'endpoints': {'node1': f'http://127.0.0.1:{self.server.server_port}'},
+ 'maxInventoryObjects': 512, 'maxInventoryBytes': 512 * 1024**2}
+ self.probe = Probe(self.binding, credential_pair=('private-test-access', 'private-test-secret'))
+
+ def tearDown(self):
+ self.server.shutdown()
+ self.server.server_close()
+ self.thread.join()
+ self.tmp.cleanup()
+
+ def invoke(self, output=None, **changes):
+ return caller_abort.disconnect_get(self.probe, changes.get('bucket', self.binding['bucketPrefix']),
+ changes.get('key', '中文/literal%2F space.bin'), changes.get('version', VERSION),
+ output or self.root / 'call', before_disconnect=changes.get('before_disconnect'),
+ receive_buffer_bytes=changes.get('receive_buffer_bytes'))
+
+ def test_one_signed_exact_version_get_closes_real_socket_and_retains_unknown(self):
+ proof = self.invoke()
+ self.assertTrue(self.server.disconnected.wait(3), 'server must observe this caller EOF')
+ self.assertEqual(self.server.eof, [b''])
+ self.assertEqual(len(self.server.requests), 1)
+ path, headers = self.server.requests[0]
+ self.assertEqual(path, '/rv-heal-0123456789abcdef/%E4%B8%AD%E6%96%87/literal%252F%20space.bin?versionId=' + VERSION)
+ self.assertTrue(headers['Authorization'].startswith('AWS4-HMAC-SHA256 Credential=private-test-access/'))
+ self.assertEqual(headers['Host'], f'127.0.0.1:{self.server.server_port}')
+ self.assertEqual(headers['User-Agent'], proof['callerMarker'])
+ self.assertEqual(path, proof['requestTarget'])
+ from botocore.auth import S3SigV4Auth
+ from botocore.awsrequest import AWSRequest
+ from botocore.credentials import Credentials
+ signed = headers['Authorization'].split('SignedHeaders=')[1].split(',')[0].split(';')
+ checked = AWSRequest(method='GET', url=self.probe.endpoint + path,
+ headers={k: v for k, v in headers.items() if k.lower() in signed})
+ checked.context['timestamp'] = headers['X-Amz-Date']
+ auth = S3SigV4Auth(Credentials('private-test-access', 'private-test-secret'), 's3', 'us-east-1')
+ expected = auth.signature(auth.string_to_sign(checked, auth.canonical_request(checked)), checked)
+ self.assertEqual(headers['Authorization'].split('Signature=')[1], expected)
+ self.assertEqual(proof['connectionAttempts'], 1)
+ self.assertIsNone(proof['receiveBufferBytesRequested'])
+ self.assertIsNone(proof['receiveBufferBytesEffective'])
+ self.assertTrue(proof['requestWriteCompleted'] and proof['clientSocketClosed'] and proof['shutdownCompleted'])
+ self.assertTrue(proof['resultUnknown'])
+ for field in ('responseObserved', 'acknowledged', 'retryAllowed', 'serverOwnershipProven', 'canonicalCasePassClaimed'):
+ self.assertFalse(proof[field], field)
+ intent = self.root / 'call/request.json'
+ self.assertEqual(proof['requestSha256'], hashlib.sha256(intent.read_bytes()).hexdigest())
+ written = json.loads((self.root / 'call/request-written.json').read_text())
+ self.assertTrue(written['requestWriteCompleted'])
+ self.assertLessEqual(written['requestWrittenMonotonicNs'], proof['disconnectStartedMonotonicNs'])
+ self.assertLessEqual(proof['disconnectStartedMonotonicNs'], proof['clientClosedMonotonicNs'])
+ self.assertEqual(json.loads((self.root / 'call/disconnected.json').read_text()), proof)
+ text = ''.join(p.read_text() for p in (self.root / 'call').iterdir())
+ for secret in ('private-test-access', 'private-test-secret', 'Authorization'):
+ self.assertNotIn(secret, text)
+
+ def test_observer_runs_after_durable_write_while_caller_socket_is_open(self):
+ observations = []
+ def observer(sent):
+ self.assertEqual(json.loads((self.root / 'call/request-written.json').read_text()), sent)
+ self.assertFalse(self.server.disconnected.is_set())
+ observations.append(sent['callerMarker'])
+ sent['serverOwnershipProven'] = True
+ proof = self.invoke(before_disconnect=observer)
+ self.assertEqual(observations, [proof['callerMarker']])
+ self.assertFalse(proof['serverOwnershipProven'])
+ self.assertTrue(self.server.disconnected.wait(3))
+
+ def test_small_bounded_receive_buffer_is_persisted_and_applied_to_real_socket(self):
+ proof = self.invoke(receive_buffer_bytes=4096)
+ intent = json.loads((self.root / 'call/request.json').read_text())
+ self.assertEqual(intent['receiveBufferBytesRequested'], 4096)
+ self.assertEqual(proof['receiveBufferBytesRequested'], 4096)
+ self.assertGreaterEqual(proof['receiveBufferBytesEffective'], 4096)
+ self.assertLessEqual(proof['receiveBufferBytesEffective'], 4096 * 4)
+ self.assertTrue(self.server.disconnected.wait(3))
+
+ def test_invalid_receive_buffer_is_rejected_before_output_or_network(self):
+ with self.assertRaises(ValueError):
+ self.invoke(receive_buffer_bytes=1)
+ self.assertFalse((self.root / 'call').exists())
+ self.assertEqual(self.server.requests, [])
+
+ def test_observer_failure_closes_only_the_caller_and_retains_unknown(self):
+ def observer(sent):
+ raise ValueError('no service ownership window')
+ with self.assertRaisesRegex(ValueError, 'no service ownership'):
+ self.invoke(before_disconnect=observer)
+ self.assertTrue(self.server.disconnected.wait(3))
+ proof = json.loads((self.root / 'call/disconnected.json').read_text())
+ self.assertTrue(proof['clientSocketClosed'] and proof['resultUnknown'])
+ self.assertNotIn('disconnectStartedMonotonicNs', proof)
+ self.assertEqual(len(self.server.requests), 1)
+
+ def test_observer_binding_drift_aborts_and_closes_without_another_request(self):
+ def observer(sent):
+ self.probe.binding['candidateCommit'] = 'f' * 40
+ with self.assertRaisesRegex(ValueError, 'binding changed during'):
+ self.invoke(before_disconnect=observer)
+ self.assertTrue(self.server.disconnected.wait(3))
+ self.assertEqual(len(self.server.requests), 1)
+
+ def test_existing_evidence_directory_prevents_replay_without_another_connection(self):
+ self.invoke()
+ self.assertTrue(self.server.disconnected.wait(3))
+ original = (self.root / 'call/disconnected.json').read_bytes()
+ with patch.object(caller_abort.socket, 'create_connection', side_effect=AssertionError('no replay')):
+ with self.assertRaises(FileExistsError):
+ self.invoke()
+ self.assertEqual((self.root / 'call/disconnected.json').read_bytes(), original)
+ self.assertEqual(len(self.server.requests), 1)
+
+ def test_intent_persistence_failure_prevents_any_network_request(self):
+ with patch.object(caller_abort, 'save', side_effect=OSError('fsync failed')), \
+ patch.object(caller_abort.socket, 'create_connection', side_effect=AssertionError('network before intent')):
+ with self.assertRaisesRegex(OSError, 'fsync failed'):
+ self.invoke()
+ self.assertEqual(self.server.requests, [])
+
+ def test_evidence_failure_after_send_still_closes_socket_without_retry(self):
+ original_save = caller_abort.save
+ def fail_written(path, value):
+ if path.name == 'request-written.json':
+ raise OSError('written receipt unavailable')
+ original_save(path, value)
+ with patch.object(caller_abort, 'save', side_effect=fail_written):
+ with self.assertRaisesRegex(OSError, 'written receipt unavailable'):
+ self.invoke()
+ self.assertTrue(self.server.disconnected.wait(3))
+ self.assertEqual(self.server.eof, [b''])
+ proof = json.loads((self.root / 'call/disconnected.json').read_text())
+ self.assertEqual(proof['errorType'], 'OSError')
+ self.assertTrue(proof['resultUnknown'] and proof['clientSocketClosed'])
+ self.assertEqual(proof['connectionAttempts'], 1)
+ self.assertEqual(len(self.server.requests), 1)
+
+ def test_connection_failure_is_recorded_and_cannot_retry(self):
+ with patch.object(caller_abort.socket, 'create_connection', side_effect=ConnectionRefusedError('private detail')) as connect:
+ with self.assertRaises(ConnectionRefusedError):
+ self.invoke()
+ self.assertEqual(connect.call_count, 1)
+ proof = json.loads((self.root / 'call/disconnected.json').read_text())
+ self.assertEqual(proof['errorType'], 'ConnectionRefusedError')
+ self.assertEqual(proof['connectionAttempts'], 1)
+ self.assertFalse(proof['requestInvocationStarted'])
+ self.assertFalse(proof['requestWriteCompleted'])
+ self.assertNotIn('private detail', json.dumps(proof))
+
+ def test_directory_sync_failure_prevents_network(self):
+ with patch.object(caller_abort.os, 'fsync', side_effect=OSError('directory sync failed')), \
+ patch.object(caller_abort.socket, 'create_connection', side_effect=AssertionError('network before durable directory')):
+ with self.assertRaisesRegex(OSError, 'directory sync failed'):
+ self.invoke()
+ self.assertEqual(self.server.requests, [])
+
+ def test_shutdown_failure_still_closes_original_socket_and_retains_unknown(self):
+ original = caller_abort.socket.create_connection
+ sockets = []
+ class FailingShutdown:
+ def __init__(self, sock):
+ self.sock = sock
+ def __getattr__(self, name):
+ return getattr(self.sock, name)
+ def shutdown(self, how):
+ raise OSError('shutdown failed')
+ def connect(*args, **kwargs):
+ sock = FailingShutdown(original(*args, **kwargs))
+ sockets.append(sock)
+ return sock
+ with patch.object(caller_abort.socket, 'create_connection', side_effect=connect):
+ with self.assertRaisesRegex(OSError, 'shutdown failed'):
+ self.invoke()
+ self.assertTrue(self.server.disconnected.wait(3))
+ self.assertEqual(self.server.eof, [b''])
+ self.assertEqual(sockets[0].fileno(), -1)
+ proof = json.loads((self.root / 'call/disconnected.json').read_text())
+ self.assertTrue(proof['requestWriteCompleted'] and proof['clientSocketClosed'] and proof['resultUnknown'])
+ self.assertEqual(proof['errorType'], 'OSError')
+ self.assertNotIn('shutdownCompleted', proof)
+ self.assertEqual(proof['connectionAttempts'], 1)
+
+ def test_changed_endpoint_or_binding_is_rejected_before_request(self):
+ self.binding['endpoints']['node1'] = 'http://127.0.0.1:9400'
+ with patch.object(caller_abort.socket, 'create_connection', side_effect=AssertionError('endpoint drift')):
+ with self.assertRaisesRegex(ValueError, 'endpoint differs'):
+ self.invoke()
+ self.binding['endpoints']['node1'] = self.probe.endpoint
+ original_save = caller_abort.save
+ def drift_after_intent(path, value):
+ original_save(path, value)
+ if path.name == 'request.json':
+ self.binding['candidateBinarySha256'] = 'f' * 64
+ with patch.object(caller_abort, 'save', side_effect=drift_after_intent), \
+ patch.object(caller_abort.socket, 'create_connection', side_effect=AssertionError('binding drift')):
+ with self.assertRaisesRegex(ValueError, 'binding changed'):
+ self.invoke()
+ proof = json.loads((self.root / 'call/disconnected.json').read_text())
+ self.assertEqual(proof['connectionAttempts'], 0)
+ self.assertFalse(proof['requestInvocationStarted'])
+
+ def test_foreign_bucket_null_or_missing_version_and_expired_lease_never_connect(self):
+ with patch.object(caller_abort.socket, 'create_connection', side_effect=AssertionError('invalid request connected')):
+ for changes in ({'bucket': 'foreign-bucket'}, {'version': 'null'}, {'version': ''},
+ {'version': '00000000-0000-0000-0000-000000000000'}, {'key': ''}):
+ with self.subTest(changes=changes), self.assertRaises(ValueError):
+ self.invoke(**changes)
+ self.binding['leaseExpiresAt'] = (datetime.now(timezone.utc) - timedelta(seconds=1)).isoformat()
+ with self.assertRaisesRegex(ValueError, 'lease expired'):
+ self.invoke()
+ self.assertEqual(self.server.requests, [])
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/test_validation_heal_capacity.py b/test_validation_heal_capacity.py
new file mode 100644
index 00000000..84f0471e
--- /dev/null
+++ b/test_validation_heal_capacity.py
@@ -0,0 +1,453 @@
+import copy
+import errno
+import json
+import os
+from pathlib import Path
+from types import SimpleNamespace
+from io import BytesIO
+import subprocess
+import tempfile
+import unittest
+from unittest.mock import patch
+
+from scripts.validation_heal import bounded_volume as volume, capacity, fleet_node
+from scripts.validation_heal.matrix import AUTOMATIC_CASES, select
+from scripts.validation_heal.heal_probe import Probe
+from test_validation_heal_authorization import binding
+
+
+ROOT = Path(__file__).parent
+
+
+class CapacityContractTests(unittest.TestCase):
+ def test_all_catalogued_automatic_cases_reach_the_top_level_dispatcher(self):
+ for case in sorted(AUTOMATIC_CASES):
+ with self.subTest(case=case):
+ observed = subprocess.run(["bash", str(ROOT / "scripts/run_validation_case.sh"),
+ case, "/nonexistent/candidate.json", "a" * 40, "/nonexistent/evidence"],
+ capture_output=True, text=True, timeout=5)
+ self.assertEqual(observed.returncode, 2)
+ self.assertIn("candidate manifest missing", observed.stderr)
+ self.assertNotIn("executor is not migrated", observed.stderr)
+ self.assertIn(capacity.CASE_ID, AUTOMATIC_CASES)
+ self.assertEqual(select(capacity.CASE_ID)[0]["variants"], list(capacity.BRANCHES))
+
+ def test_inner_dispatch_uses_the_actual_capacity_executor(self):
+ observed = subprocess.run(["bash", str(ROOT / "scripts/validation_heal/run_case.sh"),
+ "--validation-case", capacity.CASE_ID, "--dry-run"], capture_output=True, text=True, timeout=5)
+ self.assertEqual(observed.returncode, 0, observed.stderr)
+ source = (ROOT / "scripts/validation_heal/run_case.sh").read_text()
+ self.assertIn("heal.enospc-inodes-and-quota) driver=capacity.py", source)
+
+ def test_capacity_refusals_do_not_accept_unrelated_denials_or_success(self):
+ observed = {"acknowledged": False, "resultUnknown": False, "clientCallInvoked": True,
+ "response": {"ResponseMetadata": {"HTTPStatusCode": 400}, "Error": {
+ "Code": "InvalidRequest", "Message": "Bucket quota exceeded. Current usage: 1 bytes, limit: 2 bytes"}}}
+ self.assertEqual(capacity.refusal(observed, quota=True)["state"], "REFUSED")
+ for change in ("success", "not-invoked", "auth", "generic-invalid", "wrong-http"):
+ broken = copy.deepcopy(observed)
+ if change == "success": broken["acknowledged"] = True
+ elif change == "not-invoked": broken["clientCallInvoked"] = False
+ elif change == "auth": broken["response"]["Error"]["Code"] = "AccessDenied"
+ elif change == "generic-invalid": broken["response"]["Error"]["Message"] = "bad query"
+ else: broken["response"]["ResponseMetadata"]["HTTPStatusCode"] = 503
+ with self.subTest(change=change), self.assertRaises(ValueError):
+ capacity.refusal(broken, quota=True)
+ unknown = {"acknowledged": False, "resultUnknown": True, "clientCallInvoked": True}
+ self.assertFalse(capacity.refusal(unknown, quota=False)["retryAllowed"])
+ storage = {**observed, "response": {"ResponseMetadata": {"HTTPStatusCode": 503},
+ "Error": {"Code": "ServiceUnavailable"}}}
+ self.assertEqual(capacity.refusal(storage, quota=False)["state"], "REFUSED")
+ with self.assertRaises(ValueError): capacity.refusal(observed, quota=False)
+
+ def test_exact_candidate_probe_distinguishes_blocks_inodes_and_release(self):
+ observed = {"uid": 0, "gid": 0, "device": 8, "writeErrno": errno.ENOSPC,
+ "freeBlocks": 0, "availableBlocks": 0, "freeInodes": 91}
+ volume.validate_probe(observed, 8, capacity.BRANCHES[0], True)
+ with self.assertRaises(ValueError): volume.validate_probe(observed, 8, capacity.BRANCHES[1], True)
+ inode = {**observed, "freeInodes": 0, "freeBlocks": 11, "availableBlocks": 11}
+ volume.validate_probe(inode, 8, capacity.BRANCHES[1], True)
+ released = {**inode, "freeInodes": 30, "writeErrno": 0}
+ volume.validate_probe(released, 8, capacity.BRANCHES[1], False)
+ for name, value in (("uid", 1000), ("device", 9), ("writeErrno", errno.EACCES),
+ ("freeInodes", True), ("writeErrno", 0)):
+ with self.subTest(name=name, value=value), self.assertRaises(ValueError):
+ volume.validate_probe({**inode, name: value}, 8, capacity.BRANCHES[1], True)
+ with self.assertRaises(ValueError): volume.validate_probe(inode, 8, capacity.BRANCHES[1], False)
+
+ def test_fill_is_bounded_and_requires_real_enospc(self):
+ stats = SimpleNamespace(f_bfree=0, f_ffree=900)
+ with tempfile.TemporaryDirectory() as directory:
+ folder = Path(directory)
+ (folder / "padding").mkdir()
+ with patch.object(volume.os, "open", return_value=7), patch.object(volume.os, "close"), \
+ patch.object(volume.os, "write", side_effect=[1024**2, OSError(errno.ENOSPC, "full")]), \
+ patch.object(volume.os, "fsync"), patch.object(volume.os, "statvfs", return_value=stats):
+ result = volume.fill(folder, capacity.BRANCHES[0])
+ self.assertEqual(result["allocatedFillerBytes"], 1024**2)
+ self.assertEqual(result["actualErrno"], errno.ENOSPC)
+ with patch.object(volume.os, "open", side_effect=OSError(errno.ENOSPC, "inodes")), \
+ patch.object(volume.os, "statvfs", return_value=SimpleNamespace(f_bfree=500, f_ffree=0)):
+ self.assertEqual(volume.fill(folder, capacity.BRANCHES[1])["actualErrno"], errno.ENOSPC)
+ with patch.object(volume.os, "open", side_effect=OSError(errno.EIO, "unrelated")), self.assertRaises(ValueError):
+ volume.fill(folder, capacity.BRANCHES[1])
+
+ def test_recovery_rejects_unrecognized_filler_without_deleting_it(self):
+ with tempfile.TemporaryDirectory() as directory:
+ folder = Path(directory)
+ (folder / "padding").mkdir()
+ unexpected = folder / "padding" / "acknowledged-object"
+ unexpected.write_bytes(b"preserve")
+ with self.assertRaises(ValueError): volume.release_filler(folder)
+ self.assertEqual(unexpected.read_bytes(), b"preserve")
+
+ def test_block_fill_consumes_small_tail_after_large_write_enospc(self):
+ tail = SimpleNamespace(f_bfree=1, f_ffree=900, f_frsize=4096)
+ full = SimpleNamespace(f_bfree=0, f_ffree=900, f_frsize=4096)
+ with tempfile.TemporaryDirectory() as directory:
+ folder = Path(directory); (folder / 'padding').mkdir()
+ with patch.object(volume.os, 'open', return_value=7), patch.object(volume.os, 'close'), \
+ patch.object(volume.os, 'write', side_effect=[1024**2, OSError(errno.ENOSPC, 'large allocation'),
+ 4096, OSError(errno.ENOSPC, 'full')]) as write, patch.object(volume.os, 'fsync'), \
+ patch.object(volume.os, 'statvfs', side_effect=[tail, full, full]):
+ result = volume.fill(folder, capacity.BRANCHES[0])
+ self.assertEqual(result['allocatedFillerBytes'], 1024**2 + 4096)
+ self.assertEqual(result['freeBlocks'], 0)
+ self.assertEqual(result['actualErrno'], errno.ENOSPC)
+ self.assertEqual(len(write.call_args_list[2].args[1]), 4096)
+
+ def test_same_file_tail_refusal_uses_a_separate_owned_file(self):
+ tail = SimpleNamespace(f_bfree=1, f_ffree=900, f_frsize=4096)
+ full = SimpleNamespace(f_bfree=0, f_ffree=900, f_frsize=4096)
+ with tempfile.TemporaryDirectory() as directory:
+ folder = Path(directory); (folder / 'padding').mkdir()
+ with patch.object(volume.os, 'open', side_effect=[7, 8]) as opened, \
+ patch.object(volume.os, 'close') as close, patch.object(volume.os, 'fsync'), \
+ patch.object(volume.os, 'write', side_effect=[1024**2,
+ OSError(errno.ENOSPC, 'large'), OSError(errno.ENOSPC, 'same inode allocation'),
+ 4096, OSError(errno.ENOSPC, 'full')]), \
+ patch.object(volume.os, 'statvfs', side_effect=[tail, tail, full, full]):
+ result = volume.fill(folder, capacity.BRANCHES[0])
+ self.assertEqual(result['freeBlocks'], 0)
+ self.assertEqual(result['allocatedFillerBytes'], 1024**2 + 4096)
+ self.assertEqual(result['allocatedFillerInodes'], 1)
+ self.assertEqual(opened.call_args_list[1].args, (folder / 'padding' / 'inode-0000',
+ os.O_CREAT | os.O_EXCL | os.O_WRONLY | os.O_NOFOLLOW, 0o600))
+ self.assertEqual([call.args[0] for call in close.call_args_list], [7, 8])
+
+ def test_separate_tail_file_cannot_loop_without_progress_or_claim_remaining_blocks(self):
+ tail = SimpleNamespace(f_bfree=1, f_ffree=900, f_frsize=4096)
+ with tempfile.TemporaryDirectory() as directory:
+ folder = Path(directory); (folder / 'padding').mkdir()
+ with patch.object(volume.os, 'open', side_effect=[7, 8]) as opened, \
+ patch.object(volume.os, 'close') as close, patch.object(volume.os, 'fsync'), \
+ patch.object(volume.os, 'write', side_effect=[1024**2] +
+ [OSError(errno.ENOSPC, 'refused')] * 3), \
+ patch.object(volume.os, 'statvfs', return_value=tail), \
+ self.assertRaisesRegex(ValueError, 'minimum-block fill left 1 free blocks'):
+ volume.fill(folder, capacity.BRANCHES[0])
+ self.assertEqual(opened.call_count, 2)
+ self.assertEqual([call.args[0] for call in close.call_args_list], [7, 8])
+
+ def test_block_tail_never_exceeds_the_128_total_padding_inode_budget(self):
+ tail = SimpleNamespace(f_bfree=1, f_ffree=900, f_frsize=4096)
+ with tempfile.TemporaryDirectory() as directory:
+ folder = Path(directory); (folder / 'padding').mkdir()
+ refused = OSError(errno.ENOSPC, 'same inode allocation')
+ with patch.object(volume.os, 'open', side_effect=range(7, 136)) as opened, \
+ patch.object(volume.os, 'close') as close, patch.object(volume.os, 'fsync'), \
+ patch.object(volume.os, 'write', side_effect=[1024**2, refused, refused] +
+ [v for _ in range(127) for v in (4096, refused)]), \
+ patch.object(volume.os, 'statvfs', return_value=tail), \
+ self.assertRaisesRegex(ValueError, 'minimum-block fill left 1 free blocks'):
+ volume.fill(folder, capacity.BRANCHES[0])
+ self.assertEqual(opened.call_count, 128)
+ self.assertEqual(close.call_count, 128)
+ self.assertEqual(opened.call_args.args[0].name, 'inode-0126')
+
+ def test_minimum_block_refusal_with_free_blocks_never_claims_full_exhaustion(self):
+ tail = SimpleNamespace(f_bfree=1, f_ffree=900, f_frsize=4096)
+ with tempfile.TemporaryDirectory() as directory:
+ folder = Path(directory); (folder / 'padding').mkdir()
+ with patch.object(volume.os, 'open', return_value=7), patch.object(volume.os, 'close'), \
+ patch.object(volume.os, 'write', side_effect=OSError(errno.ENOSPC, 'refused')) as write, \
+ patch.object(volume.os, 'statvfs', return_value=tail), \
+ self.assertRaisesRegex(ValueError, 'minimum-block fill left 1 free blocks'):
+ volume.fill(folder, capacity.BRANCHES[0])
+ self.assertEqual(write.call_count, 2)
+
+ def test_loop_detach_binding_requires_the_exact_backing_inode_and_device(self):
+ row = {"name": "/dev/loop7", "back-file": "/owned/image", "back-ino": 31,
+ "back-maj:min": f"{os.major(2049)}:{os.minor(2049)}"}
+ with patch.object(volume, "command", return_value=json.dumps({"loopdevices": [row]})), \
+ patch.object(volume, "regular", return_value=SimpleNamespace(st_ino=31, st_dev=2049)):
+ self.assertEqual(volume.loop_for(Path("/owned/image")), "/dev/loop7")
+ for change in ("inode", "device", "duplicate", "nonloop"):
+ rows = [dict(row)]
+ if change == "inode": rows[0]["back-ino"] = 99
+ elif change == "device": rows[0]["back-maj:min"] = "999:999"
+ elif change == "duplicate": rows.append(dict(row))
+ else: rows[0]["name"] = "/dev/sda"
+ with self.subTest(change=change), patch.object(volume, "command", return_value=json.dumps({"loopdevices": rows})), \
+ self.assertRaises(ValueError):
+ volume.loop_for(Path("/owned/image"))
+
+ def test_only_the_dedicated_runtime_can_allocate_bounded_volumes(self):
+ with tempfile.TemporaryDirectory() as directory:
+ state = Path(directory)
+ with self.assertRaises(ValueError):
+ volume.create(state, (), {"runtimeProfile": "manual", "runId": "gh-1-1-h1"}, capacity.BRANCHES[0])
+ self.assertFalse((state / volume.MARKER).exists())
+ config = fleet_node.runtime_config("bounded-capacity")
+ self.assertEqual(config["RUSTFS_UNSAFE_BYPASS_DISK_CHECK"], "true")
+ self.assertEqual(config["RUSTFS_HEAL_MRF_ENABLE"], "false")
+ self.assertEqual(config["RUSTFS_HEAL_AUTO_HEAL_ENABLE"], "false")
+ self.assertEqual(config["RUSTFS_SCANNER_ENABLED"], "false")
+
+ def test_bounded_journal_profile_requires_fixed_block_exhaustion_and_disables_background_sources(self):
+ with tempfile.TemporaryDirectory() as directory:
+ state = Path(directory)
+ owner = {"runtimeProfile": "journal-bounded", "runId": "gh-1-1-h1"}
+ with self.assertRaisesRegex(ValueError, "only fixed block"):
+ volume.create(state, (), owner, "bounded-inode-exhaustion")
+ self.assertFalse((state / volume.MARKER).exists())
+ with self.assertRaises(ValueError): volume.views(state, (), owner)
+ with self.assertRaises(ValueError): volume.properties(state, (), owner)
+ config = fleet_node.runtime_config("journal-bounded")
+ self.assertEqual(config["RUSTFS_UNSAFE_BYPASS_DISK_CHECK"], "true")
+ self.assertEqual(config["RUSTFS_HEAL_PAGE_PARALLEL_ENABLE"], "false")
+ for field in ("RUSTFS_HEAL_MRF_ENABLE", "RUSTFS_HEAL_AUTO_HEAL_ENABLE", "RUSTFS_SCANNER_ENABLED"):
+ self.assertEqual(config[field], "false")
+
+ def test_bounded_journal_start_uses_only_verified_private_service_bindings(self):
+ state, roots = Path('/owned/state'), [Path('/owned/disk' + str(i)) for i in range(4)]
+ owner = {"runtimeProfile": "journal-bounded", "unit": "owned-test.service", "binarySha256": "a" * 64,
+ "volumes": [str(root / 'data') for root in roots]}
+ bindings = ['--property=PrivateMounts=true', '--property=BindPaths=/verified/private/data:/owned/disk0/data']
+ def execute(*args, **kwargs):
+ if args[:2] == ('systemctl', 'show'):
+ return SimpleNamespace(stdout='LogRateLimitIntervalUSec=1s\nLogRateLimitBurst=1000000\n')
+ return SimpleNamespace(stdout='systemd 249 (249.11)\n')
+ with patch.object(fleet_node, "owned", return_value=(state, roots, owner)), patch.object(fleet_node, "stopped"), \
+ patch.object(fleet_node, "digest", return_value=owner["binarySha256"]), \
+ patch.object(fleet_node, "command", side_effect=execute) as command, \
+ patch.object(fleet_node, "gate_module", return_value=SimpleNamespace(checked_gate=lambda *args: None, start_properties=lambda *args: [])), \
+ patch.object(fleet_node, "quorum_gate_module", return_value=SimpleNamespace(properties=lambda *args: [])), \
+ patch.object(fleet_node, "replacement_module", return_value=SimpleNamespace(start_properties=lambda *args: [])), \
+ patch.object(fleet_node, "volume_module", return_value=SimpleNamespace(properties=lambda *args: bindings)):
+ started = fleet_node.start({"runId": "gh-1-1-h1"})
+ issued = next(call.args for call in command.call_args_list if call.args[0] == 'systemd-run')
+ self.assertEqual(issued[0], 'systemd-run')
+ self.assertIn('--property=LogRateLimitIntervalSec=1s', issued)
+ self.assertIn('--property=LogRateLimitBurst=1000000', issued)
+ self.assertEqual(started['journalPolicy']['rateBurst'], 1000000)
+ self.assertEqual(started['journalPolicy']['systemdVersion'], 'systemd 249 (249.11)')
+ self.assertFalse(started['journalPolicy']['hostWideSettingsChanged'])
+ for property in bindings: self.assertIn(property, issued)
+ self.assertIn('--property=RemainAfterExit=yes', issued)
+ self.assertEqual(issued[-4:], tuple(owner["volumes"]))
+
+ def test_owned_journal_policy_rejects_zero_ignored_missing_or_different_effective_values(self):
+ for output in ('', 'LogRateLimitIntervalUSec=0\nLogRateLimitBurst=0\n',
+ 'LogRateLimitIntervalUSec=30s\nLogRateLimitBurst=10000\n',
+ 'LogRateLimitIntervalUSec=1s\nLogRateLimitBurst=999999\n',
+ 'LogRateLimitBurst=1000000\n'):
+ with self.subTest(output=output), patch.object(fleet_node, 'command',
+ return_value=SimpleNamespace(stdout=output)) as command:
+ with self.assertRaisesRegex(ValueError, 'journal rate policy differs'):
+ fleet_node.journal_policy({'unit': 'owned.service'})
+ self.assertEqual(command.call_count, 1)
+ self.assertEqual(command.call_args.args[:3], ('systemctl', 'show', 'owned.service'))
+ for interval in ('1s', '1000000'):
+ with self.subTest(interval=interval), patch.object(fleet_node, 'command', side_effect=[
+ SimpleNamespace(stdout=f'LogRateLimitIntervalUSec={interval}\nLogRateLimitBurst=1000000\n'),
+ SimpleNamespace(stdout='systemd 249 (249.11)\n')]):
+ self.assertEqual(fleet_node.journal_policy({'unit': 'owned.service'})['rateIntervalSeconds'], 1)
+ for version in ('', 'unknown daemon\n'):
+ with self.subTest(version=version), patch.object(fleet_node, 'command', side_effect=[
+ SimpleNamespace(stdout='LogRateLimitIntervalUSec=1s\nLogRateLimitBurst=1000000\n'),
+ SimpleNamespace(stdout=version)]):
+ with self.assertRaisesRegex(ValueError, 'systemd version is unavailable'):
+ fleet_node.journal_policy({'unit': 'owned.service'})
+
+ def test_bounded_journal_capacity_refuses_arbitrary_limits_paths_or_process_birth(self):
+ owner = {"runtimeProfile": "journal-bounded"}
+ identity = {field: field for field in fleet_node.PROCESS_FIELDS}; identity['pid'] = 50
+ request = {"runId": "gh-1-1-h1", "action": "bounded-volumes-fill", "expected": identity}
+ backend = SimpleNamespace(views=lambda *args: (), read=lambda *args: {'branch': 'bounded-volume-enospc'},
+ fill=lambda *args: {}, witness=lambda *args, **kwargs: {})
+ with patch.object(fleet_node, 'owned', return_value=(None, (), owner)), \
+ patch.object(fleet_node, 'volume_module', return_value=backend):
+ with patch.object(fleet_node, 'process', side_effect=[identity, identity]):
+ self.assertTrue(fleet_node.volume_operation(request)['sameOriginalProcess'])
+ for extra in ({'path': '/foreign'}, {'imageBytes': 10**12}, {'branch': 'bounded-inode-exhaustion'}):
+ with self.subTest(extra=extra), self.assertRaises(ValueError): fleet_node.volume_operation({**request, **extra})
+ with patch.object(fleet_node, 'process', return_value={**identity, 'startTicks': 999}), self.assertRaises(ValueError):
+ fleet_node.volume_operation(request)
+
+ def test_stopped_fill_is_restricted_to_fixed_journal_volumes_and_stopped_candidate(self):
+ owner = {"runtimeProfile": "journal-bounded"}
+ events = []
+ backend = SimpleNamespace(read=lambda *args: {"branch": "bounded-volume-enospc"},
+ views=lambda *args: tuple(range(4)), fill=lambda view, branch: events.append((view, branch)) or {"actualErrno": 28})
+ request = {"runId": "gh-1-1-h1", "action": "bounded-volumes-fill", "quiescent": True}
+ with patch.object(fleet_node, "owned", return_value=(None, (), owner)), \
+ patch.object(fleet_node, "stopped") as stopped, patch.object(fleet_node, "volume_module", return_value=backend):
+ result = fleet_node.volume_operation(request)
+ stopped.assert_called_once_with(owner)
+ self.assertEqual(events, [(index, "bounded-volume-enospc") for index in range(4)])
+ self.assertTrue(result["candidateProcessWitnessRequired"])
+ self.assertFalse(result["productRepairClaimed"])
+ for changes in ({"path": "/shared"}, {"expected": {}}, {"limitBytes": 1}):
+ events.clear()
+ with self.assertRaises(ValueError): fleet_node.volume_operation({**request, **changes})
+ self.assertFalse(events)
+ for profile in ("manual", "bounded-capacity"):
+ owner["runtimeProfile"] = profile; events.clear()
+ with self.assertRaises(ValueError): fleet_node.volume_operation(request)
+ self.assertFalse(events)
+ owner["runtimeProfile"] = "journal-bounded"; events.clear()
+ with patch.object(fleet_node, "stopped", side_effect=ValueError("candidate still running")):
+ with self.assertRaises(ValueError): fleet_node.volume_operation(request)
+ self.assertFalse(events)
+
+ def test_allocation_registers_exact_undo_before_formatting_only_bounded_files(self):
+ with tempfile.TemporaryDirectory() as directory:
+ state = Path(directory) / "state"
+ state.mkdir()
+ roots = tuple(Path(directory) / f"root{index}" for index in range(4))
+ for root in roots:
+ (root / "data").mkdir(parents=True)
+ owner = {"runtimeProfile": "bounded-capacity", "runId": "gh-1-1-h1"}
+ calls = []
+ def checked_command(*args):
+ calls.append(args)
+ self.assertTrue((state / volume.MARKER).exists())
+ intent = json.loads((state / volume.MARKER).read_text())
+ self.assertEqual(len(intent["volumes"]), 4)
+ if args[0] == "mkfs.ext4":
+ self.assertIn(Path(args[-1]).parent, roots)
+ self.assertEqual(Path(args[-1]).name, "bounded-volume.img")
+ self.assertNotIn("/dev/", args[-1])
+ return "/dev/loop7\n" if args[0] == "losetup" else ""
+ with patch.object(volume, "command", side_effect=checked_command), \
+ patch.object(volume, "regular", return_value=SimpleNamespace(st_size=100)), \
+ patch.object(volume, "loop_for", return_value="/dev/loop7"), \
+ patch.object(volume, "mounted", return_value={"verified": True}), \
+ patch.object(volume, "configure_block_exhaustion", return_value={"reservedClustersAfter": 0}) as reserve, \
+ patch.object(volume.os, "posix_fallocate", create=True) as allocate, \
+ patch.object(volume.os, "statvfs", return_value=SimpleNamespace(f_blocks=32768, f_frsize=4096, f_files=1024)), \
+ patch.object(volume.shutil, "disk_usage", return_value=SimpleNamespace(free=10 * 1024**3)):
+ result = volume.create(state, roots, owner, capacity.BRANCHES[0])
+ self.assertEqual(allocate.call_count, 4)
+ self.assertEqual(reserve.call_count, 4)
+ self.assertEqual(len(result["ext4ReservedClusterConfiguration"]), 4)
+ self.assertTrue(all(call.args[1:] == (0, volume.IMAGE_BYTES) for call in allocate.call_args_list))
+ self.assertEqual(sum(args[0] == "mkfs.ext4" for args in calls), 4)
+ self.assertFalse(result["hostSharedMountRemounted"])
+ self.assertFalse(result["independentPhysicalDriveCountIncreased"])
+
+ def test_private_ext4_reserve_configuration_requires_exact_mount_and_readback(self):
+ row = {"disk": 1, "uuid": "owned-uuid"}
+ entry = {"source": "/dev/loop7", "uuid": row["uuid"]}
+ for change in ('valid', 'already-zero', 'unmounted', 'shared-device', 'changed-before',
+ 'changed-after', 'malformed', 'oversize', 'foreign-owner', 'symlink-type',
+ 'short-write', 'wrong-readback'):
+ mounts = [entry, entry, entry]
+ reads = [b'655\n', b'0\n']
+ mode, uid, written = 0o100644, 0, 2
+ if change == 'already-zero': reads[0] = b'0\n'
+ elif change == 'unmounted': mounts[0] = None
+ elif change == 'shared-device': mounts[0] = {**entry, 'source': '/dev/sda1'}
+ elif change == 'changed-before': mounts[1] = {**entry, 'uuid': 'foreign'}
+ elif change == 'changed-after': mounts[2] = None
+ elif change == 'malformed': reads[0] = b'655x\n'
+ elif change == 'oversize': reads[0] = b'9999999999\n'
+ elif change == 'foreign-owner': uid = 1000
+ elif change == 'symlink-type': mode = 0o120777
+ elif change == 'short-write': written = 1
+ elif change == 'wrong-readback': reads[1] = b'655\n'
+ with self.subTest(change=change), patch.object(volume, 'mounted', side_effect=mounts), \
+ patch.object(volume.os, 'open', return_value=7) as opened, \
+ patch.object(volume.os, 'fstat', return_value=SimpleNamespace(st_mode=mode, st_uid=uid)), \
+ patch.object(volume.os, 'read', side_effect=reads), patch.object(volume.os, 'lseek'), \
+ patch.object(volume.os, 'write', return_value=written) as write, \
+ patch.object(volume.os, 'close') as close:
+ if change in {'valid', 'already-zero'}:
+ result = volume.configure_block_exhaustion(row)
+ self.assertEqual(result['reservedClustersBefore'], int(reads[0]))
+ self.assertEqual(result['reservedClustersAfter'], 0)
+ self.assertFalse(result['sharedFilesystemModified'])
+ self.assertEqual(opened.call_args.args, (Path('/sys/fs/ext4/loop7/reserved_clusters'),
+ os.O_RDWR | os.O_NOFOLLOW))
+ write.assert_called_once_with(7, b'0\n')
+ else:
+ with self.assertRaises(ValueError): volume.configure_block_exhaustion(row)
+ if change not in {'changed-after', 'short-write', 'wrong-readback'}:
+ write.assert_not_called()
+ if opened.called: close.assert_called_once_with(7)
+ else: close.assert_not_called()
+
+ def test_registered_quota_probe_signs_and_preserves_the_exact_request(self):
+ class Response(BytesIO):
+ code = 200
+ def __init__(self):
+ super().__init__(b"{}")
+ self.headers = {"Content-Length": "2"}
+ probe = Probe(binding())
+ path = "/rustfs/admin/v3/quota/" + probe.binding["bucketPrefix"]
+ with patch.object(probe, "credentials", return_value=("access", "secret")), \
+ patch.object(probe, "_open_admin", side_effect=lambda outgoing: Response()) as outgoing:
+ for limit in (1024**2, 256 * 1024**2):
+ body = json.dumps({"quota": limit, "quota_type": "HARD"}).encode()
+ observed = probe.admin(path, [], body, method="PUT")
+ self.assertEqual(observed["requestBody"].encode(), body)
+ self.assertEqual(outgoing.call_args.args[0].data, body)
+ self.assertIn("Authorization", outgoing.call_args.args[0].headers)
+ self.assertEqual(probe.admin(path, [], b"", method="GET")["httpStatus"], 200)
+
+ def test_quota_config_probe_signs_the_exact_owned_bucket_query(self):
+ probe = Probe(binding())
+ path = "/rustfs/admin/v3/get-bucket-quota"
+ class Response(BytesIO):
+ code = 200
+ def __init__(self):
+ super().__init__(b'{"quota":0,"size":0,"rate":0,"requests":0}')
+ self.headers = {"Content-Length": str(self.getbuffer().nbytes)}
+ with patch.object(probe, "credentials", return_value=("access", "secret")), \
+ patch.object(probe, "_open_admin", return_value=Response()) as outgoing:
+ observed = probe.admin(path, [("bucket", probe.binding['bucketPrefix'])], b"", method="GET")
+ self.assertEqual(observed["httpStatus"], 200)
+ self.assertEqual(outgoing.call_args.args[0].get_method(), "GET")
+ self.assertEqual(outgoing.call_args.args[0].full_url,
+ probe.endpoint + path + '?bucket=' + probe.binding['bucketPrefix'])
+ self.assertIn('Authorization', outgoing.call_args.args[0].headers)
+ for query, method, body, auth in (([], 'GET', b'', 'signed'),
+ ([('bucket', 'foreign-bucket')], 'GET', b'', 'signed'),
+ ([('bucket', probe.binding['bucketPrefix'])] * 2, 'GET', b'', 'signed'),
+ ([('bucket', probe.binding['bucketPrefix'])], 'PUT', b'', 'signed'),
+ ([('bucket', probe.binding['bucketPrefix'])], 'GET', b'{}', 'signed'),
+ ([('bucket', probe.binding['bucketPrefix'])], 'GET', b'', 'anonymous')):
+ with self.subTest(query=query, method=method, body=body, auth=auth), \
+ patch.object(probe, '_open_admin') as outgoing, self.assertRaises(ValueError):
+ probe.admin(path, query, body, method=method, auth=auth)
+ outgoing.assert_not_called()
+
+ def test_quota_probe_refuses_foreign_buckets_unregistered_limits_and_auth(self):
+ probe = Probe(binding())
+ path = "/rustfs/admin/v3/quota/" + probe.binding["bucketPrefix"]
+ for route, method, body, auth in (
+ ("/rustfs/admin/v3/quota/foreign-bucket", "PUT", b'{"quota":1048576,"quota_type":"HARD"}', "signed"),
+ (path, "DELETE", b"", "signed"),
+ (path, "PUT", b'{"quota":123,"quota_type":"HARD"}', "signed"),
+ (path, "PUT", b'{"quota":true,"quota_type":"HARD"}', "signed"),
+ (path, "GET", b"mutation", "signed"),
+ (path, "GET", b"", "anonymous")):
+ with self.subTest(route=route, method=method, body=body, auth=auth), \
+ patch.object(probe, "_open_admin") as outgoing, self.assertRaises(ValueError):
+ probe.admin(route, [], body, method=method, auth=auth)
+ outgoing.assert_not_called()
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test_validation_heal_checkpoint.py b/test_validation_heal_checkpoint.py
new file mode 100644
index 00000000..0f8d8405
--- /dev/null
+++ b/test_validation_heal_checkpoint.py
@@ -0,0 +1,431 @@
+"""An actual set checkpoint is distinct from a bucket/root scan and counters."""
+
+import base64
+import copy
+import hashlib
+import json
+import unittest
+from types import SimpleNamespace
+from unittest.mock import patch
+
+from scripts.validation_heal import checkpoint as reader, journal_fault as journal
+
+
+TOKEN = "12345678-1234-4234-8234-123456789abc"
+INCARNATION = "22345678-1234-4234-8234-123456789abc"
+BUCKET = "rv-heal-0123456789abcdef"
+
+
+def encoded(value, *, signed=False):
+ value = copy.deepcopy(value)
+ if signed:
+ value["integrity_digest"] = None
+ for field in ("processed_objects", "failed_objects", "skipped_objects"): value[field].sort()
+ unsigned = json.dumps(value, sort_keys=True, ensure_ascii=False, separators=(",", ":")).encode()
+ value["integrity_digest"] = base64.b64encode(hashlib.sha256(unsigned).digest()).decode()
+ raw = json.dumps(value).encode()
+ return {"present": True, **journal.entry(raw), "rawBase64": base64.b64encode(raw).decode()}
+
+
+class AdminCheckpointTests(unittest.TestCase):
+ def fixture(self):
+ value = {"schema_version": 8, "task_id": TOKEN, "checkpoint_time": 100,
+ "current_bucket_index": 0, "current_object_index": 2,
+ "processed_objects": ["objects/first", "objects/second"], "failed_objects": [], "skipped_objects": [],
+ "successful_objects": 2, "failed_object_count": 0, "skipped_object_count": 0,
+ "skipped_new_versions": 0, "skipped_ilm_expired": 0, "processed_bytes": 2,
+ "total_objects": 3, "total_bytes": 3, "baseline_generation": None, "baseline_known": False,
+ "counter_unknown": False, "integrity_digest": None,
+ "admin": {"set_disk_id": "pool_0_set_0", "options": reader.OPTIONS, "buckets": {BUCKET: INCARNATION},
+ "completed": False, "outcome": {"execution": {"state": "running"}, "coverage": "partial",
+ "objects": [], "objectsTruncated": False, "counters": {"processed": 2, "healed": 1, "unchanged": 1,
+ "skipped": 0, "failed": 0, "unknown": 0, "attemptFailures": 0, "overflowed": False}}}}
+ resume = {"schema_version": 7, "task_id": TOKEN, "task_type": "admin_erasure_set",
+ "set_disk_id": "pool_0_set_0", "completed": False, "replacement_targets": [], "replacement_generation": None}
+ rows = [{"disk": disk, "relativePath": name, "present": False}
+ for disk in range(4) for name in journal.names("checkpoint", TOKEN)]
+ rows[4].update(encoded(value, signed=True)); rows[5].update(encoded(resume))
+ return {"node": {"readOnly": True, "quiescent": True, "family": "checkpoint", "taskId": TOKEN,
+ "directoryEnumerationUsed": False, "records": rows}}, value, resume
+
+ def test_exact_original_checkpoint_resume_pair_has_one_physical_owner(self):
+ observed, _, _ = self.fixture()
+ owner = reader.owned_checkpoint(observed, TOKEN, BUCKET)
+ self.assertEqual((owner["node"], owner["disk"], owner["bucketIncarnationId"]), ("node", 1, INCARNATION))
+ self.assertTrue(owner["originalDurableResponsibility"])
+
+ def test_root_or_bucket_scan_without_checkpoint_cannot_be_used_as_durable_progress(self):
+ observed, _, _ = self.fixture()
+ for row in observed["node"]["records"]: row["present"] = False
+ with self.assertRaisesRegex(ValueError, "NOT_OBSERVED"): reader.owned_checkpoint(observed, TOKEN, BUCKET)
+
+ def test_original_bytes_hash_exact_rows_and_unique_owner_are_required(self):
+ for change in ("bytes", "hash", "duplicate-row", "second-owner", "wrong-disk", "resume-owner", "omitted-disk", "blocked"):
+ observed, _, _ = self.fixture(); rows = observed["node"]["records"]
+ if change == "bytes": rows[4]["rawBase64"] = base64.b64encode(b"changed").decode()
+ elif change == "hash": rows[4]["sha256"] = "b" * 64
+ elif change == "duplicate-row": rows[0] = copy.deepcopy(rows[4])
+ elif change == "second-owner": observed["other"] = copy.deepcopy(observed["node"])
+ elif change == "wrong-disk": rows[4]["disk"] = True
+ elif change == "resume-owner": rows[1].update({key: rows[5][key] for key in ("present", "size", "sha256", "rawBase64")}); rows[5]["present"] = False
+ elif change == "omitted-disk": rows.pop()
+ else: rows[6].update(encoded({"blocked": True}))
+ with self.subTest(change=change), self.assertRaises(ValueError): reader.owned_checkpoint(observed, TOKEN, BUCKET)
+
+ def test_wrong_scope_incarnation_options_and_fabricated_progress_are_rejected(self):
+ changes = [("set_disk_id", "pool_0_set_1"), ("options", {**reader.OPTIONS, "pool": True}),
+ ("options", {**reader.OPTIONS, "nolock": True}), ("buckets", {"foreign": INCARNATION}),
+ ("buckets", {BUCKET: None}), ("completed", True)]
+ for key, new in changes:
+ observed, value, _ = self.fixture(); value["admin"][key] = new
+ observed["node"]["records"][4].update(encoded(value, signed=True))
+ with self.subTest(key=key, new=new), self.assertRaises(ValueError): reader.owned_checkpoint(observed, TOKEN, BUCKET)
+ observed, value, _ = self.fixture()
+ for name in ("processed", "healed", "unchanged"): value["admin"]["outcome"]["counters"][name] = 0
+ observed["node"]["records"][4].update(encoded(value, signed=True))
+ with self.assertRaisesRegex(ValueError, "NOT_OBSERVED"): reader.owned_checkpoint(observed, TOKEN, BUCKET)
+
+ def test_replacement_or_completed_resume_cannot_be_rebound_to_admin_task(self):
+ for key, new in [("task_id", INCARNATION), ("task_type", "erasure_set"), ("schema_version", 6),
+ ("set_disk_id", "pool_1_set_0"), ("completed", True), ("replacement_generation", TOKEN)]:
+ observed, _, resume = self.fixture(); resume[key] = new
+ observed["node"]["records"][5].update(encoded(resume))
+ with self.subTest(key=key), self.assertRaises(ValueError): reader.owned_checkpoint(observed, TOKEN, BUCKET)
+
+ def test_live_census_requires_two_stable_reads_bound_to_original_process(self):
+ observed, _, _ = self.fixture(); value = observed["node"]
+ value.update(quiescent=False, stable=True, processBound=True, independentReads=2)
+ reader.owned_checkpoint(observed, TOKEN, BUCKET)
+ for key, new in [("stable", False), ("processBound", False), ("independentReads", 1)]:
+ changed = copy.deepcopy(observed); changed["node"][key] = new
+ with self.subTest(key=key), self.assertRaises(ValueError): reader.owned_checkpoint(changed, TOKEN, BUCKET)
+
+ def test_set_admission_keeps_explicit_selectors_and_actual_canonical_token(self):
+ active = []; calls = []
+ fleet = SimpleNamespace(record=lambda label, value: value)
+ probe = SimpleNamespace(admin=lambda path, query, body: calls.append((path, query, json.loads(body)))
+ or {"httpStatus": 200, "response": {"clientToken": TOKEN}})
+ self.assertEqual(reader.admit(probe, fleet, active), TOKEN)
+ self.assertEqual(calls, [(reader.PATH, [], reader.OPTIONS)])
+ self.assertEqual(active, [(reader.PATH, TOKEN)])
+
+ def test_earlier_capture_requires_original_bytes_bound_to_process_and_physical_owner(self):
+ observed, _, _ = self.fixture()
+ owner = reader.owned_checkpoint(observed, TOKEN, BUCKET)
+ from scripts.validation_heal.fleet_node import PROCESS_FIELDS
+ identity = {name: name for name in PROCESS_FIELDS}
+ identity.update(pid=50, state="S", binarySha256="a" * 64)
+ receipt = {"processBound": True, "originalProductBytes": True, "productMetadataMutated": False,
+ "runId": "gh-12345-1-h1", "family": "checkpoint", "taskId": TOKEN, "disk": 1,
+ "relativePath": owner["checkpoint"]["relativePath"], "faultId": "F07-early", "binarySha256": "a" * 64,
+ "captured": journal.entry(reader.bytes_of(owner["checkpoint"]))}
+ requests = []
+ fleet = SimpleNamespace(run_id=receipt["runId"], frozen_processes={"node": identity}, record=lambda label, value: value,
+ remote=lambda node, action, **params: requests.append((node, action, params)) or
+ (identity if action == "process" else receipt))
+ with patch.object(reader, "freeze", return_value=(owner, identity)):
+ result = reader.capture_earlier(fleet, TOKEN, BUCKET, "F07-early")
+ self.assertEqual(result["captureId"], "F07-early")
+ self.assertEqual(requests[0], ("node", "journal-capture", {
+ "family": "checkpoint", "taskId": TOKEN, "disk": 1, "relativePath": receipt["relativePath"],
+ "faultId": "F07-early", "expectedBefore": receipt["captured"], "expected": identity}))
+ self.assertEqual([request[1] for request in requests], ['journal-capture', 'process-signal', 'process'])
+ self.assertEqual(fleet.frozen_processes, {})
+ for key, value in [("processBound", False), ("productMetadataMutated", True), ("disk", True),
+ ("binarySha256", "b" * 64), ("captured", {"size": 3, "sha256": "a" * 64})]:
+ fleet.frozen_processes["node"] = identity
+ original = receipt[key]; receipt[key] = value
+ with self.subTest(key=key), self.assertRaises(ValueError):
+ reader.capture_earlier(fleet, TOKEN, BUCKET, "F07-early")
+ self.assertEqual(fleet.frozen_processes, {"node": identity})
+ receipt[key] = original
+
+ def test_atomic_earlier_capture_keeps_the_registered_birth_frozen_without_an_ssh_resume_gap(self):
+ observed, _, _ = self.fixture(); owner = reader.owned_checkpoint(observed, TOKEN, BUCKET)
+ from scripts.validation_heal.fleet_node import PROCESS_FIELDS
+ identity = {name: name for name in PROCESS_FIELDS}; identity.update(pid=50)
+ receipt = {'processBound': True, 'originalProductBytes': True, 'productMetadataMutated': False,
+ 'runId': 'gh-12345-1-h1', 'family': 'checkpoint', 'taskId': TOKEN, 'disk': owner['disk'],
+ 'relativePath': owner['checkpoint']['relativePath'], 'faultId': 'F07-early',
+ 'binarySha256': identity['binarySha256'], 'captured': journal.entry(reader.bytes_of(owner['checkpoint'])),
+ 'checkpointAdvancedAndFrozen': True, 'pidfdBound': True, 'serviceFrozen': True,
+ 'observedProcessed': 3, 'processIdentity': identity}
+ calls = []
+ fleet = SimpleNamespace(run_id=receipt['runId'], frozen_processes={'node': identity},
+ record=lambda label, value: value, remote=lambda node, action, **params: calls.append(action) or receipt)
+ with patch.object(reader, 'freeze', return_value=(owner, identity)):
+ earlier = reader.capture_earlier(fleet, TOKEN, BUCKET, 'F07-early', advance=True)
+ self.assertEqual(calls, ['journal-capture-advance'])
+ self.assertTrue(earlier['advancedAndFrozen'])
+ self.assertEqual(earlier['processIdentity'], identity)
+ self.assertEqual(fleet.frozen_processes, {'node': identity})
+ for key, bad in [('checkpointAdvancedAndFrozen', False), ('pidfdBound', False), ('serviceFrozen', False),
+ ('observedProcessed', True), ('observedProcessed', 2), ('processIdentity', {})]:
+ original = receipt[key]; receipt[key] = bad
+ with self.subTest(key=key, bad=bad), patch.object(reader, 'freeze', return_value=(owner, identity)), \
+ self.assertRaises(ValueError):
+ reader.capture_earlier(fleet, TOKEN, BUCKET, 'F07-early', advance=True)
+ receipt[key] = original
+ self.assertEqual(fleet.frozen_processes, {'node': identity})
+
+ def test_atomic_frozen_generation_still_requires_full_fleet_checkpoint_and_root_censuses(self):
+ observed, value, _ = self.fixture(); roots, _ = self.root_fixture()
+ earlier = reader.owned_checkpoint(observed, TOKEN, BUCKET)
+ from scripts.validation_heal.fleet_node import PROCESS_FIELDS
+ identity = {name: name for name in PROCESS_FIELDS}; identity.update(pid=50)
+ earlier.update(advancedAndFrozen=True, processIdentity=identity)
+ value['checkpoint_time'] = 101; value['current_object_index'] = 3
+ value['processed_objects'].append('objects/third'); value['successful_objects'] = 3
+ value['admin']['outcome']['counters'].update(processed=3, unchanged=2)
+ observed['node']['records'][4].update(encoded(value, signed=True))
+ for tree in (observed, roots):
+ for peer in ('peer1', 'peer2', 'peer3'):
+ tree[peer] = copy.deepcopy(tree['node'])
+ tree[peer]['records'] = [{'disk': r['disk'], 'relativePath': r['relativePath'], 'present': False}
+ for r in tree[peer]['records']]
+ for change in (None, 'unregistered', 'changed-birth', 'not-frozen', 'second-owner'):
+ fleet = SimpleNamespace(nodes=['node', 'peer1', 'peer2', 'peer3'],
+ frozen_processes={} if change == 'unregistered' else {'node': identity},
+ record=lambda label, value: value)
+ if change == 'changed-birth': fleet.frozen_processes['node'] = {**identity, 'pid': 51}
+ calls = []
+ fleet.remote = lambda node, action, **params: calls.append(action) or {
+ **identity, 'state': 'S' if change == 'not-frozen' else 'T'}
+ current = copy.deepcopy(observed)
+ if change == 'second-owner': current['peer1'] = copy.deepcopy(current['node'])
+ def census(fleet, token, label, **params):
+ self.assertNotIn('nodes', params)
+ self.assertIn(label, ('Admin-checkpoint-frozen', 'Admin-root-frozen'))
+ return roots if params.get('family') == 'root' else current
+ with self.subTest(change=change), patch.object(reader, 'records', side_effect=census) as records:
+ if change:
+ with self.assertRaises(ValueError): reader.freeze(fleet, TOKEN, BUCKET, earlier=earlier)
+ else:
+ frozen, birth = reader.freeze(fleet, TOKEN, BUCKET, earlier=earlier)
+ self.assertEqual(birth, identity)
+ self.assertEqual(frozen['value']['admin']['outcome']['counters']['processed'], 3)
+ self.assertEqual(records.call_count, 2)
+ self.assertNotIn('process-signal', calls)
+
+ def test_stale_boundary_requires_actual_newer_progress_in_the_same_original_responsibility(self):
+ observed, value, _ = self.fixture()
+ earlier = reader.owned_checkpoint(observed, TOKEN, BUCKET)
+ with self.assertRaisesRegex(ValueError, "NOT_OBSERVED"): reader.after_generation(earlier, earlier)
+ value["checkpoint_time"] += 1; value["current_object_index"] += 1
+ value["admin"]["outcome"]["counters"]["processed"] += 1
+ value["admin"]["outcome"]["counters"]["unchanged"] += 1
+ observed["node"]["records"][4].update(encoded(value, signed=True))
+ current = reader.owned_checkpoint(observed, TOKEN, BUCKET)
+ reader.after_generation(current, earlier)
+ for key, new in [("disk", 2), ("node", "peer"), ("taskId", INCARNATION), ("bucketIncarnationId", TOKEN)]:
+ with self.subTest(key=key), self.assertRaises(ValueError): reader.after_generation({**current, key: new}, earlier)
+ changed = copy.deepcopy(current)
+ changed["value"]["admin"]["options"]["pool"] = False
+ changed["checkpoint"].update(encoded(changed["value"], signed=True))
+ with self.assertRaises(ValueError): reader.after_generation(changed, earlier)
+
+ def root_fixture(self):
+ value = {'schema': 3, 'task_id': TOKEN, 'priority': 'High', 'retry_attempts': 0,
+ 'created_at': {'secs_since_epoch': 100, 'nanos_since_epoch': 0},
+ 'heal_type': {'type': 'erasure_set', 'buckets': [], 'set_disk_id': 'pool_0_set_0', 'all_buckets': True},
+ 'options': {'scan_mode': 2, 'remove_corrupted': False, 'recreate_missing': False, 'update_parity': False,
+ 'recursive': True, 'dry_run': False, 'no_lock': False, 'pool_index': 0, 'set_index': 0}}
+ rows = [{'disk': disk, 'relativePath': name, 'present': False}
+ for disk in range(4) for name in journal.names('root', TOKEN)]
+ rows[3].update(encoded(value))
+ census = {'node': {'family': 'root', 'taskId': TOKEN, 'quiescent': True, 'readOnly': True,
+ 'directoryEnumerationUsed': False, 'records': rows}}
+ return census, value
+
+ def test_root_admission_is_original_erasure_set_responsibility_with_one_exact_owner(self):
+ census, _ = self.root_fixture()
+ owner = reader.owned_root_admission(census, TOKEN)
+ self.assertEqual((owner['node'], owner['disk']), ('node', 1))
+ self.assertTrue(owner['originalDurableResponsibility'])
+ for change in ('missing', 'terminal', 'duplicate', 'second-owner', 'hash', 'omitted-disk', 'unstable'):
+ census, value = self.root_fixture(); rows = census['node']['records']
+ if change == 'missing': rows[3]['present'] = False
+ elif change == 'terminal': rows[4].update(encoded(value))
+ elif change == 'duplicate': rows[0] = copy.deepcopy(rows[3])
+ elif change == 'second-owner': census['peer'] = copy.deepcopy(census['node'])
+ elif change == 'hash': rows[3]['sha256'] = 'a' * 64
+ elif change == 'omitted-disk': rows.pop()
+ else: census['node'].update(quiescent=False, stable=False, processBound=True, independentReads=2)
+ with self.subTest(change=change), self.assertRaises(ValueError): reader.owned_root_admission(census, TOKEN)
+
+ def test_root_erasure_admission_rejects_foreign_task_scope_options_time_and_terminal_shape(self):
+ for change in ('task', 'schema', 'scope', 'scope-type', 'pool-type', 'lock', 'retry', 'priority', 'time', 'bucket-incarnation'):
+ census, value = self.root_fixture()
+ if change == 'task': value['task_id'] = INCARNATION
+ elif change == 'schema': value['schema'] = 2
+ elif change == 'scope': value['heal_type'] = {'type': 'cluster'}
+ elif change == 'scope-type': value['heal_type']['all_buckets'] = 1
+ elif change == 'pool-type': value['options']['pool_index'] = False
+ elif change == 'lock': value['options']['no_lock'] = True
+ elif change == 'retry': value['retry_attempts'] = 1
+ elif change == 'priority': value['priority'] = 'Normal'
+ elif change == 'time': value['created_at']['nanos_since_epoch'] = 1_000_000_000
+ else: value['bucket_incarnation_id'] = INCARNATION
+ census['node']['records'][3].update(encoded(value))
+ with self.subTest(change=change), self.assertRaises(ValueError): reader.owned_root_admission(census, TOKEN)
+
+ def test_freeze_and_crash_preserve_checkpoint_resume_and_the_actual_root_admission(self):
+ from scripts.validation_heal.fleet_node import PROCESS_FIELDS
+ observed, _, _ = self.fixture(); roots, _ = self.root_fixture()
+ root = reader.owned_root_admission(roots, TOKEN)
+ identity = {name: name for name in PROCESS_FIELDS}; identity.update(pid=50, state='S')
+ observed['node'].update(processIdentity=identity, processBound=True, stable=True, independentReads=2)
+ actions = []
+ fleet = SimpleNamespace(nodes=['node'], frozen_processes={}, record=lambda label, value: value, stop=lambda: actions.append('stop'))
+ def remote(node, action, **params):
+ actions.append(action)
+ if action == 'process': return {**identity, 'state': 'T' if 'process-signal' in actions else 'S'}
+ if action == 'process-crash': return {'actualSignal': 9, 'pidfdBound': True, 'graceful': False, 'platformEventIssued': False}
+ return {}
+ fleet.remote = remote
+ def records(fleet, token, label, **params): return roots if params.get('family') == 'root' else observed
+ with patch.object(reader, 'records', side_effect=records):
+ frozen, process = reader.freeze(fleet, TOKEN, BUCKET)
+ self.assertEqual(frozen['rootAdmission'], root)
+ stopped = reader.stop_for_fault(fleet, TOKEN, BUCKET, frozen, process)
+ self.assertEqual(stopped['checkpoint'], frozen['checkpoint'])
+ self.assertTrue(stopped['rootContinuity']['nonTimeoutIntentUnchanged'])
+ self.assertTrue(stopped['rootContinuity']['budgetNotRefunded'])
+ self.assertEqual(fleet.frozen_processes, {})
+
+ def test_freeze_precedes_root_observation_to_keep_the_actual_checkpoint_window(self):
+ from scripts.validation_heal.fleet_node import PROCESS_FIELDS
+ observed, _, _ = self.fixture(); roots, _ = self.root_fixture()
+ identity = {name: name for name in PROCESS_FIELDS}; identity.update(pid=50, state='S')
+ observed['node'].update(processIdentity=identity, processBound=True, stable=True, independentReads=2)
+ actions = []
+ fleet = SimpleNamespace(nodes=['node'], frozen_processes={}, record=lambda label, value: value)
+ def remote(node, action, **params):
+ actions.append(action)
+ return {**identity, 'state': 'T' if 'process-signal' in actions else 'S'}
+ fleet.remote = remote
+ def records(fleet, token, label, **params):
+ if params.get('family') == 'root':
+ self.assertIn('process-signal', actions)
+ self.assertEqual(label, 'Admin-root-frozen')
+ return roots
+ return observed
+ with patch.object(reader, 'records', side_effect=records):
+ frozen, _ = reader.freeze(fleet, TOKEN, BUCKET)
+ self.assertTrue(frozen['rootAdmission']['originalDurableResponsibility'])
+
+ def test_foreign_root_after_freeze_retains_the_original_paused_process_for_cleanup(self):
+ from scripts.validation_heal.fleet_node import PROCESS_FIELDS
+ observed, _, _ = self.fixture(); roots, value = self.root_fixture()
+ identity = {name: name for name in PROCESS_FIELDS}; identity.update(pid=50, state='T')
+ observed['node'].update(processIdentity=identity, processBound=True, stable=True, independentReads=2)
+ fleet = SimpleNamespace(nodes=['node'], frozen_processes={}, record=lambda label, value: value, remote=lambda *args, **kwargs: identity)
+ def records(fleet, token, label, **params):
+ if label == 'Admin-root-frozen':
+ changed = copy.deepcopy(roots); altered = copy.deepcopy(value); altered['task_id'] = INCARNATION
+ changed['node']['records'][3].update(encoded(altered)); return changed
+ return roots if params.get('family') == 'root' else observed
+ with patch.object(reader, 'records', side_effect=records), self.assertRaisesRegex(ValueError, 'original explicit Admin'):
+ reader.freeze(fleet, TOKEN, BUCKET)
+ self.assertEqual(fleet.frozen_processes, {'node': identity})
+
+ def test_owner_is_frozen_before_observing_other_nodes_or_refreshing_its_process(self):
+ from scripts.validation_heal.fleet_node import PROCESS_FIELDS
+ observed, _, _ = self.fixture(); roots, _ = self.root_fixture()
+ identity = {name: name for name in PROCESS_FIELDS}; identity.update(pid=50)
+ observed['node'].update(processIdentity=identity, processBound=True, stable=True, independentReads=2)
+ fleet = SimpleNamespace(nodes=['node', 'peer1', 'peer2', 'peer3'], frozen_processes={},
+ record=lambda label, value: value)
+ actions = []
+ def remote(node, action, **params):
+ if action == 'process': self.assertIn('process-signal', actions)
+ actions.append(action)
+ return {**identity, 'state': 'T'}
+ fleet.remote = remote
+ def all_nodes(value):
+ full = copy.deepcopy(value)
+ for peer in fleet.nodes[1:]:
+ full[peer] = copy.deepcopy(value['node'])
+ full[peer]['records'] = [{'disk': r['disk'], 'relativePath': r['relativePath'], 'present': False}
+ for r in full[peer]['records']]
+ return full
+ def records(fleet, token, label, **params):
+ if label == 'Admin-checkpoint-live':
+ self.assertEqual(params.get('nodes'), ('node',))
+ return observed
+ self.assertIn('process-signal', actions)
+ self.assertNotIn('nodes', params)
+ return all_nodes(roots if params.get('family') == 'root' else observed)
+ with patch.object(reader, 'records', side_effect=records):
+ frozen, birth = reader.freeze(fleet, TOKEN, BUCKET)
+ self.assertEqual(birth, identity)
+ self.assertTrue(frozen['rootAdmission']['originalDurableResponsibility'])
+
+ def test_live_record_drift_is_retried_inside_the_original_observation_budget(self):
+ from scripts.validation_heal.fleet_node import PROCESS_FIELDS
+ observed, _, _ = self.fixture(); roots, _ = self.root_fixture()
+ identity = {name: name for name in PROCESS_FIELDS}; identity.update(pid=50)
+ observed['node'].update(processIdentity=identity, processBound=True, stable=True, independentReads=2)
+ fleet = SimpleNamespace(nodes=['node'], frozen_processes={}, record=lambda label, value: value,
+ remote=lambda *a, **k: {**identity, 'state': 'T'})
+ calls = []
+ def records(fleet, token, label, **params):
+ calls.append(label)
+ if len(calls) == 1:
+ raise ValueError('NOT_OBSERVED: exact durable records changed between the two bounded reads')
+ return roots if params.get('family') == 'root' else observed
+ with patch.object(reader, 'records', side_effect=records), patch.object(reader.time, 'sleep'):
+ frozen, birth = reader.freeze(fleet, TOKEN, BUCKET)
+ self.assertEqual(calls.count('Admin-checkpoint-live'), 2)
+ self.assertEqual(birth, identity)
+ self.assertTrue(frozen['rootAdmission']['originalDurableResponsibility'])
+
+ def test_record_drift_timeout_and_non_observation_errors_do_not_signal(self):
+ for message in ('NOT_OBSERVED: exact durable record changed during a bounded read',
+ 'foreign process birth', 'checkpoint digest is invalid'):
+ events = []
+ fleet = SimpleNamespace(nodes=['node'], frozen_processes={}, record=lambda label, value: value,
+ remote=lambda *a, **k: events.append(a))
+ with self.subTest(message=message), patch.object(reader, 'records', side_effect=ValueError(message)), \
+ patch.object(reader.time, 'monotonic', side_effect=[0, 61]), self.assertRaises(ValueError):
+ reader.freeze(fleet, TOKEN, BUCKET)
+ self.assertEqual(events, [])
+ self.assertEqual(fleet.frozen_processes, {})
+
+ def test_full_census_after_early_freeze_still_rejects_a_second_physical_owner(self):
+ from scripts.validation_heal.fleet_node import PROCESS_FIELDS
+ observed, _, _ = self.fixture()
+ identity = {name: name for name in PROCESS_FIELDS}; identity.update(pid=50)
+ observed['node'].update(processIdentity=identity, processBound=True, stable=True, independentReads=2)
+ fleet = SimpleNamespace(nodes=['node', 'peer1', 'peer2', 'peer3'], frozen_processes={},
+ record=lambda label, value: value, remote=lambda *args, **params: {**identity, 'state': 'T'})
+ def records(fleet, token, label, **params):
+ if label == 'Admin-checkpoint-live': return observed
+ full = {node: copy.deepcopy(observed['node']) for node in fleet.nodes}
+ return full
+ with patch.object(reader, 'records', side_effect=records), self.assertRaises(ValueError):
+ reader.freeze(fleet, TOKEN, BUCKET)
+ self.assertEqual(fleet.frozen_processes, {'node': identity})
+
+ def test_unbound_census_birth_never_signals_a_process(self):
+ from scripts.validation_heal.fleet_node import PROCESS_FIELDS
+ for change in ('missing-birth', 'missing-field', 'boolean-pid', 'unstable', 'unbound'):
+ observed, _, _ = self.fixture()
+ identity = {name: name for name in PROCESS_FIELDS}; identity.update(pid=50)
+ observed['node'].update(processIdentity=identity, processBound=True, stable=True, independentReads=2)
+ if change == 'missing-birth': observed['node'].pop('processIdentity')
+ elif change == 'missing-field': identity.pop('startTicks')
+ elif change == 'boolean-pid': identity['pid'] = True
+ elif change == 'unstable': observed['node']['stable'] = False
+ else: observed['node']['processBound'] = False
+ actions = []
+ fleet = SimpleNamespace(nodes=['node'], frozen_processes={}, record=lambda label, value: value,
+ remote=lambda *a, **k: actions.append(a))
+ with patch.object(reader, 'records', return_value=observed), self.subTest(change=change), \
+ self.assertRaises(ValueError):
+ reader.freeze(fleet, TOKEN, BUCKET)
+ self.assertEqual(actions, [])
+ self.assertEqual(fleet.frozen_processes, {})
+
+
+if __name__ == "__main__": unittest.main()
diff --git a/test_validation_heal_coordinator_crash.py b/test_validation_heal_coordinator_crash.py
new file mode 100644
index 00000000..1a246469
--- /dev/null
+++ b/test_validation_heal_coordinator_crash.py
@@ -0,0 +1,264 @@
+import copy
+from datetime import datetime
+import hashlib
+import json
+import os
+from pathlib import Path
+import tempfile
+import sys
+import types
+import unittest
+from unittest.mock import patch
+
+from scripts.validation_heal import coordinator_crash as executor, replacement_journal, root_restart, mrf_replay
+from scripts.validation_heal import healthy, fleet_node
+from scripts.validation_heal.matrix import AUTOMATIC_CASES, select
+import test_validation_heal as fixtures
+
+
+class CoordinatorCrashTests(unittest.TestCase):
+ token = "01234567-89ab-4def-8123-456789abcdef"
+
+ def peers(self):
+ return {node: {**{name: name + "-" + node for name in fleet_node.PROCESS_FIELDS},
+ "pid": 100 + index, "startTicks": 1000 + index} for index, node in enumerate(("node1", "node2", "node3", "node4"))}
+
+ def test_coordinator_restart_keeps_every_target_process_and_boot(self):
+ before, after = self.peers(), self.peers()
+ after["node1"].update(pid=200, startTicks=2000)
+ self.assertTrue(executor.continuity(before, after, "node1")["everyTargetAndPeerKeptOriginalProcess"])
+ for kind in ("target-pid", "target-mount", "owner-env", "owner-boot", "missing", "unchanged"):
+ bad = copy.deepcopy(after)
+ if kind == "target-pid": bad["node4"]["pid"] += 1
+ elif kind == "target-mount": bad["node4"]["mountNamespace"] = "other"
+ elif kind == "owner-env": bad["node1"]["environmentFileSha256"] = "other"
+ elif kind == "owner-boot": bad["node1"]["bootId"] = "other"
+ elif kind == "missing": bad.pop("node2")
+ else: bad["node1"] = before["node1"]
+ with self.subTest(kind=kind), self.assertRaises(ValueError): executor.continuity(before, bad, "node1", private_mounts=True)
+ after["node1"]["mountNamespace"] = "mnt:[new]"
+ with self.assertRaises(ValueError): executor.continuity(before, after, "node1")
+ executor.continuity(before, after, "node1", private_mounts=True)
+
+ def test_actual_kill_refuses_graceful_exit_platform_event_and_changed_peer(self):
+ peers = self.peers()
+ proof = {"actualSignal": 9, "signal": "SIGKILL", "graceful": False, "pidfdBound": True, "platformEventIssued": False}
+ for kind in ("good", "graceful", "signal", "platform", "pidfd", "peer"):
+ calls, changed = [], copy.deepcopy(proof)
+ if kind == "graceful": changed["graceful"] = True
+ elif kind == "signal": changed["actualSignal"] = 15
+ elif kind == "platform": changed["platformEventIssued"] = True
+ elif kind == "pidfd": changed["pidfdBound"] = False
+ def remote(node, action, **params):
+ calls.append((node, action, params))
+ if action == "process-crash": return changed
+ if action == "process": return {**peers[node], **({"pid": 999} if kind == "peer" else {})}
+ return {}
+ fleet = types.SimpleNamespace(nodes=list(peers), frozen_processes={"node1": peers["node1"]},
+ remote=remote, record=lambda label, value: value)
+ with self.subTest(kind=kind):
+ if kind == "good":
+ executor.kill(fleet, "node1", peers["node1"], peers)
+ self.assertNotIn("node1", fleet.frozen_processes)
+ self.assertEqual(calls[0][2]["expected"], peers["node1"])
+ self.assertEqual([node for node, action, _ in calls if action == "process"], list(peers)[1:])
+ else:
+ with self.assertRaises(ValueError): executor.kill(fleet, "node1", peers["node1"], peers)
+
+ def test_crash_preserves_original_root_intent_without_fabricated_timeout_checkpoint(self):
+ before = root_restart.root_intent(fixtures.HealRootRestartOracleTest().records(), self.token, "node1")
+ proof = root_restart.crashed_owner(before, copy.deepcopy(before))
+ self.assertFalse(proof["gracefulTimeoutCheckpointClaimed"])
+ for kind in ("disk", "sha", "timeout", "scope", "token"):
+ bad = copy.deepcopy(before)
+ if kind == "disk": bad["disk"] += 1
+ elif kind == "sha": bad["sha256"] = "other"
+ elif kind == "timeout": bad["value"]["options"]["timeout"] = {"secs": 250, "nanos": 0}
+ elif kind == "scope": bad["value"]["heal_type"] = {"type": "bucket", "bucket": "other"}
+ else: bad["value"]["task_id"] = "another"
+ with self.subTest(kind=kind), self.assertRaises(ValueError): root_restart.crashed_owner(before, bad)
+
+ def checkpoint(self):
+ endpoint = "http://rustfs-node4:9000/data/rustfs4/owned/data"
+ generation = {"taskId": self.token, "generation": self.token, "setDiskId": "pool_0_set_0", "targetSlots": [endpoint]}
+ intent = {"schema_version": 7, "task_id": self.token, "replacement_generation": self.token,
+ "set_disk_id": generation["setDiskId"], "replacement_targets": [endpoint], "completed": False,
+ "replacement_target_identities": [{"endpoint": endpoint, "canonical_path": "/data/rustfs4/owned/data",
+ "physical_device_ids": ["uuid:disk"], "filesystem_identity": "filesystem"}]}
+ rows = []
+ for disk in range(4):
+ for kind, (suffix, _) in replacement_journal.FILES.items():
+ row = {"disk": disk, "kind": kind, "present": disk == 2 and kind != "proof",
+ "relativePath": f".rustfs.sys/buckets/ahm-replacement/{self.token}_{suffix}"}
+ if row["present"]:
+ raw = b"sealed" if kind == "seal" else json.dumps(intent).encode()
+ row.update(size=len(raw), sha256=hashlib.sha256(raw).hexdigest())
+ row.update({"sealed": True} if kind == "seal" else {"value": copy.deepcopy(intent)})
+ rows.append(row)
+ return {"taskId": self.token, "readOnly": True, "stable": True, "records": rows}, generation
+
+ def test_replacement_checkpoint_requires_sealed_exact_generation_and_target_identity(self):
+ observed, generation = self.checkpoint()
+ self.assertEqual(executor.replacement_checkpoint(observed, generation)["sealedRecordDisks"], [2])
+ for kind in ("missing", "duplicate", "schema", "generation", "target", "device", "complete", "proof", "path", "unsealed", "hash"):
+ bad = copy.deepcopy(observed)
+ intent = next(row for row in bad["records"] if row["present"] and row["kind"] == "intent")
+ seal = next(row for row in bad["records"] if row["present"] and row["kind"] == "seal")
+ if kind == "missing": intent["present"] = False
+ elif kind == "duplicate": bad["records"][-1] = copy.deepcopy(bad["records"][0])
+ elif kind == "schema": intent["value"]["schema_version"] = True
+ elif kind == "generation": intent["value"]["replacement_generation"] = "another"
+ elif kind == "target": intent["value"]["replacement_targets"] = ["another"]
+ elif kind == "device": intent["value"]["replacement_target_identities"][0]["physical_device_ids"] = []
+ elif kind == "complete": intent["value"]["completed"] = True
+ elif kind == "proof": bad["records"][-1].update(present=True, size=10, sha256="a" * 64)
+ elif kind == "path": intent["relativePath"] = "foreign"
+ elif kind == "unsealed": seal["present"] = False
+ else: seal["sha256"] = "a" * 64
+ with self.subTest(kind=kind), self.assertRaises(ValueError): executor.replacement_checkpoint(bad, generation)
+
+ def test_replacement_journal_reads_only_exact_names_and_rejects_symlink_hardlink_and_oversize(self):
+ with tempfile.TemporaryDirectory() as folder:
+ roots = [Path(folder).resolve() / str(disk) for disk in range(4)]
+ for root in roots: (root / "data").mkdir(parents=True)
+ parent = roots[2] / "data/.rustfs.sys/buckets/ahm-replacement"
+ parent.mkdir(parents=True)
+ path = parent / (self.token + "_ahm_replacement_intent.json")
+ path.write_text('{"task_id":"' + self.token + '"}')
+ (parent / "unrelated_ahm_replacement_intent.json").write_text("untrusted non-JSON control")
+ observed = replacement_journal.read_records(roots, self.token, fleet_node.checked)
+ self.assertEqual(sum(row["present"] for row in observed["records"]), 1)
+ self.assertTrue(observed["readOnly"])
+ raw = path.read_bytes()
+ for kind in ("symlink", "hardlink", "oversize"):
+ path.unlink()
+ if kind == "symlink": path.symlink_to(parent / "unrelated_ahm_replacement_intent.json")
+ elif kind == "hardlink": os.link(parent / "unrelated_ahm_replacement_intent.json", path)
+ else: path.write_bytes(b"x" * (1024**2 + 1))
+ with self.subTest(kind=kind), self.assertRaises((ValueError, OSError)):
+ replacement_journal.read_records(roots, self.token, fleet_node.checked)
+ path.unlink(); path.write_bytes(raw)
+ with self.assertRaises(ValueError): replacement_journal.read_records(roots, "../other", fleet_node.checked)
+
+ def mrf_scene(self):
+ bucket, _, source = fixtures.HealNodeOutageTest().snapshot()
+ scene, obligations = {}, []
+ for node, value in source.items():
+ scene[node] = {"formats": copy.deepcopy(value["formats"]), "files": []}
+ for index in range(2):
+ key = f"offline/new-{index:02d}.bin"
+ for entry in value["files"]:
+ row = copy.deepcopy(entry)
+ row["path"] = row["path"].replace("new.bin", key)
+ row["sha256"] = hashlib.sha256(row["path"].encode() + node.encode() + str(row["disk"]).encode()).hexdigest()
+ if row.get("metadata"):
+ row["metadata"]["key"] = key
+ row["metadata"]["versions"] = [row["metadata"]["versions"][0]]
+ row["metadata"]["versions"][0]["isLatest"] = True
+ scene[node]["files"].append(row)
+ if node == "node1": obligations.append({"bucket": bucket, "key": key, "versionId": "new-version",
+ "poolIndex": 0, "setIndex": 0, "kind": 3})
+ authorities = copy.deepcopy(scene)
+ authorities["node4"]["files"] = []
+ scene["node4"]["files"] = [row for row in scene["node4"]["files"] if "new-00" in row["path"]]
+ return scene, authorities, bucket, obligations
+
+ def test_mrf_partial_progress_is_not_a_final_hash_or_streaming_proof(self):
+ scene, authorities, bucket, obligations = self.mrf_scene()
+ proof = executor.mrf_progress(scene, authorities, bucket, obligations, "node4")
+ self.assertTrue(proof["partial"])
+ self.assertEqual(proof["pendingACKs"], 1)
+ self.assertFalse(proof["finalStreamingProofClaimed"])
+ empty = copy.deepcopy(scene); empty["node4"]["files"] = []
+ self.assertFalse(executor.mrf_progress(empty, authorities, bucket, obligations, "node4")["partial"])
+ for kind in ("role", "version", "authority", "part"):
+ bad, originals = copy.deepcopy(scene), copy.deepcopy(authorities)
+ version = next(row for row in bad["node4"]["files"] if row.get("metadata"))["metadata"]["versions"][0]
+ if kind == "role": version["erasure"]["index"] = 1
+ elif kind == "version": version["dataDir"] = "other"
+ elif kind == "authority": next(row for row in originals["node1"]["files"] if row.get("metadata"))["metadata"]["versions"][0]["size"] += 1
+ else: next(row for row in bad["node4"]["files"] if not row.get("metadata"))["sha256"] = "incomplete"
+ with self.subTest(kind=kind), self.assertRaises(ValueError): executor.mrf_progress(bad, originals, bucket, obligations, "node4")
+
+ def events(self):
+ stamp = "2026-10-11T00:00:00+00:00"
+ obligation = {"bucket": "owned", "key": "key", "versionId": "null", "poolIndex": 0, "setIndex": 0, "kind": 3}
+ rows = [{"target": "rustfs::heal::manager", "message": "Heal queue request accepted", "source": "mrf",
+ "result": "accepted", "request_id": self.token, "timestamp": stamp},
+ {"target": "rustfs::heal::task", "message": "Heal object started", "stage": "start", "task_id": self.token,
+ "bucket": "owned", "object": "key", "version_id": "None", "timestamp": stamp}]
+ bindings = {mrf_replay.identity(obligation): [{"node": "node1"}]}
+ return {"node1": rows}, [obligation], bindings, datetime.fromisoformat(stamp)
+
+ def test_mrf_source_requires_exact_ack_and_admission_without_terminal_completion(self):
+ events, obligations, bindings, stamp = self.events()
+ self.assertEqual(executor.active_mrf_task(events, obligations, bindings, stamp, "node4")[0][:2], ("node1", self.token))
+ for kind in ("source", "version", "key", "terminal", "missing-version", "target"):
+ bad = copy.deepcopy(events)
+ if kind == "source": bad["node1"][0]["source"] = "scanner"
+ elif kind == "version": bad["node1"][1]["version_id"] = "foreign"
+ elif kind == "key": bad["node1"][1]["object"] = "foreign"
+ elif kind == "terminal": bad["node1"].append({"target": "rustfs::heal::manager", "task_id": self.token,
+ "state": "task_completed", "timestamp": stamp.isoformat()})
+ elif kind == "missing-version": del bad["node1"][1]["version_id"]
+ self.assertEqual(executor.active_mrf_task(bad, obligations, bindings, stamp, "node1" if kind == "target" else "node4"), [])
+
+ def test_rust_debug_version_representation_never_merges_distinct_versions(self):
+ for raw in ("version", 'Some("version")'): self.assertTrue(mrf_replay.source_version(raw, "version"))
+ for raw in ("None", "null", 'Some("other")', "other", 'Some("version") trailing'):
+ self.assertFalse(mrf_replay.source_version(raw, "version"))
+ for raw in (None, "None", "null", "00000000-0000-0000-0000-000000000000"):
+ self.assertTrue(mrf_replay.source_version(raw, "null"))
+ self.assertFalse(mrf_replay.source_version('Some("other")', "null"))
+
+ def test_mrf_crash_admission_is_bounded_with_other_sources_disabled(self):
+ config = fleet_node.runtime_config("mrf-crash")
+ self.assertEqual(config["RUSTFS_HEAL_MRF_REPLAY_BATCH"], "1")
+ self.assertEqual(config["RUSTFS_HEAL_MAX_CONCURRENT_HEALS"], "1")
+ self.assertEqual(config["RUSTFS_HEAL_MAX_CONCURRENT_PER_SET"], "1")
+ self.assertEqual(config["RUSTFS_HEAL_MRF_ENABLE"], "true")
+ self.assertEqual(config["RUSTFS_SCANNER_ENABLED"], "false")
+ self.assertEqual(config["RUSTFS_HEAL_AUTO_HEAL_ENABLE"], "false")
+ self.assertNotIn("RUSTFS_HEAL_MAX_CONCURRENT_HEALS", fleet_node.runtime_config("durable-mrf"))
+
+ def test_remote_checkpoint_module_is_carried_in_the_actual_ssh_program(self):
+ sources = []
+ def invoke(args, **options):
+ sources.append(options["input"])
+ compile(sources[-1], "node-source", "exec")
+ return types.SimpleNamespace(returncode=0, stdout='{"readOnly":true}', stderr="")
+ with tempfile.TemporaryDirectory() as folder, patch.object(healthy.subprocess, "run", side_effect=invoke):
+ fleet = healthy.Fleet(["node1"], Path(folder), "gh-12345-1-h2")
+ self.assertTrue(fleet.remote("node1", "replacement-records", taskId=self.token)["readOnly"])
+ self.assertIn("ahm-replacement/", sources[0])
+ self.assertIn("def read_records(roots, token, checked):", sources[0])
+
+ def test_all_three_mechanisms_are_required_and_dirty_cleanup_stops_dispatch(self):
+ self.assertEqual(select("F04")[0]["variants"], list(executor.BRANCHES))
+ self.assertIn(executor.CASE_ID, AUTOMATIC_CASES)
+ for dirty in (False, True):
+ with tempfile.TemporaryDirectory() as folder:
+ output = Path(folder)
+ candidate = output / "candidate.json"
+ candidate.write_text(json.dumps({"source_sha": executor.REFERENCE["candidateCommit"]}))
+ argv = ["program", "--candidate", str(candidate), "--rc-bin", "rc", "--evidence", str(output),
+ "--tool-directory", "tools", "--case-id", executor.CASE_ID, "--testing-commit", "a" * 40,
+ "--binary-sha", "b" * 64]
+ root = {"topology": "H2", "verdict": "FAIL", "cleanup": "FAIL" if dirty else "PASS"}
+ mrf = {"topology": "H2", "branch": "offline-put", "verdict": "PASS", "cleanup": "PASS"}
+ replacement_result = {"topology": "H2", "branch": executor.BRANCHES[2], "verdict": "PASS", "cleanup": "PASS"}
+ with patch.object(sys, "argv", argv), patch.dict(os.environ, {"GITHUB_RUN_ID": "12345"}), \
+ patch.object(executor, "dx_topology", return_value=root) as root_run, \
+ patch.object(executor.mrf_replay, "run_branch", return_value=mrf) as mrf_run, \
+ patch.object(executor, "run_replacement", return_value=replacement_result) as replacement_run:
+ with self.assertRaises(ValueError): executor.main()
+ summary = json.loads((output / "healthy-summary.json").read_text())
+ self.assertFalse(summary["complete"])
+ self.assertEqual(summary["cleanup"], "FAIL" if dirty else "PASS")
+ self.assertEqual(len(summary["results"]), 1 if dirty else 3)
+ self.assertEqual(root_run.call_args.kwargs["target_nodes"], ["vm001", "vm002", "vm003"])
+ self.assertEqual(mrf_run.call_count, 0 if dirty else 1)
+ self.assertEqual(replacement_run.call_count, 0 if dirty else 1)
+
+
+if __name__ == "__main__": unittest.main()
diff --git a/test_validation_heal_encrypted.py b/test_validation_heal_encrypted.py
new file mode 100644
index 00000000..2709502e
--- /dev/null
+++ b/test_validation_heal_encrypted.py
@@ -0,0 +1,170 @@
+import base64
+import copy
+import hashlib
+import json
+import os
+from pathlib import Path
+import sys
+import tempfile
+import types
+import unittest
+from unittest.mock import patch
+
+from scripts.validation_heal import encrypted, encryption_runtime, heal_fault
+from scripts.validation_heal.heal_probe import Probe, redact
+from scripts.validation_heal.shards import map_target
+from scripts.validation_heal.matrix import AUTOMATIC_CASES, select
+import test_validation_heal as shard_fixtures
+from test_validation_heal_authorization import binding
+
+
+class EncryptedObjectTests(unittest.TestCase):
+ def test_reviewed_candidate_requires_its_own_inspector_before_fleet_deploy(self):
+ for commit in (*encrypted.REVIEWED_CANDIDATES, "f" * 40):
+ with self.subTest(commit=commit), tempfile.TemporaryDirectory() as folder:
+ root = Path(folder)
+ candidate = {"source_sha": commit}
+ (root / "candidate.json").write_text(json.dumps(candidate))
+ args = types.SimpleNamespace(evidence=root, candidate=root / "candidate.json", tool_directory=root / "tools")
+ with patch.dict(os.environ, {"GITHUB_RUN_ID": "12345"}), patch.object(encrypted, "Fleet") as fleet, \
+ patch.object(encrypted, "verify_inspector", side_effect=ValueError("inspector boundary")) as inspector:
+ message = "inspector boundary" if commit in encrypted.REVIEWED_CANDIDATES else "unreviewed encrypted"
+ with self.assertRaisesRegex(ValueError, message):
+ encrypted.run_branch(args, "H1", ["node1"], "sse-c")
+ fleet.return_value.deploy.assert_not_called()
+ if commit in encrypted.REVIEWED_CANDIDATES:
+ inspector.assert_called_once_with(args.tool_directory, candidate)
+ else:
+ inspector.assert_not_called()
+
+ def geometry(self):
+ before = shard_fixtures.HealShardOracleTest().snapshot()
+ key, bucket = "objects/0008.bin", "rv-heal-0123456789abcdef"
+ expected = {"key": key, "versionId": "null", "isLatest": True, "deleteMarker": False,
+ "payload": {"size": encrypted.SIZE, "sha256": "plaintext"}}
+ for observation in before.values():
+ for entry in observation["files"]:
+ if entry["path"] == f"{bucket}/{key}/xl.meta":
+ for version in entry["metadata"]["versions"]:
+ version["size"] = encrypted.SIZE + 16000
+ version["parts"][0].update(size=encrypted.SIZE + 16000, actualSize=encrypted.SIZE)
+ return before, bucket, expected
+
+ def test_ciphertext_geometry_keeps_plain_mapper_strict_and_does_not_rewrite_evidence(self):
+ before, bucket, expected = self.geometry()
+ saved = copy.deepcopy(before)
+ target = encrypted.map_cipher_target(before, bucket, expected, "data")
+ self.assertTrue(target["encryptedPhysicalLayer"])
+ self.assertEqual(target["plaintextACK"], expected["payload"])
+ self.assertNotEqual(target["ciphertextPartSize"], expected["payload"]["size"])
+ self.assertEqual(before, saved)
+ with self.assertRaisesRegex(ValueError, "encrypted"):
+ map_target(before, bucket, expected["key"], "data", 1)
+
+ def test_ciphertext_policy_rejects_plaintext_downgrade_wrong_ack_and_unsupported_transforms(self):
+ for kind in ("plain", "actual", "size", "multipart", "inline", "unbounded", "bool"):
+ before, bucket, expected = self.geometry()
+ meta = next(entry for entry in before["node1"]["files"] if entry["path"] == f"{bucket}/{expected['key']}/xl.meta")
+ version = meta["metadata"]["versions"][0]
+ if kind == "plain": version["parts"][0]["size"] = encrypted.SIZE
+ if kind == "actual": version["parts"][0]["actualSize"] -= 1
+ if kind == "size": version["size"] -= 1
+ if kind == "multipart": version["parts"].append(copy.deepcopy(version["parts"][0]))
+ if kind == "inline": version["inline"] = {"length": 1}
+ if kind == "unbounded": version["parts"][0]["size"] = encrypted.SIZE + 2 * 1024**2
+ if kind == "bool": version["parts"][0]["actualSize"] = True
+ with self.subTest(kind=kind), self.assertRaises(ValueError): encrypted.map_cipher_target(before, bucket, expected, "data")
+
+ def test_customer_key_is_resolved_only_for_sdk_and_both_key_forms_are_redacted(self):
+ key, wrong = "0123456789abcdef0123456789abcdef", "fedcba9876543210fedcba9876543210"
+ probe = Probe(binding())
+ sdk = unittest.mock.Mock()
+ sdk.head_object.return_value = {"ResponseMetadata": {"HTTPStatusCode": 200}}
+ with patch.dict(os.environ, {"HEAL_SSE_C_KEY": key, "HEAL_SSE_C_WRONG_KEY": wrong}), patch.object(probe, "s3_client", return_value=sdk):
+ for reference, actual in (("$HEAL_SSE_C_KEY", key), ("$HEAL_SSE_C_WRONG_KEY", wrong)):
+ observed = probe.s3("head_object", {"Bucket": probe.binding["bucketPrefix"], "Key": "encrypted",
+ "SSECustomerAlgorithm": "AES256", "SSECustomerKey": reference})
+ self.assertTrue(observed["acknowledged"])
+ self.assertEqual(sdk.head_object.call_args.kwargs["SSECustomerKey"], actual)
+ self.assertEqual(observed["params"]["SSECustomerKey"], "[REDACTED]")
+ self.assertNotIn(actual, json.dumps(redact({"error": "rejected " + actual})))
+
+ def test_encryption_headers_reject_silent_plaintext_or_wrong_key_identity(self):
+ with patch.dict(os.environ, {"HEAL_SSE_C_KEY": "0123456789abcdef0123456789abcdef"}):
+ good = {"ServerSideEncryption": "aws:kms", "SSEKMSKeyId": "owned-key"}
+ encrypted.encryption_headers(good, "encrypted-versioned", "owned-key")
+ for changed in ({}, {**good, "SSEKMSKeyId": "other"}, {**good, "ServerSideEncryption": "AES256"}):
+ with self.assertRaises(ValueError): encrypted.encryption_headers(changed, "encrypted-versioned", "owned-key")
+ raw_key = os.environ["HEAL_SSE_C_KEY"].encode()
+ customer = {"SSECustomerAlgorithm": "AES256", "SSECustomerKeyMD5": base64.b64encode(hashlib.md5(raw_key, usedforsecurity=False).digest()).decode()}
+ encrypted.encryption_headers(customer, "sse-c", "unused")
+ with self.assertRaises(ValueError): encrypted.encryption_headers({**customer, "ServerSideEncryption": "AES256"}, "sse-c", "unused")
+
+ def test_key_refusal_requires_reviewed_explicit_error_and_never_accepts_missing_object(self):
+ for customer, status, code in ((True, 400, "InvalidRequest"), (False, 403, "AccessDenied")):
+ good = {"acknowledged": False, "resultUnknown": False,
+ "response": {"ResponseMetadata": {"HTTPStatusCode": status}, "Error": {"Code": code}}}
+ self.assertTrue(encrypted.denied_key(good, customer=customer)["missingOrWrongKeyIsNotObjectAbsence"])
+ for kind in ("transport", "payload", "absence", "success"):
+ bad = copy.deepcopy(good)
+ if kind == "transport": bad["resultUnknown"] = True
+ if kind == "payload": bad["payload"] = {"sha256": "decrypted"}
+ if kind == "absence": bad["response"] = {"ResponseMetadata": {"HTTPStatusCode": 404}, "Error": {"Code": "NoSuchKey"}}
+ if kind == "success": bad["acknowledged"] = True
+ with self.subTest(customer=customer, kind=kind), self.assertRaises(ValueError): encrypted.denied_key(bad, customer=customer)
+
+ def test_versioned_fixture_preserves_real_distinct_acks_and_every_generation(self):
+ sequence = [0]
+ def invoke(operation, params, body=None):
+ if operation == "get_bucket_versioning": return {"Status": "Enabled"}
+ if operation == "put_object":
+ sequence[0] += 1
+ return {"VersionId": f"real-{sequence[0]}", "ServerSideEncryption": "aws:kms", "SSEKMSKeyId": "owned-key"}
+ return {}
+ recorder = types.SimpleNamespace(call=invoke)
+ with patch.object(encrypted, "COUNT", 2):
+ expected = encrypted.seed_encrypted(recorder, "bucket", "encrypted-versioned", "owned-key")
+ self.assertEqual(len(expected), 6)
+ self.assertEqual(sum(item["isLatest"] for item in expected), 2)
+ self.assertEqual(len({item["versionId"] for item in expected}), 6)
+ self.assertEqual(len({item["payload"]["sha256"] for item in expected}), 6)
+ with patch.object(encrypted, "COUNT", 1), self.assertRaises(ValueError):
+ encrypted.seed_encrypted(types.SimpleNamespace(call=lambda *args: {"Status": "Enabled", "VersionId": "null",
+ "ServerSideEncryption": "aws:kms", "SSEKMSKeyId": "owned-key"}), "bucket", "encrypted-versioned", "owned-key")
+
+ def test_inventory_decryption_options_cannot_override_identity_or_expose_raw_key(self):
+ probe = Probe.__new__(Probe)
+ probe.binding = {"bucketPrefix": "rv-heal-test-bucket"}
+ for options in ({"Bucket": "unowned"}, {"Key": "other"}, {"VersionId": "other"},
+ {"SSECustomerAlgorithm": "AES256", "SSECustomerKey": "raw-secret"}, {"SSECustomerKey": "$HEAL_SSE_C_WRONG_KEY"}):
+ with self.subTest(options=options), self.assertRaises(ValueError):
+ probe.inventory("rv-heal-test-bucket", object_read_options=options)
+
+ @unittest.skipUnless(sys.platform == "linux", "actual Linux FD/mount guard executes in CI")
+ def test_static_dependency_keeps_private_key_file_and_configured_process_identity(self):
+ with tempfile.TemporaryDirectory() as folder:
+ state = Path(folder)
+ original = b"RUSTFS_ACCESS_KEY=temporary\n"
+ (state / "service.env").write_bytes(original)
+ owner = {"runId": "gh-12345-1-h1"}
+ key_id = "rv-heal-key-" + owner["runId"]
+ key = base64.b64encode(bytes(range(32))).decode()
+ proof = encryption_runtime.configure(state, owner, {"keyId": key_id, "keyBase64": key}, heal_fault)
+ self.assertNotIn(key, json.dumps(proof))
+ self.assertFalse(proof["sharedKmsTouched"])
+ self.assertNotIn(key.encode(), (state / "service.env").read_bytes())
+ settings = encryption_runtime.settings_for(state, key_id)
+ digest = lambda path: hashlib.sha256(path.read_bytes()).hexdigest()
+ checked = lambda path, boundary: path
+ self.assertTrue(encryption_runtime.attest(state, owner, checked, digest, settings)["loadedByCandidate"])
+ with self.assertRaises(ValueError): encryption_runtime.attest(state, owner, checked, digest, {**settings, "RUSTFS_KMS_BACKEND": "local"})
+ (state / "static-kms-secret").chmod(0o644)
+ with self.assertRaises(ValueError): encryption_runtime.attest(state, owner, checked, digest, settings)
+ with self.assertRaises(ValueError): encryption_runtime.configure(state, owner, {"keyId": key_id, "keyBase64": key}, heal_fault)
+
+ def test_all_source_encryption_branches_have_an_automatic_entry(self):
+ self.assertEqual(select("D08")[0]["variants"], list(encrypted.BRANCHES))
+ self.assertIn(encrypted.CASE_ID, AUTOMATIC_CASES)
+
+
+if __name__ == "__main__": unittest.main()
diff --git a/test_validation_heal_endpoint_resolution.py b/test_validation_heal_endpoint_resolution.py
new file mode 100644
index 00000000..31613085
--- /dev/null
+++ b/test_validation_heal_endpoint_resolution.py
@@ -0,0 +1,96 @@
+"""Read-only DNS admission for the current auto-testing race endpoints."""
+import subprocess
+import json
+from pathlib import Path
+import tempfile
+from types import SimpleNamespace
+import unittest
+from unittest.mock import Mock, patch
+
+from scripts.validation_heal import fleet_node, race
+
+
+class EndpointResolutionTests(unittest.TestCase):
+ def request(self, topology='h2'):
+ return {'runId': 'gh-12345-1-' + topology, 'action': 'endpoint-resolution'}
+
+ def test_observation_resolves_only_existing_fixed_aliases_without_retries_or_mutations(self):
+ def resolve(args, **kwargs):
+ self.assertEqual(args[:2], ['getent', 'ahosts'])
+ self.assertEqual(kwargs['timeout'], 10)
+ return SimpleNamespace(returncode=0, stdout='10.0.0.1 STREAM\n10.0.0.1 DGRAM\n')
+ for topology, count in [('h1', 1), ('h2', 4)]:
+ with self.subTest(topology=topology), patch.object(fleet_node.subprocess, 'run', side_effect=resolve) as run:
+ observed = fleet_node.endpoint_resolution(self.request(topology))
+ self.assertTrue(observed['resolved'] and observed['readOnly'])
+ self.assertFalse(observed['mutated'] or observed['canonicalPassClaimed'] or observed['hostIdentityCertified'])
+ self.assertEqual(run.call_count, count)
+ self.assertEqual(list(observed['entries']), [f'rustfs-node{i}' for i in range(1, count + 1)])
+ self.assertTrue(all(row['addresses'] == ['10.0.0.1'] for row in observed['entries'].values()))
+
+ def test_unresolved_empty_timeout_or_malformed_resolver_output_is_retained_and_refused(self):
+ variants = [SimpleNamespace(returncode=2, stdout=''), SimpleNamespace(returncode=0, stdout=''),
+ SimpleNamespace(returncode=0, stdout='not-an-ip STREAM\n'),
+ subprocess.TimeoutExpired(['getent'], 10), OSError('getent unavailable')]
+ for value in variants:
+ with self.subTest(value=value), patch.object(fleet_node.subprocess, 'run') as run:
+ if isinstance(value, Exception): run.side_effect = value
+ else: run.return_value = value
+ observed = fleet_node.endpoint_resolution(self.request('h1'))
+ self.assertFalse(observed['resolved'])
+ self.assertEqual(run.call_count, 1)
+ self.assertFalse(observed['entries']['rustfs-node1']['resolved'])
+
+ def test_operator_cannot_supply_a_different_hostname_or_command(self):
+ with patch.object(fleet_node.subprocess, 'run') as run:
+ with self.assertRaises(ValueError):
+ fleet_node.endpoint_resolution({**self.request(), 'host': 'other'})
+ with self.assertRaises(ValueError):
+ fleet_node.endpoint_resolution({**self.request(), 'action': 'reserve'})
+ run.assert_not_called()
+
+ def test_controller_retains_all_read_only_results_and_stops_before_reservation(self):
+ good = {'readOnly': True, 'mutated': False, 'resolved': True}
+ bad = {**good, 'resolved': False}
+ for bad_host in ('runner', 'vm002', None):
+ with self.subTest(bad_host=bad_host), tempfile.TemporaryDirectory() as directory:
+ fleet = race.Fleet(['vm000','vm001','vm002','vm003'], Path(directory), 'gh-12345-1-h2')
+ calls = []; records = {}
+ def remote(node, action):
+ calls.append((node, action))
+ return bad if node == bad_host else good
+ fleet.remote = remote
+ fleet.record = lambda label, value: records.setdefault(label, value)
+ with patch.object(race, 'endpoint_resolution', return_value=bad if bad_host == 'runner' else good):
+ if bad_host is None: race.verify_endpoint_resolution(fleet)
+ else:
+ with self.assertRaisesRegex(ValueError, 'name resolution failed'):
+ race.verify_endpoint_resolution(fleet)
+ self.assertEqual(set(calls), {(n,'endpoint-resolution') for n in fleet.nodes})
+ self.assertEqual(set(records['endpoint-resolution']['peers']), set(fleet.nodes))
+ self.assertEqual(fleet.reserved, [])
+
+ def test_real_case_refuses_dns_precondition_before_deploy_seed_or_fault(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ candidate = root / 'candidate.json'
+ candidate.write_text(json.dumps({'source_sha': race.VERSION_CANDIDATE,'package_sha256': 'a' * 64}))
+ rc = root / 'rc'; rc.write_bytes(b'fixture-only')
+ args = SimpleNamespace(evidence=root / 'evidence',candidate=candidate,rc_bin=rc,
+ tool_directory=root,testing_commit='b' * 40,binary_sha='c' * 64,case_id='heal.race-unversioned-overwrite')
+ fleet = Mock(nodes=['vm000'],run_id='gh-12345-1-h1')
+ fleet.record.side_effect = lambda label,value:value
+ fleet.remote.return_value = {'readOnly':True,'mutated':False,'resolved':False}
+ fleet.cleanup.return_value = True
+ with patch.object(race,'Fleet',return_value=fleet), patch.object(race,'verify_inspector',return_value={}), \
+ patch.object(race,'endpoint_resolution',return_value={'readOnly':True,'mutated':False,'resolved':False}), \
+ patch.object(race,'seed_fixture') as seed, patch.dict(race.os.environ,{'GITHUB_RUN_ID':'12345'}):
+ result = race.run_branch(args,'H1',['vm000'],'overwrite-vs-deep')
+ self.assertEqual(result['verdict'],'FAIL')
+ self.assertIn('name resolution failed',result['failure']['message'])
+ fleet.deploy.assert_not_called();seed.assert_not_called()
+ self.assertTrue(all(call.args[1] == 'endpoint-resolution' for call in fleet.remote.call_args_list))
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/test_validation_heal_environment.py b/test_validation_heal_environment.py
new file mode 100644
index 00000000..5c493fad
--- /dev/null
+++ b/test_validation_heal_environment.py
@@ -0,0 +1,454 @@
+import copy
+import json
+from pathlib import Path
+import tempfile
+import types
+import unittest
+from unittest.mock import patch
+
+from scripts.validation_heal import environment
+from scripts.validation_heal import fleet_node
+from scripts.validation_heal import replacement_preflight
+from scripts.validation_heal.healthy import Fleet
+
+
+class EnvironmentTests(unittest.TestCase):
+ def fixture(self, count=4):
+ return {f'n{node}': {'hostname': f'actual-host-{node}', 'inspectionOnly': True, 'mutated': False,
+ 'canonicalPassClaimed': False, 'mounts': [{'target': f'/data/rustfs{number}', 'mounted': number <= count,
+ 'independentBlockDeviceVerified': number <= count, 'writableMount': True,
+ 'physicalDevice': f'8:{number}', 'availableBytes': 4 * 1024**3,
+ 'mount': {'uuid': f'uuid-{node}-{number}', 'target': f'/data/rustfs{number}', 'fstype': 'xfs'}}
+ for number in range(1, 9)]} for node in range(4)}
+
+ def test_four_current_mounts_prove_resources_for_h3_and_do_not_invent_h4(self):
+ result = environment.summarize(self.fixture())
+ self.assertEqual(result['independentEligibleEndpoints'], 16)
+ self.assertTrue(result['resourceShapes']['H3']['mountsAvailable'])
+ self.assertFalse(result['resourceShapes']['H4']['mountsAvailable'])
+ self.assertFalse(result['actualErasureLayoutVerified'])
+ self.assertFalse(result['canonicalPassClaimed'])
+ self.assertFalse(result['resourceShapes']['H6']['platformRestartAuthorityVerified'])
+
+ def test_eight_independent_mounts_are_only_a_resource_shape_not_live_ec84_proof(self):
+ result = environment.summarize(self.fixture(8))
+ self.assertEqual(result['independentEligibleEndpoints'], 32)
+ self.assertTrue(result['resourceShapes']['H4']['mountsAvailable'])
+ self.assertEqual(result['resourceShapes']['H4']['requiredEndpoints'], 24)
+ self.assertFalse(result['faultBackendVerified'])
+ self.assertFalse(result['compatibleReleasePairVerified'])
+
+ def test_partition_aliases_missing_endpoints_and_shared_filesystem_identities_fail(self):
+ for kind in ('partition', 'endpoint', 'shared', 'host', 'mutated', 'pass'):
+ observed = self.fixture()
+ if kind == 'partition': observed['n0']['mounts'][1]['physicalDevice'] = '8:1'
+ elif kind == 'endpoint': observed['n0']['mounts'][-1] = copy.deepcopy(observed['n0']['mounts'][0])
+ elif kind == 'shared': observed['n1']['mounts'][0]['mount']['uuid'] = observed['n0']['mounts'][0]['mount']['uuid']
+ elif kind == 'host': observed['n1']['hostname'] = observed['n0']['hostname']
+ elif kind == 'mutated': observed['n0']['mutated'] = True
+ else: observed['n0']['canonicalPassClaimed'] = True
+ with self.subTest(kind=kind), self.assertRaises(ValueError): environment.summarize(observed)
+
+ def test_directories_stacked_devices_readonly_mounts_or_low_space_do_not_count(self):
+ for kind in ('directory', 'stacked', 'readonly', 'space', 'fs'):
+ observed = self.fixture()
+ row = observed['n0']['mounts'][0]
+ if kind == 'directory': row['mounted'] = False
+ elif kind == 'stacked': row['independentBlockDeviceVerified'] = False
+ elif kind == 'readonly': row['writableMount'] = False
+ elif kind == 'space': row['availableBytes'] = 1024
+ else: row['mount']['fstype'] = 'tmpfs'
+ result = environment.summarize(observed)
+ self.assertEqual(result['independentEligibleEndpoints'], 15)
+ self.assertFalse(result['resourceShapes']['H1']['mountsAvailable'])
+
+ def test_current_remote_adapter_includes_a_compilable_readonly_inventory_action(self):
+ actions = []
+ def invoke(argv, **params):
+ source = params['input']
+ compile(source, 'remote-inventory', 'exec')
+ actions.append(source)
+ self.assertIn('environment_capabilities', source)
+ return types.SimpleNamespace(returncode=0, stdout='{"inspectionOnly":true,"mutated":false}', stderr='')
+ with tempfile.TemporaryDirectory() as name, patch('scripts.validation_heal.healthy.subprocess.run', side_effect=invoke):
+ fleet = Fleet(['n0'], Path(name), 'gh-1234-1-h2')
+ self.assertFalse(fleet.remote('n0', 'environment-capabilities')['mutated'])
+ self.assertFalse(fleet.remote('n0', 'replacement-admission-preflight')['mutated'])
+ self.assertIn('replacement_admission_preflight', actions[-1])
+
+ def test_b03_admission_requires_exact_endpoint_mounts_and_reviewed_switch_adapter(self):
+ roots = [Path(f'/data/rustfs{index}/heal-validation/gh-1234-1-h1') for index in range(1, 5)]
+ ordinary = lambda path: {
+ 'endpoint': str(path), 'canonicalPath': str(path), 'endpointExists': False,
+ 'mounted': False, 'mount': None, 'mountId': None, 'filesystemIdentity': None,
+ 'device': '8:1', 'physicalDeviceIds': ['nvme0n1'],
+ 'nearestExistingAncestor': str(path.parent.parent), 'ancestorMount': None, 'writable': True,
+ }
+ root = {**ordinary(Path('/')), 'endpointExists': True, 'mounted': True, 'mountId': '1',
+ 'filesystemIdentity': '1:0:1', 'physicalDeviceIds': ['nvme-root']}
+ with patch.object(fleet_node, 'paths', return_value=(Path('/state'), roots)), \
+ patch.object(fleet_node, '_replacement_mount_identity', side_effect=lambda path: root if path == Path('/') else ordinary(path)):
+ result = fleet_node.replacement_admission_preflight({'runId': 'gh-1234-1-h1', 'action': 'replacement-admission-preflight'})
+ self.assertFalse(result['admitted'])
+ self.assertFalse(result['mutated'])
+ self.assertFalse(result['checks']['exactEndpointMounts'])
+ self.assertFalse(result['checks']['mountSwitchAdapterReviewed'])
+ self.assertIn('ordinary directories', result['reason'])
+
+ def test_b03_admission_stops_before_parent_mount_preflight_or_start(self):
+ with tempfile.TemporaryDirectory() as name:
+ fleet = Fleet(['n0'], Path(name), 'gh-1234-1-h1', runtime_profile='replacement',
+ require_replacement_admission=True)
+ actions = []
+ fleet.record = lambda label, value: value
+ fleet.remote = lambda node, action, **params: actions.append(action) or {
+ 'readOnly': True, 'mutated': False, 'canonicalPassClaimed': False, 'admitted': False,
+ }
+ with self.assertRaisesRegex(ValueError, 'admission preflight failed'):
+ fleet.deploy('a' * 64, 'rv-heal-' + 'a' * 16, 'access', 'secret')
+ self.assertEqual(actions, ['replacement-admission-preflight'])
+ self.assertEqual(fleet.reserved, [])
+
+ def test_b03_predeployment_gate_is_read_only_and_checks_h1_h2_before_bootstrap(self):
+ check_names = {
+ 'exactEndpointMounts', 'mountIdsPresent', 'filesystemIdentitiesDistinct',
+ 'endpointPhysicalDevicesDistinct', 'endpointDevicesSeparateFromRoot',
+ 'mountSwitchAdapterReviewed',
+ }
+ calls = []
+ class ReadOnlyFleet:
+ def __init__(self, nodes, evidence, run_id):
+ self.nodes, self.evidence, self.run_id = nodes, evidence, run_id
+ self.evidence.mkdir(parents=True, exist_ok=True)
+ def remote(self, node, action):
+ calls.append((self.run_id, node, action))
+ admitted = node != 'vm002'
+ return {'runId': self.run_id, 'readOnly': True, 'mutated': False,
+ 'canonicalPassClaimed': False, 'admitted': admitted,
+ 'checks': {name: admitted for name in check_names}}
+ def record(self, label, value):
+ (self.evidence / (label + '.json')).write_text(json.dumps(value))
+ return value
+ candidate = {'source_sha': 'a' * 40, 'package_sha256': 'b' * 64,
+ 'package_url': 'https://example.invalid/rustfs.deb'}
+ with tempfile.TemporaryDirectory() as folder, patch.object(replacement_preflight, 'Fleet', ReadOnlyFleet):
+ summary = replacement_preflight.run_preflight(['vm000', 'vm001', 'vm002', 'vm003'],
+ Path(folder), '12345', '2', 'c' * 40, candidate)
+ self.assertEqual(summary['status'], 'BLOCKED')
+ self.assertTrue(summary['replacementAdmissionBlocked'])
+ self.assertFalse(summary['mutationStarted'])
+ self.assertEqual(summary['cleanupDisposition'], 'NOT_REQUIRED_NO_MUTATION')
+ self.assertEqual([row['topology'] for row in summary['results']], ['H1', 'H2'])
+ self.assertEqual(len(calls), 5)
+ self.assertTrue(all(action == 'replacement-admission-preflight' for _, _, action in calls))
+ self.assertTrue((Path(folder) / 'healthy-summary.json').is_file())
+
+ def test_b03_predeployment_transport_failure_is_infrastructure_error(self):
+ class ReadOnlyFleet:
+ def __init__(self, nodes, evidence, run_id):
+ self.nodes, self.evidence = nodes, evidence
+ self.evidence.mkdir(parents=True, exist_ok=True)
+ def remote(self, node, action):
+ if node == 'vm002': raise OSError('SSH unavailable')
+ return {'readOnly': True, 'mutated': False, 'canonicalPassClaimed': False,
+ 'admitted': False, 'checks': {'exactEndpointMounts': False,
+ 'mountIdsPresent': False, 'filesystemIdentitiesDistinct': False,
+ 'endpointPhysicalDevicesDistinct': False, 'endpointDevicesSeparateFromRoot': False,
+ 'mountSwitchAdapterReviewed': False}}
+ def record(self, label, value): return value
+ candidate = {'source_sha': 'a' * 40, 'package_sha256': 'b' * 64,
+ 'package_url': 'https://example.invalid/rustfs.deb'}
+ with tempfile.TemporaryDirectory() as folder, patch.object(replacement_preflight, 'Fleet', ReadOnlyFleet):
+ summary = replacement_preflight.run_preflight(['vm000', 'vm001', 'vm002', 'vm003'],
+ Path(folder), '12345', '2', 'c' * 40, candidate)
+ self.assertEqual(summary['status'], 'INFRA_ERROR')
+ self.assertFalse(summary['replacementAdmissionBlocked'])
+ self.assertEqual(summary['infrastructureErrors'][0]['node'], 'vm002')
+
+ def test_b04_reuses_replacement_admission_summary_without_claiming_mutation(self):
+ class ReadOnlyFleet:
+ def __init__(self, nodes, evidence, run_id):
+ self.nodes, self.evidence = nodes, evidence
+ self.evidence.mkdir(parents=True, exist_ok=True)
+ def remote(self, node, action):
+ return {'readOnly': True, 'mutated': False, 'canonicalPassClaimed': False,
+ 'admitted': False, 'checks': {
+ 'exactEndpointMounts': False, 'mountIdsPresent': False,
+ 'filesystemIdentitiesDistinct': False,
+ 'endpointPhysicalDevicesDistinct': True,
+ 'endpointDevicesSeparateFromRoot': True,
+ 'mountSwitchAdapterReviewed': False}}
+ def record(self, label, value): return value
+ candidate = {'source_sha': 'a' * 40, 'package_sha256': 'b' * 64,
+ 'package_url': 'https://example.invalid/rustfs.deb'}
+ with tempfile.TemporaryDirectory() as folder, patch.object(replacement_preflight, 'Fleet', ReadOnlyFleet):
+ summary = replacement_preflight.run_preflight(['vm000', 'vm001', 'vm002', 'vm003'],
+ Path(folder), '12345', '2', 'c' * 40, candidate,
+ 'heal.disk-replacement-unformatted')
+ self.assertEqual(summary['caseId'], 'heal.disk-replacement-unformatted')
+ self.assertEqual(summary['status'], 'BLOCKED')
+ self.assertTrue(summary['replacementAdmissionBlocked'])
+ self.assertFalse(summary['mutationStarted'])
+ self.assertEqual(summary['cleanupDisposition'], 'NOT_REQUIRED_NO_MUTATION')
+
+ def test_b03_runner_preflight_precedes_bootstrap_and_package_activation(self):
+ source = (Path(__file__).parent / 'scripts/validation_heal/run_case.sh').read_text()
+ gate = source.index('replacement_preflight.py')
+ bootstrap = source.index('(bootstrap)')
+ package_activation = source.index('package_active=1', gate)
+ self.assertLess(gate, package_activation)
+ self.assertLess(package_activation, bootstrap)
+ for status in ('BLOCKED', 'INFRA_ERROR'):
+ self.assertLess(source.index('preflight_status}" = ' + status, gate), package_activation)
+ self.assertIn('--predeployment-blocked', source[gate:bootstrap])
+
+ def test_b03_case_result_references_each_topology_preflight_record(self):
+ source = (Path(__file__).parent / 'scripts/validation_heal/run_case.sh').read_text()
+ gate = source.index('replacement_preflight.py')
+ bootstrap = source.index('(bootstrap)')
+ preflight = source[gate:bootstrap]
+ self.assertIn('find "${evidence}/replacement-admission" -type f -name \'*.json\'', preflight)
+ self.assertIn('preflight_relative="${preflight_file#${artifacts}/}"', preflight)
+ self.assertIn('--predeployment-blocked "${preflight_evidence_args[@]}"', preflight)
+ self.assertIn('"${preflight_evidence_args[@]}"', preflight)
+
+ def test_b04_runner_and_executor_use_the_same_replacement_admission_contract(self):
+ source = (Path(__file__).parent / 'scripts/validation_heal/run_case.sh').read_text()
+ gate = source.index('replacement_preflight.py')
+ bootstrap = source.index('(bootstrap)')
+ self.assertIn('heal.disk-replacement-unformatted) driver=unformatted.py; replacement_admission_gate=1', source)
+ self.assertIn('--case-id "${case_id}"', source[gate:bootstrap])
+ self.assertLess(gate, bootstrap)
+ self.assertIn('replacement admission did not permit deployment or fixture seed', source)
+ self.assertIn('reservation-time recheck blocked the case', source)
+ executor = (Path(__file__).parent / 'scripts/validation_heal/unformatted.py').read_text()
+ self.assertIn('require_replacement_admission=True', executor)
+
+ def runtime_command(self, *, units='', listeners='', state=None):
+ state = state or 'LoadState=loaded\nActiveState=active\nSubState=running\nMainPID=42\n'
+ def invoke(*args, **params):
+ if args[:2] == ('systemctl', 'show'): output = state
+ elif args[:2] == ('systemctl', 'list-units'): output = units
+ elif args[0] == 'ss': output = listeners
+ else: self.fail(f'Unexpected runtime command: {args}')
+ return types.SimpleNamespace(returncode=0, stdout=output, stderr='')
+ return invoke
+
+ def test_runtime_records_service_and_port_without_exposing_process_arguments(self):
+ command = self.runtime_command(
+ units='rustfs-validation-heal-gh-123-1-h2.service loaded inactive dead private fixture\n',
+ listeners='LISTEN 0 4096 [::]:9000 [::]:* users:(("private-label",pid=42,fd=7))\n')
+ with patch.object(fleet_node, 'command', side_effect=command) as calls:
+ result = fleet_node.environment_service_state()
+ self.assertEqual(result['standardService']['MainPID'], 42)
+ self.assertEqual(result['port9000Listeners'], [{'localAddress': '[::]:9000', 'pids': [42]}])
+ self.assertEqual(result['loadedValidationUnits'][0]['active'], 'inactive')
+ self.assertNotIn('private-label', str(result))
+ self.assertNotIn('private fixture', str(result))
+ self.assertEqual(len(calls.call_args_list), 6)
+ for field in ('mutated', 'cleanStateCertified', 'globalWorkflowIdleCertified', 'retainedSceneRecoveryCertified'):
+ self.assertFalse(result[field])
+
+ def test_idle_runtime_and_unattributed_listener_never_certify_clean_state(self):
+ for listener in ('', 'LISTEN 0 4096 0.0.0.0:9000 0.0.0.0:*\n'):
+ with self.subTest(listener=listener), patch.object(fleet_node, 'command', side_effect=self.runtime_command(
+ state='LoadState=not-found\nActiveState=inactive\nSubState=dead\nMainPID=0\n', listeners=listener)):
+ result = fleet_node.environment_service_state()
+ self.assertFalse(result['cleanStateCertified'])
+ self.assertEqual(result['standardService']['MainPID'], 0)
+ if listener: self.assertEqual(result['port9000Listeners'][0]['pids'], [])
+
+ def test_runtime_refuses_partial_failed_ambiguous_or_unrecognized_observations(self):
+ scenarios = [
+ self.runtime_command(state='LoadState=loaded\nMainPID=42\n'),
+ self.runtime_command(units='rustfs-validation-heal-gh-123-1-h2.service loaded active running\n' * 2),
+ self.runtime_command(units='rustfs-validation-heal-unrecognized.service loaded active running\n'),
+ self.runtime_command(listeners='LISTEN 0 4096 0.0.0.0:9001 0.0.0.0:*\n'),
+ ]
+ for command in scenarios:
+ with patch.object(fleet_node, 'command', side_effect=command), self.assertRaises(ValueError):
+ fleet_node.environment_service_state()
+ failed = types.SimpleNamespace(returncode=1, stdout='LoadState=loaded\nActiveState=active\nSubState=running\nMainPID=42\n', stderr='')
+ with patch.object(fleet_node, 'command', return_value=failed), self.assertRaises(ValueError):
+ fleet_node.environment_service_state()
+
+ def test_runtime_refuses_service_or_listener_changes_between_observations(self):
+ for changing in ('service', 'listener', 'validation-unit'):
+ command = self.runtime_command()
+ counts = {}
+ def invoke(*args, **params):
+ result = command(*args, **params)
+ counts[args] = counts.get(args, 0) + 1
+ if counts[args] == 2:
+ if changing == 'service' and args[:2] == ('systemctl', 'show'):
+ result.stdout = result.stdout.replace('MainPID=42', 'MainPID=43')
+ elif changing == 'listener' and args[0] == 'ss':
+ result.stdout = 'LISTEN 0 4096 0.0.0.0:9000 0.0.0.0:*\n'
+ elif changing == 'validation-unit' and args[:2] == ('systemctl', 'list-units'):
+ result.stdout = 'rustfs-validation-heal-gh-123-1-h2.service loaded active running\n'
+ return result
+ with self.subTest(changing=changing), patch.object(fleet_node, 'command', side_effect=invoke), self.assertRaises(ValueError):
+ fleet_node.environment_service_state()
+
+ def test_legacy_mount_only_inventory_does_not_imply_runtime_or_clean_proof(self):
+ result = environment.summarize(self.fixture())
+ self.assertFalse(result['runtimeObservationsComplete'])
+ self.assertFalse(result['cleanStateCertified'])
+
+
+class I05PlatformFilterTests(unittest.TestCase):
+ def test_i05_checks_every_peer_tool_before_reserving_any_namespace(self):
+ with tempfile.TemporaryDirectory() as folder:
+ fleet = Fleet(['vm000', 'vm001', 'vm002', 'vm003'], Path(folder), 'gh-123-1-h2',
+ runtime_profile='lock-timeout')
+ def remote(node, action, **params):
+ self.assertEqual(action, 'preflight')
+ self.assertEqual(params['runtimeProfile'], 'lock-timeout')
+ if node == 'vm002': raise ValueError('reviewed tool unavailable')
+ return {'hostname': node}
+ with patch.object(fleet, 'remote', side_effect=remote) as calls, \
+ self.assertRaisesRegex(ValueError, 'tool unavailable'):
+ fleet.deploy('a' * 64, 'rv-heal-' + 'a' * 16, 'access', 'secret')
+ self.assertEqual([call.args[0] for call in calls.call_args_list], ['vm000', 'vm001', 'vm002'])
+ self.assertEqual(fleet.reserved, [])
+
+ def test_tool_inventory_keeps_broken_kernel_wrapper_separate_from_working_binary(self):
+ wrapper = Path('/usr/sbin/bpftool')
+ binary = Path('/usr/lib/linux-tools/6.17.0-1020-azure/bpftool')
+ calls = []
+ def invoke(*args, **kwargs):
+ calls.append(args)
+ failed = args[0] == str(wrapper)
+ return types.SimpleNamespace(returncode=1 if failed else 0,
+ stdout='' if failed else ('bpftool v7.6.0' if args[1] == '--version' else '[]'),
+ stderr='bpftool not found for kernel 6.17.0-1022' if failed else '')
+ with patch.object(fleet_node.shutil, 'which', return_value=str(wrapper)), \
+ patch.object(Path, 'glob', return_value=[binary]), \
+ patch.object(Path, 'resolve', autospec=True, side_effect=lambda path, **kw: path), \
+ patch.object(Path, 'is_file', return_value=True), \
+ patch.object(Path, 'stat', return_value=types.SimpleNamespace(st_uid=0, st_mode=0o755)), \
+ patch.object(fleet_node, 'digest', return_value='a' * 64), \
+ patch.object(fleet_node, 'command', side_effect=invoke):
+ result = fleet_node.bpf_tool_inventory()
+ self.assertEqual(result['selectedPath'], str(wrapper))
+ self.assertEqual([row['version']['returncode'] for row in result['tools']], [1, 0])
+ self.assertEqual([row['rootCgroupQuery']['jsonListObserved'] for row in result['tools']], [False, True])
+ self.assertTrue(all(row['serviceAttachmentsVerified'] is False for row in result['tools']))
+ self.assertFalse(result['mutated'])
+ self.assertEqual(calls, [(str(path), *args) for path in (wrapper, binary) for args in (
+ ('--version',), ('-j', 'cgroup', 'show', '/sys/fs/cgroup'))])
+
+ def owner(self):
+ return {'runtimeProfile': 'lock-timeout', 'runId': 'gh-123-1-h1',
+ 'unit': 'rustfs-validation-heal-gh-123-1-h1.service'}
+
+ def test_only_i05_private_unit_receives_filter_and_missing_backend_refuses(self):
+ with patch.object(fleet_node, 'i05_bpf_tool', return_value={'path': 'reviewed'}), \
+ patch.object(fleet_node, 'paths'), \
+ patch.object(Path, 'is_file', return_value=True):
+ properties = fleet_node.i05_filter_properties(self.owner())
+ self.assertEqual(properties, ['--property=IPAddressDeny=168.63.129.16/32',
+ '--property=IPAddressAllow='])
+ with self.assertRaises(ValueError):
+ fleet_node.i05_filter_properties({**self.owner(), 'unit': 'rustfs.service'})
+ with patch.object(fleet_node, 'i05_bpf_tool', side_effect=ValueError('missing reviewed tool')):
+ with self.assertRaises(ValueError): fleet_node.i05_filter_properties(self.owner())
+ self.assertEqual(fleet_node.i05_filter_properties({'runtimeProfile': 'manual'}), [])
+
+ def test_reviewed_tool_requires_digest_version_and_protected_executable(self):
+ for variant in ('valid', 'digest', 'version', 'owner', 'writable', 'executable'):
+ with self.subTest(variant=variant), \
+ patch.object(Path, 'resolve', autospec=True, side_effect=lambda path, **kw: path), \
+ patch.object(Path, 'is_file', return_value=True), \
+ patch.object(Path, 'stat', return_value=types.SimpleNamespace(
+ st_uid=1000 if variant == 'owner' else 0, st_mode=0o777 if variant == 'writable' else 0o755)), \
+ patch.object(fleet_node.os, 'access', return_value=variant != 'executable'), \
+ patch.object(fleet_node, 'digest', return_value='0' * 64 if variant == 'digest'
+ else fleet_node.I05_BPFTOOL_SHA256), \
+ patch.object(fleet_node, 'command', return_value=types.SimpleNamespace(
+ stdout='unexpected' if variant == 'version' else 'bpftool v7.7.0\nusing libbpf v1.7\nfeatures:\n')):
+ if variant == 'valid':
+ result = fleet_node.i05_bpf_tool()
+ self.assertEqual(result['sha256'], fleet_node.I05_BPFTOOL_SHA256)
+ self.assertFalse(result['serviceAttachmentsVerified'])
+ else:
+ with self.assertRaises(ValueError): fleet_node.i05_bpf_tool()
+
+ def policy(self, variant=None):
+ owner = self.owner()
+ group = '/system.slice/' + owner['unit']
+ values = {'IPAddressDeny': '168.63.129.16/32', 'IPAddressAllow': '',
+ 'ControlGroup': group, 'MainPID': '42'}
+ rows = [{'id': i, 'attach_type': direction, 'name': 'sd_fw_' + direction, 'attach_flags': 'multi'}
+ for i, direction in enumerate(('ingress', 'egress'), 1)]
+ programs = {row['id']: {'id': row['id'], 'name': row['name'], 'type': 'cgroup_skb',
+ 'uid': 0, 'tag': 'a' * 16} for row in rows}
+ if variant == 'libbpf-names':
+ for row in rows: row['attach_type'] = 'cgroup_inet_' + row['attach_type']
+ elif variant == 'duplicate-direction': rows[1]['attach_type'] = 'cgroup_inet_ingress'
+ elif variant == 'foreign-direction': rows[0]['attach_type'] = 'cgroup_inet_sock_create'
+ elif variant == 'invalid-direction': rows[0]['attach_type'] = ['ingress']
+ elif variant == 'flags-array': rows[0]['attach_flags'] = ['multi']
+ elif variant == 'ignored-property': values['IPAddressDeny'] = ''
+ elif variant == 'allow': values['IPAddressAllow'] = 'any'
+ elif variant == 'foreign-cgroup': values['ControlGroup'] = '/system.slice/rustfs.service'
+ elif variant == 'pid': values['MainPID'] = '43'
+ elif variant == 'unsupported-kernel': rows = []
+ elif variant == 'missing-egress': rows.pop()
+ elif variant == 'foreign-program': rows[0]['name'] = 'foreign'
+ elif variant == 'duplicate-id': rows[1]['id'] = rows[0]['id']
+ elif variant == 'override': rows[0]['attach_flags'] = 'override'
+ elif variant == 'program-id': programs[1]['id'] = 999
+ elif variant == 'program-type': programs[1]['type'] = 'socket_filter'
+ elif variant == 'program-owner': programs[1]['uid'] = 1000
+ elif variant == 'program-tag': programs[1]['tag'] = ''
+ elif variant == 'query-failed': pass
+ def invoke(*args, **kwargs):
+ if args[:2] == ('systemctl', 'show'):
+ raw = '\n'.join(k + '=' + v for k, v in values.items())
+ elif args[:4] == ('/reviewed/bpftool', '-j', 'cgroup', 'show'):
+ if variant == 'query-failed': raise ValueError('kernel query refused')
+ raw = json.dumps(rows)
+ elif args[:5] == ('/reviewed/bpftool', '-j', 'prog', 'show', 'id'):
+ # Both bpftool versions' single-program JSON forms are accepted.
+ program = programs[int(args[5])]
+ raw = json.dumps([program] if int(args[5]) == 1 else program)
+ else: self.fail(f'Unexpected filter command: {args}')
+ return types.SimpleNamespace(stdout=raw)
+ membership = '0::' + (group if variant != 'process-cgroup' else '/foreign')
+ with patch.object(fleet_node, 'command', side_effect=invoke) as command, \
+ patch.object(fleet_node, 'i05_bpf_tool', return_value={'resolvedPath': '/reviewed/bpftool'}), \
+ patch.object(fleet_node, 'paths'), \
+ patch.object(Path, 'read_text', return_value=membership + '\n'), \
+ patch.object(Path, 'is_dir', return_value=variant != 'missing-cgroup'), \
+ patch.object(Path, 'resolve', autospec=True, side_effect=lambda path, **kw:
+ Path('/foreign') if variant == 'aliased-cgroup' else path):
+ result = fleet_node.i05_filter_policy(owner, 42)
+ return result, command
+
+ def test_effective_properties_require_actual_process_bound_kernel_programs(self):
+ result, calls = self.policy()
+ self.assertEqual(result['deniedAddress'], '168.63.129.16/32')
+ self.assertEqual({row['attach_type'] for row in result['kernelPrograms']}, {'ingress', 'egress'})
+ self.assertFalse(result['hostFirewallChanged'])
+ self.assertFalse(result['persistentNetworkSettingsChanged'])
+ self.assertTrue(result['snapshotOnly'])
+ self.assertEqual([call.args[0] for call in calls.call_args_list],
+ ['systemctl', '/reviewed/bpftool', '/reviewed/bpftool', '/reviewed/bpftool'])
+
+ def test_libbpf_attach_names_keep_raw_kernel_evidence_and_same_directions(self):
+ result, _ = self.policy('libbpf-names')
+ self.assertEqual({row['attach_type'] for row in result['kernelPrograms']},
+ {'cgroup_inet_ingress', 'cgroup_inet_egress'})
+ self.assertEqual({row['direction'] for row in result['kernelPrograms']}, {'ingress', 'egress'})
+
+ def test_configured_properties_do_not_hide_missing_foreign_or_changed_kernel_filter(self):
+ for variant in ('ignored-property', 'allow', 'foreign-cgroup', 'pid', 'unsupported-kernel',
+ 'missing-egress', 'foreign-program', 'duplicate-id', 'override', 'program-id',
+ 'program-type', 'program-owner', 'program-tag', 'query-failed',
+ 'process-cgroup', 'missing-cgroup', 'aliased-cgroup',
+ 'duplicate-direction', 'foreign-direction', 'invalid-direction', 'flags-array'):
+ with self.subTest(variant=variant), self.assertRaises(ValueError): self.policy(variant)
+
+
+if __name__ == '__main__': unittest.main()
diff --git a/test_validation_heal_environment_gate.py b/test_validation_heal_environment_gate.py
new file mode 100644
index 00000000..5ece294c
--- /dev/null
+++ b/test_validation_heal_environment_gate.py
@@ -0,0 +1,347 @@
+import copy
+from datetime import datetime, timedelta, timezone
+import hashlib
+import json
+import os
+from pathlib import Path
+import subprocess
+import sys
+import tempfile
+import types
+import unittest
+from unittest.mock import patch
+
+from scripts.validation_heal.environment_gate import decision, write_blocked
+from scripts.validation_heal.execution_policy import (
+ CAPABILITIES_ENV,
+ ENABLE_ENV,
+ ENVIRONMENT_GATED_CASES,
+ EVIDENCE_BLOCKED_CASES,
+)
+from scripts.validation_heal.matrix import AUTOMATIC_CASES, DISPATCHABLE_CASES, cases
+from scripts.validation_report import render_report, validate_results
+from scripts.validation_heal import environment_cases as extended
+from scripts.validation_heal.matrix import select, variant_topologies
+
+
+class EnvironmentGateTest(unittest.TestCase):
+ def test_first_and_second_batches_are_disjoint_and_classified(self):
+ self.assertEqual(len(ENVIRONMENT_GATED_CASES), 13)
+ self.assertEqual(len(EVIDENCE_BLOCKED_CASES), 4)
+ self.assertTrue(set(ENVIRONMENT_GATED_CASES).isdisjoint(EVIDENCE_BLOCKED_CASES))
+ self.assertTrue(set(ENVIRONMENT_GATED_CASES) <= DISPATCHABLE_CASES)
+ self.assertFalse(set(EVIDENCE_BLOCKED_CASES) & DISPATCHABLE_CASES)
+ self.assertEqual(len(AUTOMATIC_CASES), 67)
+
+ def test_recipes_carry_policy_without_changing_source_case_count(self):
+ inventory = {item["id"]: item for item in cases()}
+ for case_id, policy in {**ENVIRONMENT_GATED_CASES, **EVIDENCE_BLOCKED_CASES}.items():
+ self.assertEqual(inventory[case_id]["executionPolicy"], policy)
+ self.assertEqual(sum(item.get("executionPolicy", {}).get("kind") == "environment-gated" for item in inventory.values()), 13)
+ self.assertEqual(sum(item.get("executionPolicy", {}).get("kind") == "evidence-blocked" for item in inventory.values()), 4)
+
+ def test_default_gate_is_blocked_without_touching_fleet(self):
+ case_id = "heal.mixed-version-upgrade-rollback"
+ result = decision(case_id, environ={})
+ self.assertEqual(result["state"], "disabled")
+ self.assertEqual(result["reasonCode"], "ENV_DISABLED_BY_DEFAULT")
+ self.assertFalse(result["enabled"])
+ self.assertEqual(set(result["missingCapabilities"]), set(ENVIRONMENT_GATED_CASES[case_id]["requiredCapabilities"]))
+
+ def test_explicit_gate_requires_every_capability(self):
+ case_id = "heal.ec84-multi-set-restart"
+ result = decision(case_id, environ={ENABLE_ENV: "1"}, capabilities=[])
+ self.assertEqual(result["state"], "blocked")
+ self.assertEqual(result["reasonCode"], "ENV_CAPABILITY_MISSING")
+ self.assertTrue(result["missingCapabilities"])
+
+ def test_capability_manifest_alone_needs_bound_runtime(self):
+ case_id = "heal.kms-and-tier-outage"
+ policy = ENVIRONMENT_GATED_CASES[case_id]
+ result = decision(case_id, environ={ENABLE_ENV: "1"}, capabilities=policy["requiredCapabilities"])
+ self.assertEqual(result["state"], "ready")
+ self.assertEqual(result["reasonCode"], "ENV_RUNTIME_REQUIRED")
+
+ def test_blocked_result_is_reportable_with_reason_and_evidence(self):
+ case_id = "heal.race-ilm-expiry-and-transition"
+ with tempfile.TemporaryDirectory() as tmp:
+ root = Path(tmp)
+ candidate = root / "candidate.json"
+ candidate.write_text(json.dumps({"package_url": "https://example.invalid/rustfs.deb", "package_sha256": "a" * 64}))
+ artifacts = root / "artifacts"
+ result = write_blocked(case_id, artifacts, candidate=candidate, testing_commit="b" * 40, environ={})
+ self.assertEqual(result["verdict"], "BLOCKED")
+ self.assertEqual(result["execution_policy"]["kind"], "environment-gated")
+ self.assertFalse(json.loads((artifacts / "evidence/environment-gate.json").read_text())["fleetTouched"])
+ catalog = json.loads(Path("validation-cases.json").read_text())
+ rows, errors = validate_results(catalog, artifacts / "cases", gate_statuses=set())
+ self.assertFalse(errors, errors)
+ self.assertEqual(next(row for row in rows if row["case_id"] == case_id)["verdict"], "BLOCKED")
+
+ def test_dispatchers_include_all_environment_gated_cases(self):
+ for path in (Path("scripts/run_validation_case.sh"), Path("scripts/validation_heal/run_case.sh")):
+ text = path.read_text()
+ for case_id in ENVIRONMENT_GATED_CASES:
+ self.assertIn(case_id, text)
+
+
+class ExtendedScenariosTest(unittest.TestCase):
+ def binding(self):
+ return {"schemaVersion": 1, "runId": "manual-environment-test", "jumpHostOnly": True,
+ "exclusiveHealService": True, "leaseExpiresAt": (datetime.now(timezone.utc) + timedelta(days=10)).isoformat(),
+ "bucketPrefix": "rv-heal-environment-test", "candidateCommit": "a" * 40, "harnessCommit": "b" * 40,
+ "candidatePackageSha256": "c" * 64, "candidateBinarySha256": "d" * 64, "rcBinarySha256": "e" * 64,
+ "endpoints": {"node1": "http://localhost:19000"}, "defaultEndpoint": "node1",
+ "maxInventoryObjects": 10000, "maxInventoryBytes": 8 * 1024**3}
+
+ def topology(self, code="H1"):
+ count = 24 if code == "H4" else 12 if code == "H3" else 4
+ disks = [{"hostname": "node1", "mountDevice": str(index), "diskUUID": "disk-" + str(index),
+ "poolIndex": 0, "setIndex": index // 12 if code in {"H3", "H4"} else 0,
+ "setSlot": index % 12 if code in {"H3", "H4"} else index,
+ "dataShards": 8 if code in {"H3", "H4"} else 2,
+ "parityShards": 4 if code in {"H3", "H4"} else 2} for index in range(count)]
+ return {"disks": disks, "processes": [{"hostname": "node1", "binarySha256": "d" * 64, "pid": 1234}]}
+
+ def test_every_source_branch_has_executable_control_flow_or_explicit_native_block(self):
+ templates = []
+ for case_id, policy in ENVIRONMENT_GATED_CASES.items():
+ recipe = select(case_id)[0]
+ operations = extended.required_operations(case_id)
+ self.assertTrue({"preflight", "prepare", "recover", "cleanup"} <= operations)
+ mapping = variant_topologies(recipe)
+ for branch in recipe["variants"]:
+ templates.extend((case_id, topology, branch) for topology in mapping[branch])
+ if branch in extended.NATIVE_BRANCHES: continue
+ spec = extended.scenario(policy["code"], branch)
+ self.assertTrue(set(spec.actions) <= operations)
+ if policy["code"] != "J05": self.assertTrue(spec.actions)
+ self.assertEqual(len(templates), len(set(templates)))
+ h7 = [row for row in templates if row[0] == "heal.ec84-multi-set-restart"]
+ self.assertEqual(len(h7), 4)
+
+ def test_default_dispatch_all_thirteen_never_invokes_ssh_or_extended_runtime(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ root = Path(tmp)
+ forbidden = root / "ssh-called"
+ ssh = root / "ssh"
+ ssh.write_text("#!/bin/sh\ntouch '" + str(forbidden) + "'\nexit 99\n")
+ ssh.chmod(0o755)
+ candidate = root / "candidate.json"; candidate.write_text("{}")
+ environ = {**os.environ, "PATH": str(root) + os.pathsep + os.environ["PATH"],
+ ENABLE_ENV: "0", CAPABILITIES_ENV: str(root / "invalid-capabilities"),
+ extended.RUNTIME_ENV: "/must-not-read-runtime"}
+ for case_id in ENVIRONMENT_GATED_CASES:
+ with self.subTest(case=case_id):
+ artifacts = root / case_id
+ command = ["bash", "scripts/validation_heal/run_case.sh", "--validation-case", case_id,
+ "--candidate-manifest", str(candidate), "--testing-commit", "b" * 40,
+ "--validation-artifacts-dir", str(artifacts)]
+ result = subprocess.run(command, env=environ, capture_output=True, text=True)
+ self.assertEqual(result.returncode, 0, result.stderr)
+ value = json.loads((artifacts / "cases" / (case_id + ".json")).read_text())
+ self.assertEqual(value["verdict"], "BLOCKED")
+ self.assertEqual(value["migration_status"], "unverified")
+ self.assertIn("ENV_DISABLED_BY_DEFAULT", value["reason"])
+ self.assertFalse(forbidden.exists())
+
+ def test_selected_blocked_case_fails_canonical_gate_and_report_shows_reason(self):
+ case_id = "heal.slow-io-and-eio"
+ with tempfile.TemporaryDirectory() as tmp:
+ artifacts = Path(tmp)
+ write_blocked(case_id, artifacts, candidate=None, testing_commit=None, environ={})
+ catalog = json.loads(Path("validation-cases.json").read_text())
+ rows, errors = validate_results(catalog, artifacts / "cases", gate_statuses=set(), gate_cases={case_id})
+ self.assertEqual(errors, [case_id + ": gate verdict is 'BLOCKED'"])
+ self.assertIn("ENV_DISABLED_BY_DEFAULT", render_report(rows, errors))
+ self.assertIn("专用 faultfs", render_report(rows, errors))
+
+ def test_runtime_requires_all_topologies_candidate_and_adapter_digest_before_commands(self):
+ case_id = "heal.slow-io-and-eio"
+ with tempfile.TemporaryDirectory() as tmp:
+ root = Path(tmp); rc = root / "rc"; rc.write_text("fixture rc")
+ adapter = root / "adapter"; adapter.write_text("#!/bin/sh\nexit 99\n"); adapter.chmod(0o755)
+ binding = self.binding(); binding["rcBinarySha256"] = hashlib.sha256(rc.read_bytes()).hexdigest()
+ runtime = {"schemaVersion": 1, "provider": "auto-testing", "binding": binding, "rcBinary": str(rc),
+ "sshNodes": os.environ.get("RUSTFS_NODES", "vm000 vm001 vm002 vm003").split(),
+ "topologies": ["H1", "H2"], "inspectorSha256": "f" * 64, "faultDeviceId": "device", "capabilities": ENVIRONMENT_GATED_CASES[case_id]["requiredCapabilities"],
+ "adapter": {"executable": str(adapter), "sha256": hashlib.sha256(adapter.read_bytes()).hexdigest(),
+ "operations": sorted(extended.required_operations(case_id))}}
+ candidate = {"source_sha": "a" * 40, "package_sha256": "c" * 64}
+ self.assertEqual(extended.runtime_binding(runtime, case_id, candidate, "b" * 40), binding)
+ for kind in ("candidate", "topology", "adapter", "capability", "provider", "nodes"):
+ changed = copy.deepcopy(runtime)
+ if kind == "candidate": changed["binding"]["candidateCommit"] = "f" * 40
+ elif kind == "topology": changed["topologies"] = ["H1"]
+ elif kind == "adapter": changed["adapter"]["sha256"] = "f" * 64
+ elif kind == "capability": changed["capabilities"] = []
+ elif kind == "provider": changed["provider"] = "source-validation-lab"
+ else: changed["sshNodes"] = ["unrelated-node"]
+ with self.subTest(kind=kind), self.assertRaises(ValueError):
+ extended.runtime_binding(changed, case_id, candidate, "b" * 40)
+
+ def test_physical_topology_rejects_directory_aliases_missing_sets_and_wrong_candidate(self):
+ extended.assert_topology(self.topology("H4"), "H4", "d" * 64)
+ for kind in ("alias", "slot", "count", "coding", "candidate"):
+ observed = self.topology("H4")
+ if kind == "alias": observed["disks"][1]["mountDevice"] = "0"
+ elif kind == "slot": observed["disks"][1]["setSlot"] = 0
+ elif kind == "count": observed["disks"].pop()
+ elif kind == "coding": observed["disks"][0]["dataShards"] = 2
+ else: observed["processes"][0]["binarySha256"] = "f" * 64
+ with self.subTest(kind=kind), self.assertRaises(ValueError): extended.assert_topology(observed, "H4", "d" * 64)
+
+ def test_overlap_requires_actual_original_task_and_common_activity_intersection(self):
+ value = {"clockDomain": "server-1", "intervals": [
+ {"kind": "heal", "taskId": "original", "startNs": 100, "endNs": 200, "evidenceId": "h"},
+ {"kind": "pool", "taskId": "move", "startNs": 150, "endNs": 300, "evidenceId": "p"}]}
+ extended.assert_overlap(value, task_id="original", kind="pool")
+ value["intervals"][1]["startNs"] = 201
+ with self.assertRaises(extended.NotObserved): extended.assert_overlap(value, task_id="original", kind="pool")
+ value["clockDomain"] = ""
+ with self.assertRaises(ValueError): extended.assert_overlap(value, task_id="original", kind="pool")
+
+ def terminal(self):
+ expected = [{"key": "key", "versionId": "v1", "deleteMarker": False}]
+ status = {"summary": "finished", "outcome": {"coverage": "complete", "objectsTruncated": False,
+ "execution": {"state": "completed"}, "counters": {"processed": 1, "healed": 1, "unchanged": 0, "skipped": 0,
+ "failed": 0, "unknown": 0, "attemptFailures": 0, "overflowed": False}, "objects": [
+ {"identity": {"bucket": "bucket", "object": "key", "versionId": "v1", "bucketIncarnationId": "epoch", "poolIndex": 0, "setIndex": 0},
+ "disposition": {"state": "repaired"}}]}}
+ return status, expected
+
+ def test_terminal_denominator_cannot_be_defined_by_returned_receipts_or_legacy_finish(self):
+ status, expected = self.terminal(); extended.assert_terminal(status, "bucket", expected)
+ for kind in ("missing", "wrong-version", "unknown", "metadata-only", "counter", "incarnation", "boolean-set"):
+ changed = copy.deepcopy(status); row = changed["outcome"]["objects"][0]
+ if kind == "missing": changed["outcome"]["objects"] = []
+ elif kind == "wrong-version": row["identity"]["versionId"] = "foreign"
+ elif kind == "unknown": row["disposition"]["state"] = "unknown"
+ elif kind == "metadata-only": row["disposition"]["state"] = "metadata_healthy"
+ elif kind == "counter": changed["outcome"]["counters"]["healed"] = 0
+ elif kind == "incarnation": row["identity"]["bucketIncarnationId"] = ""
+ else: row["identity"]["setIndex"] = True
+ with self.subTest(kind=kind), self.assertRaises(ValueError): extended.assert_terminal(changed, "bucket", expected)
+
+ def test_enabled_io_branch_runs_all_oracles_then_requires_review(self):
+ from test_validation_heal import HealShardOracleTest
+ original = HealShardOracleTest().snapshot()
+ bucket = "rv-heal-0123456789abcdef"
+ binding = self.binding(); binding["bucketPrefix"] = "rv-heal-0123456789"
+ def remap(bucket_name):
+ scene = copy.deepcopy(original)
+ for row in scene["node1"]["files"]:
+ row["path"] = row["path"].replace(bucket, bucket_name)
+ if "metadata" in row: row["metadata"]["bucket"] = bucket_name
+ return scene
+ class FakeAdapter:
+ def __init__(self): self.calls = []; self.injected = False; self.repaired = False; self.scene = None
+ def record(self, label, value): return value
+ def call(self, operation, **params):
+ self.calls.append(operation)
+ name = params.get("bucket")
+ if operation == "preflight": return ExtendedScenariosTest().topology()
+ if operation == "snapshot":
+ if self.scene is None: self.scene = remap(name)
+ scene = copy.deepcopy(self.scene)
+ if self.injected and not self.repaired:
+ scene["node1"]["files"] = [r for r in scene["node1"]["files"] if not (r["disk"] == self.target["disk"] and r["path"] == self.target["relativePath"])]
+ return {"snapshot": scene}
+ if operation == "apply-fault":
+ self.target = params["target"]; self.injected = True
+ return {"kind": "missing", "relativePath": self.target["relativePath"], "before": {"sha256": self.target["sha256"], "size": self.target["size"]}, "fault": None}
+ if operation == "checkpoint": return {"records": [{"taskId": "task", "bucketIncarnationId": "epoch", "sha256": "c" * 64, "durableProgress": 1}]}
+ if operation == "io-eio": return {"probe": {"uid": 1, "mountNamespace": "ns", "path": "/owned", "errno": 5}, "serviceUid": 1, "serviceMountNamespace": "ns", "deviceId": "device"}
+ if operation == "io-window": return {"clockDomain": "server", "intervals": [{"kind": "heal", "taskId": "task", "startNs": 1, "endNs": 4, "evidenceId": "h"}, {"kind": "io", "taskId": "fault", "startNs": 2, "endNs": 3, "evidenceId": "f"}]}
+ if operation == "io-recover": self.repaired = True
+ if operation == "verify-parts": return {"toolSha256": "f" * 64, "candidateCommit": "a" * 40, "parts": [{"key": "objects/0008.bin", "versionId": "null", "sha256": "part", "checksumVerified": True}]}
+ if operation == "cleanup": return {"remainingOwnedPaths": [], "activeOwnedServices": [], "remainingFaults": [], "pendingMutations": []}
+ return {}
+ class FakeProbe:
+ def __init__(self, binding, endpoint=None): self.binding = binding
+ def inventory(self, bucket, **params): return {"bucket": bucket, "entries": [{"key": "objects/0008.bin", "versionId": "null", "deleteMarker": False, "isLatest": True, "payload": {"size": 100, "sha256": "payload"}}]}
+ def admin(self, path, query, body):
+ if not query: return {"httpStatus": 200, "response": {"clientToken": "task"}}
+ status, expected = ExtendedScenariosTest().terminal()
+ row = status["outcome"]["objects"][0]["identity"]
+ from urllib.parse import unquote
+ row.update(bucket=unquote(path.rsplit("/", 1)[-1]), object="objects/0008.bin", versionId="null")
+ return {"httpStatus": 200, "response": status}
+ with tempfile.TemporaryDirectory() as tmp:
+ adapter = FakeAdapter()
+ runtime = {"binding": binding, "faultDeviceId": "device", "inspectorSha256": "f" * 64}
+ runner = extended.Runner(runtime, {}, "b" * 40, Path(tmp), adapter=adapter, probe_factory=FakeProbe)
+ def fixture(probe, directory, bucket, spec, branch):
+ baseline = probe.inventory(bucket)
+ return object(), copy.deepcopy(baseline["entries"]), baseline
+ with patch.object(runner, "fixture", side_effect=fixture):
+ row = runner.branch("heal.slow-io-and-eio", "H1", "eio")
+ self.assertEqual(row["cleanup"], "PASS", row)
+ self.assertTrue(row.get("checksCompleted"), row)
+ self.assertEqual(row["verdict"], "BLOCKED")
+ self.assertLess(adapter.calls.index("verify-parts"), adapter.calls.index("cleanup"))
+ for operation in ("apply-fault", "checkpoint", "io-eio", "io-window", "io-recover", "verify-parts", "recover", "cleanup"):
+ self.assertIn(operation, adapter.calls)
+
+ def test_power_restart_and_io_error_cannot_be_substituted_with_other_faults(self):
+ with self.assertRaises(ValueError): extended.assert_action("dedicated-host-power-cycle", {"eventKind": "SIGKILL"}, task_id="task", runtime={"hostResourceId": "dedicated"})
+ with self.assertRaises(ValueError): extended.assert_action("crash-service", {"pidBefore": 1, "pidAfter": 2, "rootBefore": "/a", "rootAfter": "/b", "signal": "SIGKILL"}, task_id="task", runtime={})
+ with self.assertRaises(ValueError): extended.assert_action("io-eio", {"probe": {"uid": 1, "mountNamespace": "ns", "path": "/owned", "errno": 13},
+ "serviceUid": 1, "serviceMountNamespace": "ns", "deviceId": "device"}, task_id="task", runtime={"faultDeviceId": "device"})
+
+ def test_partial_prepare_is_always_recovered_and_dirty_cleanup_is_retained(self):
+ test = self
+ class FakeAdapter:
+ def __init__(self, dirty): self.calls = []; self.dirty = dirty
+ def record(self, label, value): return value
+ def call(self, operation, **kwargs):
+ self.calls.append(operation)
+ if operation == "preflight": return test.topology()
+ if operation == "prepare": raise RuntimeError("lost prepare ACK")
+ if operation == "cleanup": return {"remainingOwnedPaths": ["owned"] if self.dirty else [], "activeOwnedServices": [], "remainingFaults": [], "pendingMutations": []}
+ return {}
+ for dirty in (False, True):
+ with self.subTest(dirty=dirty), tempfile.TemporaryDirectory() as tmp:
+ adapter = FakeAdapter(dirty)
+ runtime = {"binding": self.binding()}
+ runner = extended.Runner(runtime, {}, "b" * 40, Path(tmp), adapter=adapter,
+ probe_factory=lambda binding: types.SimpleNamespace(binding=binding))
+ row = runner.branch("heal.slow-io-and-eio", "H1", "eio")
+ self.assertEqual(row["verdict"], "FAIL")
+ self.assertEqual(row["cleanup"], "FAIL" if dirty else "PASS")
+ self.assertEqual(adapter.calls, ["preflight", "prepare", "recover", "cleanup"])
+ self.assertIn("lost prepare ACK", row["reason"])
+
+ def test_native_branches_do_not_invoke_resource_adapter(self):
+ runner = extended.Runner({"binding": self.binding()}, {}, "b" * 40, Path("unused"), adapter=object())
+ row = runner.branch("heal.race-ilm-expiry-and-transition", "H2", "native-ilm-fence")
+ self.assertEqual(row["verdict"], "BLOCKED")
+ self.assertEqual(row["cleanup"], "NOT_RUN")
+
+ def test_synthetic_clock_checks_full_soak_window_without_shortening_production_duration(self):
+ class Clock:
+ def __init__(self): self.now = 0
+ def clock(self): return self.now
+ def sleep(self, seconds): self.now += seconds
+ class Adapter:
+ def call(self, action, **params):
+ if action == "soak-fault": return {"faultId": "fault", "kind": params["kind"], "injectedAtNs": 1}
+ if action == "soak-recover": return {"faultId": "fault", "remainingFaults": []}
+ return {"rssBytes": 1, "openFds": 1, "queuedTasks": 1, "unresolvedRecords": 0, "temporaryBytes": 1, "panicCount": 0, "busyLoopCount": 0}
+ def record(self, label, value): return value
+ class Recorder:
+ def __init__(self): self.sequence = 0
+ def call(self, operation, params, body=None):
+ self.sequence += 1
+ return {"VersionId": params.get("VersionId", str(self.sequence))}
+ clock = Clock(); expected = []
+ runtime = {"soakLimits": {"rssBytes": 10, "openFds": 10, "queuedTasks": 10, "unresolvedRecords": 0, "temporaryBytes": 10}}
+ runner = extended.Runner(runtime, {}, "b" * 40, Path("unused"), adapter=Adapter(), clock=clock.clock, sleep=clock.sleep)
+ result = runner.soak({"branch": "24h"}, Recorder(), "bucket", expected, "H2")
+ self.assertEqual(extended.SOAK_SECONDS, {"24h": 86400, "72h": 259200})
+ self.assertGreaterEqual(result["actualElapsedSeconds"], 86400)
+ self.assertLessEqual(sum(e["key"] == "soak.bin" for e in expected), 65)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test_validation_heal_flapping.py b/test_validation_heal_flapping.py
new file mode 100644
index 00000000..069655a1
--- /dev/null
+++ b/test_validation_heal_flapping.py
@@ -0,0 +1,235 @@
+import copy
+from pathlib import Path
+import tempfile
+import types
+import unittest
+from unittest.mock import patch
+
+from scripts.validation_heal import flapping, fleet_node, drive_gate
+from scripts.validation_heal.matrix import AUTOMATIC_CASES, select
+from scripts.validation_heal.mrf_replay import identity
+import test_validation_heal_coordinator_crash as crash_fixtures
+import test_validation_heal_mrf as mrf_fixtures
+
+
+class FlappingTests(unittest.TestCase):
+ def scene(self):
+ scene, authorities, bucket, obligations = crash_fixtures.CoordinatorCrashTests().mrf_scene()
+ for value in (scene, authorities):
+ for node in value.values():
+ for row in node["files"]:
+ for version in row.get("metadata", {}).get("versions", []):
+ version["versionId"] = "null"
+ for item in obligations: item["versionId"] = "null"
+ return scene, authorities, bucket, obligations
+
+ def test_node_partial_replay_requires_real_original_members_and_full_ack_roles(self):
+ scene, authorities, bucket, obligations = self.scene()
+ members = {("node4", disk) for disk in range(4)}
+ proof = flapping.replay_progress(scene, authorities, bucket, obligations, members)
+ self.assertTrue(proof["partial"])
+ self.assertFalse(proof["finalStreamingProofClaimed"])
+ for kind in ("role", "version", "authority", "part", "uuid", "member"):
+ bad, original = copy.deepcopy(scene), copy.deepcopy(authorities)
+ metadata = next(row for row in bad["node4"]["files"] if row.get("metadata"))["metadata"]
+ version = metadata["versions"][0]
+ selected = members
+ if kind == "role": version["erasure"]["index"] = 1
+ elif kind == "version": version["versionId"] = "other"
+ elif kind == "authority": next(row for row in original["node1"]["files"] if row.get("metadata"))["metadata"]["versions"][0]["size"] += 1
+ elif kind == "part": next(row for row in bad["node4"]["files"] if not row.get("metadata"))["sha256"] = "missing"
+ elif kind == "uuid": bad["node4"]["formats"][0]["value"]["xl"]["this"] = "foreign"
+ else: selected = {("node5", disk) for disk in range(4)}
+ with self.subTest(kind=kind), self.assertRaises(ValueError):
+ flapping.replay_progress(bad, original, bucket, obligations, selected)
+
+ def test_single_drive_progress_uses_fifteen_surviving_authorities(self):
+ scene, authorities, bucket, obligations = self.scene()
+ full = copy.deepcopy(scene["node4"]["files"])
+ for row in copy.deepcopy(full):
+ row["path"] = row["path"].replace("new-00", "new-01")
+ if row.get("metadata"): row["metadata"]["key"] = row["metadata"]["key"].replace("new-00", "new-01")
+ full.append(row)
+ authorities["node4"]["files"] = [row for row in full if row["disk"] != 3]
+ scene["node4"]["files"] = [row for row in full if row["disk"] != 3 or "new-00" in row["path"]]
+ proof = flapping.replay_progress(scene, authorities, bucket, obligations, {("node4", 3)})
+ self.assertTrue(proof["partial"])
+ self.assertEqual(proof["pendingACKs"], 1)
+ authorities["node4"]["files"] = [row for row in authorities["node4"]["files"]
+ if not (row.get("metadata") and "new-01" in row["path"] and row["disk"] == 0)]
+ with self.assertRaises(ValueError):
+ flapping.replay_progress(scene, authorities, bucket, obligations, {("node4", 3)})
+
+ def test_zero_and_complete_reconstruction_cannot_certify_an_interruption_window(self):
+ scene, authorities, bucket, obligations = self.scene()
+ members = {("node4", disk) for disk in range(4)}
+ empty = copy.deepcopy(scene); empty["node4"]["files"] = []
+ self.assertFalse(flapping.replay_progress(empty, authorities, bucket, obligations, members)["partial"])
+ for row in copy.deepcopy(scene["node4"]["files"]):
+ row["path"] = row["path"].replace("new-00", "new-01")
+ if row.get("metadata"): row["metadata"]["key"] = row["metadata"]["key"].replace("new-00", "new-01")
+ scene["node4"]["files"].append(row)
+ self.assertFalse(flapping.replay_progress(scene, authorities, bucket, obligations, members)["partial"])
+
+ def test_incomplete_ack_cannot_retire_rebind_or_rollback_during_repeated_loss(self):
+ fixture = mrf_fixtures.DurableMrfTests()
+ record = fixture.observed()
+ from scripts.validation_heal.mrf_records import checkpoint
+ value, item = checkpoint(record), fixture.obligation()
+ lifecycle = value["intents"][0]["lifecycle"]
+ owner = {"node": "node1", "checkpointOwner": value["owner"], "sequence": value["sequence"],
+ "responsibilityId": lifecycle["responsibility_id"], "bucketIncarnationId": lifecycle["source_bucket_incarnation_id"]}
+ wanted = identity(item)
+ bindings = {wanted: [owner]}
+ self.assertEqual(flapping.retained_pending({"node1": value}, [item], bindings, {wanted})["pendingOriginalResponsibilities"], 1)
+ for kind in ("absent", "retired", "owner", "sequence", "responsibility", "incarnation", "accepted", "held"):
+ bad = copy.deepcopy(value)
+ if kind == "absent": bad["present"] = False
+ elif kind == "retired": bad["intents"] = []
+ elif kind == "owner": bad["owner"] = "other"
+ elif kind == "sequence": bad["sequence"] -= 1
+ elif kind == "responsibility": bad["intents"][0]["lifecycle"]["responsibility_id"] = "other"
+ elif kind == "incarnation": bad["intents"][0]["lifecycle"]["source_bucket_incarnation_id"] = "other"
+ elif kind == "accepted": bad["intents"][0]["lifecycle"]["last_operator_acceptance"] = {"accepted": True}
+ else: bad["intents"][0]["lifecycle"]["state"] = {"state": "held"}
+ with self.subTest(kind=kind), self.assertRaises(ValueError):
+ flapping.retained_pending({"node1": bad}, [item], bindings, {wanted})
+
+ def events(self):
+ events, obligations, _, _ = crash_fixtures.CoordinatorCrashTests().events()
+ return events, obligations
+
+ def test_task_storm_and_cross_ack_token_reuse_fail_without_counting_duplicates(self):
+ events, obligations = self.events()
+ proof = flapping.bounded_admissions(events, obligations)
+ self.assertEqual(proof["distinctAcceptedTaskIds"], 1)
+ events["node1"].append(copy.deepcopy(events["node1"][-1]))
+ self.assertEqual(flapping.bounded_admissions(events, obligations)["distinctAcceptedTaskIds"], 1)
+ for kind in ("version", "source", "key", "owner", "storm"):
+ bad = copy.deepcopy(events)
+ if kind == "version": bad["node1"][-1]["version_id"] = 'Some("other")'
+ elif kind == "source": bad["node1"][0]["source"] = "scanner"
+ elif kind == "key": bad["node1"][-1]["object"] = "foreign"
+ elif kind == "owner": bad["node2"] = copy.deepcopy(bad["node1"])
+ else:
+ pair = bad["node1"][:2]
+ for index in range(flapping.PROTOCOL["maxAcceptedTasksPerACKPerOwner"]):
+ admission, start = copy.deepcopy(pair)
+ admission["request_id"] = start["task_id"] = f"token-{index}"
+ bad["node1"].extend((admission, start))
+ with self.subTest(kind=kind), self.assertRaises(ValueError): flapping.bounded_admissions(bad, obligations)
+
+ def test_internal_system_mrf_tasks_remain_bounded_and_cannot_discharge_owned_acks(self):
+ events, obligations = self.events()
+ admission, start = copy.deepcopy(events["node1"][:2])
+ admission["request_id"] = start["task_id"] = "system-token"
+ start["bucket"], start["object"] = ".rustfs.sys", "scanner/durable-dirty-producer-replay/owned.json"
+ events["node1"].extend((admission, start))
+ proof = flapping.bounded_admissions(events, obligations)
+ self.assertEqual(proof["internalSystemTasks"], 1)
+ self.assertFalse(proof["internalSystemTasksCertifyOwnedACKs"])
+ for index in range(flapping.PROTOCOL["maxSystemTasksPerOwner"]):
+ admission, start = copy.deepcopy((admission, start))
+ admission["request_id"] = start["task_id"] = f"system-token-{index}"
+ start["object"] = f"scanner/durable-dirty-producer-replay/{index}.json"
+ events["node1"].extend((admission, start))
+ with self.assertRaises(ValueError): flapping.bounded_admissions(events, obligations)
+
+ def process(self, pid=10):
+ return {name: {"pid": pid, "startTicks": pid * 100}.get(name, name) for name in fleet_node.PROCESS_FIELDS}
+
+ def test_drive_rearm_does_not_accept_recycled_process_or_dac_bypass(self):
+ before = self.process()
+ owner = {"runtimeProfile": "flapping"}
+ calls = []
+ gate = types.SimpleNamespace(validate_live_boundary=lambda pid: calls.append(("boundary", pid)),
+ apply_gate=lambda state, roots, disk, mode: calls.append(("arm", disk, mode)) or {"state": "ARMED"})
+ quorum = types.SimpleNamespace(checked=lambda state, roots: None)
+ request = {"runId": "owned", "action": "drive-gate-arm-live", "disk": 1, "expected": before}
+ with patch.object(fleet_node, "owned", return_value=(Path("/owned"), [Path("/disk")], owner)), \
+ patch.object(fleet_node, "process", return_value=before), \
+ patch.object(fleet_node, "gate_module", return_value=gate), \
+ patch.object(fleet_node, "quorum_gate_module", return_value=quorum):
+ self.assertTrue(fleet_node.drive_gate_arm_live(request)["sameOriginalProcess"])
+ self.assertEqual(calls, [("boundary", 10), ("arm", 1, "permission-denied")])
+ calls.clear()
+ with self.assertRaises(ValueError):
+ fleet_node.drive_gate_arm_live({**request, "expected": self.process(11)})
+ self.assertEqual(calls, [])
+ with patch.object(gate, "validate_live_boundary", side_effect=ValueError("DAC bypass")):
+ with self.assertRaises(ValueError): fleet_node.drive_gate_arm_live(request)
+ self.assertEqual(calls, [])
+
+ def test_three_drive_gate_cycles_preserve_recovery_intent_and_original_inode(self):
+ with tempfile.TemporaryDirectory() as folder:
+ state, root = Path(folder) / "state", Path(folder) / "root"
+ state.mkdir(); root.mkdir(); data = root / "data"; data.mkdir(mode=0o700)
+ inode = data.stat().st_ino
+ original = Path.lstat
+ def rooted(path):
+ observed = original(path)
+ return types.SimpleNamespace(st_mode=observed.st_mode, st_uid=0, st_nlink=observed.st_nlink,
+ st_size=observed.st_size, st_dev=observed.st_dev, st_ino=observed.st_ino)
+ for cycle in range(3):
+ # Only ownership is simulated. The actual inode, recovery
+ # marker and three permission changes persist on the filesystem.
+ with patch.object(Path, "lstat", rooted):
+ drive_gate.apply_gate(state, [root], 0, "permission-denied")
+ drive_gate.activate_gate(state, [root])
+ self.assertEqual(data.stat().st_mode & 0o777, 0)
+ self.assertTrue((state / "drive-gate.json").exists())
+ drive_gate.release_gate(state, [root])
+ self.assertEqual(data.stat().st_ino, inode)
+ self.assertEqual(data.stat().st_mode & 0o777, 0o700)
+ self.assertFalse((state / "drive-gate.json").exists())
+
+ def test_original_node_return_requires_new_birth_without_host_candidate_or_settings_change(self):
+ before, after = self.process(10), self.process(11)
+ flapping.node_return(before, after)
+ for name in ("pid", "bootId", "binarySha256", "environmentFileSha256", "settings"):
+ bad = copy.deepcopy(after)
+ if name == "pid": bad = copy.deepcopy(before)
+ else: bad[name] = "changed"
+ with self.subTest(name=name), self.assertRaises(ValueError): flapping.node_return(before, bad)
+ with self.assertRaises(ValueError): flapping.same_process(before, after)
+
+ def test_bounded_resources_reject_fd_rss_cpu_queue_and_total_deadline(self):
+ resource = {"readOnly": True, "fileDescriptors": 10, "rssBytes": 100, "identity": self.process(),
+ "monotonicNs": 2 * 10**9, "clockTicksPerSecond": 100, "cpuTicks": 100, "clockDomain": "bootId"}
+ binding = {"endpoints": {"node1": "http://node1"}}
+ status = {"httpStatus": 200, "response": {"healActiveTasks": 0, "healQueueLength": 0}}
+ for kind in ("good", "fd", "rss", "cpu", "queue", "deadline", "peer"):
+ value, result = copy.deepcopy(resource), copy.deepcopy(status)
+ if kind == "fd": value["fileDescriptors"] = flapping.PROTOCOL["maxFDs"] + 1
+ elif kind == "rss": value["rssBytes"] = flapping.PROTOCOL["maxRssBytes"] + 1
+ elif kind == "cpu": value["cpuTicks"] = 100000
+ elif kind == "queue": result["response"]["healQueueLength"] = flapping.PROTOCOL["maxQueuedTasks"] + 1
+ elif kind == "peer": value["identity"] = self.process(11)
+ fleet = types.SimpleNamespace(nodes=["node1", "node4"], record=lambda label, value: value,
+ remote=lambda *args, **kwargs: value)
+ with patch.object(flapping, "Probe", return_value=types.SimpleNamespace(admin=lambda *args, **kwargs: result)):
+ budget = flapping.Budget(fleet, binding)
+ previous = copy.deepcopy(resource); previous["monotonicNs"] = 10**9; previous["cpuTicks"] = 90
+ budget.previous["node1"] = previous
+ if kind == "deadline": budget.deadline = 0
+ with self.subTest(kind=kind):
+ if kind == "good": budget.sample(["node1"])
+ else:
+ with self.assertRaises(ValueError): budget.sample(["node1"])
+
+ def test_all_source_branches_are_dispatched_with_isolated_runtime_and_bounded_protocol(self):
+ self.assertEqual(select(flapping.CASE_ID)[0]["variants"], list(flapping.BRANCHES))
+ self.assertIn(flapping.CASE_ID, AUTOMATIC_CASES)
+ config = fleet_node.runtime_config("flapping")
+ self.assertEqual(config["RUSTFS_HEAL_MRF_ENABLE"], "true")
+ self.assertEqual(config["RUSTFS_HEAL_MAX_CONCURRENT_HEALS"], "1")
+ self.assertEqual(config["RUSTFS_HEAL_MRF_REPLAY_BATCH"], "1")
+ self.assertEqual(config["RUSTFS_SCANNER_ENABLED"], "false")
+ self.assertEqual(config["RUSTFS_HEAL_AUTO_HEAL_ENABLE"], "false")
+ self.assertEqual(flapping.PROTOCOL["cycles"], 3)
+ self.assertEqual(len(flapping.PROTOCOL["uniqueACKsPerCycle"]), 3)
+ self.assertLess(sum(flapping.PROTOCOL["uniqueACKsPerCycle"]), 128)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test_validation_heal_full_restart.py b/test_validation_heal_full_restart.py
new file mode 100644
index 00000000..195bfd34
--- /dev/null
+++ b/test_validation_heal_full_restart.py
@@ -0,0 +1,219 @@
+import copy
+from datetime import datetime, timedelta, timezone
+import io
+import json
+from pathlib import Path
+import tempfile
+import types
+import unittest
+from unittest.mock import Mock, patch
+
+from scripts.validation_heal import full_restart as restart
+from scripts.validation_heal.matrix import select, AUTOMATIC_CASES
+from scripts.validation_heal.fleet_node import PROCESS_FIELDS, resource_limits, snapshot_buckets, verify_part_owned
+
+
+class FullRestartTests(unittest.TestCase):
+ token = "12345678-1234-4234-8234-123456789abc"
+
+ def test_restarted_mpu_checks_real_streamed_get_receipts_on_every_peer(self):
+ bucket, key = "rv-heal-0123456789abcdef", "pending/parts.bin"
+ binding = {"schemaVersion": 1, "runId": "gh-12345678901-1-h2", "jumpHostOnly": True,
+ "exclusiveHealService": True, "leaseExpiresAt": (datetime.now(timezone.utc) + timedelta(minutes=5)).isoformat(),
+ "bucketPrefix": bucket, "endpoints": {f"node{i}": f"http://rustfs-node{i}:9000" for i in range(1, 5)},
+ "defaultEndpoint": "node1", "candidateCommit": "a" * 40, "harnessCommit": "b" * 40,
+ "candidatePackageSha256": "c" * 64, "candidateBinarySha256": "d" * 64, "rcBinarySha256": "e" * 64,
+ "maxInventoryObjects": 2048, "maxInventoryBytes": 2 * 1024**3}
+ pending = {"bucket": bucket, "uploads": [{"key": key, "uploadId": "original-upload",
+ "parts": [{"PartNumber": 1, "ETag": "original-part"}]}]}
+ expected = restart.payload(f"{bucket}/{key}", 5 * 1024**2 + 3)
+ client = Mock()
+ recorder = Mock()
+ fleet = types.SimpleNamespace(record=lambda label, value: value)
+ calls = []
+ bad_endpoint = None
+
+ def streamed(probe):
+ def get_object(**params):
+ self.assertEqual(params, {"Bucket": bucket, "Key": key})
+ calls.append(probe.endpoint_name)
+ body = bytes([expected[0] ^ 1]) + expected[1:] if probe.endpoint_name == bad_endpoint else expected
+ return {"ContentLength": len(body), "Body": io.BytesIO(body),
+ "ResponseMetadata": {"HTTPStatusCode": 200}}
+ client.get_object.side_effect = get_object
+ return client
+
+ with patch.object(restart.Probe, "s3_client", streamed):
+ restart.complete_pending_uploads(recorder, fleet, binding, pending)
+ self.assertEqual(calls, list(binding["endpoints"]))
+ recorder.call.assert_called_once_with("complete_multipart_upload", {
+ "Bucket": bucket, "Key": key, "UploadId": "original-upload",
+ "MultipartUpload": {"Parts": [{"PartNumber": 1, "ETag": "original-part"}]}})
+ bad_endpoint = "node4"
+ calls.clear()
+ with self.assertRaisesRegex(ValueError, "lost their actual acknowledged bytes"):
+ restart.complete_pending_uploads(recorder, fleet, binding, pending)
+ self.assertEqual(calls, list(binding["endpoints"]))
+
+ def test_combined_fixture_has_explicit_bounded_physical_census_budget(self):
+ # Four roots retain xl.meta, an external part and its integrity
+ # sidecar, plus a possible metadata backup. MPU adds three parts.
+ conservative_files = 4 * (4 * (1025 + 128 + 16) + 7)
+ self.assertGreater(conservative_files, resource_limits("cursor")["maxSnapshotFiles"])
+ limits = resource_limits("full-restart")
+ self.assertLess(conservative_files, limits["maxSnapshotFiles"])
+ self.assertEqual(limits["runtimeMaxSeconds"], 2700)
+ self.assertEqual(limits["minimumDriveFreeBytes"], 2 * 1024**3)
+ with self.assertRaises(ValueError): resource_limits("unbounded")
+
+ def wanted(self):
+ wanted = {("db", f"objects/{index:04d}", "null"): {"deleteMarker": False, "payload": {"size": 17}}
+ for index in range(1025)}
+ wanted[("versions", "history", "old-version")] = {"deleteMarker": False, "payload": {"size": 19}}
+ wanted[("versions", "history", "marker")] = {"deleteMarker": True}
+ return wanted
+
+ def receipt(self):
+ repaired = {("db", "objects/0000", "null")}
+ objects = []
+ for pair in (next(iter(repaired)), ("versions", "history", "old-version"), ("versions", "history", "marker")):
+ state = "repaired" if pair in repaired else "metadata_healthy" if pair[2] == "marker" else "verified_healthy"
+ objects.append({"identity": {"bucket": pair[0], "object": pair[1], "versionId": pair[2],
+ "poolIndex": 0, "setIndex": 0, "bucketIncarnationId": "incarnation-" + pair[0]}, "disposition": {"state": state}})
+ return {"summary": "finished", "outcome": {"execution": {"state": "completed"}, "coverage": "complete",
+ "objectsTruncated": True, "objects": objects,
+ "counters": {"processed": 1027, "healed": 1, "unchanged": 1026, "skipped": 0,
+ "failed": 0, "unknown": 0, "attemptFailures": 0, "overflowed": False}}}, repaired
+
+ def test_complete_counters_do_not_make_unknown_or_cross_bucket_receipts_pass(self):
+ current, repaired = self.receipt()
+ proof = restart.responsibility(current, self.wanted(), repaired)
+ self.assertFalse(proof["diagnosticsAreFullManifest"])
+ for kind in ("unknown", "false-repair", "duplicate", "bucket", "version", "incarnation", "counter", "bool", "coverage", "truncation"):
+ with self.subTest(kind=kind):
+ bad = copy.deepcopy(current)
+ if kind == "unknown": bad["outcome"]["objects"][0]["disposition"]["state"] = "unknown"
+ elif kind == "false-repair": bad["outcome"]["objects"][1]["disposition"]["state"] = "repaired"
+ elif kind == "duplicate": bad["outcome"]["objects"].append(copy.deepcopy(bad["outcome"]["objects"][0]))
+ elif kind == "bucket": bad["outcome"]["objects"][0]["identity"]["bucket"] = "versions"
+ elif kind == "version": bad["outcome"]["objects"][1]["identity"]["versionId"] = "current-version"
+ elif kind == "incarnation": bad["outcome"]["objects"][2]["identity"]["bucketIncarnationId"] = "other"
+ elif kind == "counter": bad["outcome"]["counters"]["processed"] -= 1
+ elif kind == "bool": bad["outcome"]["counters"]["healed"] = True
+ elif kind == "coverage": bad["outcome"]["coverage"] = "partial"
+ elif kind == "truncation": bad["outcome"]["objectsTruncated"] = False
+ with self.assertRaises(ValueError): restart.responsibility(bad, self.wanted(), repaired)
+
+ def journal(self):
+ current, _ = self.receipt()
+ owner = {"node": "n0", "disk": 2, "value": {"options": {"timeout": {"secs": 149, "nanos": 0}}}}
+ terminal = {"schema": 1, "task_id": self.token, "status": "Completed", "heal_type": {"type": "cluster"},
+ "options": copy.deepcopy(owner["value"]["options"])}
+ item = {"type": "object", "bucket": "versions", "object": "history", "versionId": "marker", "objectSize": 0}
+ report = {"schema": 1, "terminal": terminal, "outcome": current["outcome"], "min_seq": 1027,
+ "next_seq": 1028, "seqed_items": [[1027, item]]}
+ observed = {node: {"taskId": self.token, "readOnly": True, "records": [
+ {"disk": disk, "kind": kind, "present": node == "n0" and disk == 2 and kind != "intent",
+ **({"value": terminal if kind == "terminal" else report} if node == "n0" and disk == 2 and kind != "intent" else {})}
+ for disk in range(4) for kind in ("intent", "terminal", "report")]} for node in ("n0", "n1", "n2", "n3")}
+ return observed, owner, current
+
+ def test_original_record_and_retained_suffix_are_required_after_all_peer_restart(self):
+ observed, owner, current = self.journal()
+ self.assertTrue(restart.terminal_records(observed, self.token, owner, current, self.wanted())["sameRecordDisk"])
+ # The product sequence also contains durable bucket events; the
+ # retained suffix must remain contiguous while next_seq exceeds the
+ # object counter by those explicit events.
+ with_offset = copy.deepcopy(observed)
+ offset_report = next(row for row in with_offset["n0"]["records"] if row["kind"] == "report" and row["present"])
+ offset_report["value"]["min_seq"] = 1028
+ offset_report["value"]["next_seq"] = 1030
+ offset_report["value"]["seqed_items"] = [[1028, {"type": "bucket", "bucket": "db", "object": "", "versionId": "", "objectSize": 0}],
+ [1029, offset_report["value"]["seqed_items"][0][1]]]
+ self.assertTrue(restart.terminal_records(with_offset, self.token, owner, current, self.wanted())["sameRecordDisk"])
+ for kind in ("missing", "owner", "intent", "token", "scope", "budget", "outcome", "sequence", "suffix", "marker-size"):
+ with self.subTest(kind=kind):
+ bad = copy.deepcopy(observed)
+ terminal = next(row for row in bad["n0"]["records"] if row["kind"] == "terminal" and row["present"])
+ report = next(row for row in bad["n0"]["records"] if row["kind"] == "report" and row["present"])
+ if kind == "missing": terminal["present"] = False
+ elif kind == "owner": report["disk"] = 1
+ elif kind == "intent": next(row for row in bad["n0"]["records"] if row["disk"] == 2 and row["kind"] == "intent")["present"] = True
+ elif kind == "token": terminal["value"]["task_id"] = "new-task"
+ elif kind == "scope": terminal["value"]["heal_type"] = {"type": "bucket", "bucket": "db"}
+ elif kind == "budget": terminal["value"]["options"]["timeout"]["secs"] = 300
+ elif kind == "outcome": report["value"]["outcome"]["coverage"] = "partial"
+ elif kind == "sequence": report["value"]["seqed_items"][0][0] = 1026
+ elif kind == "suffix": report["value"]["next_seq"] += 1
+ elif kind == "marker-size": report["value"]["seqed_items"][0][1]["objectSize"] = 17
+ with self.assertRaises(ValueError): restart.terminal_records(bad, self.token, owner, current, self.wanted())
+
+ def processes(self):
+ return {node: {**{name: name + "-" + node for name in PROCESS_FIELDS}, "pid": 100 + index, "startTicks": 1000 + index}
+ for index, node in enumerate(("n0", "n1", "n2", "n3"))}
+
+ def test_each_original_peer_must_really_restart_without_changing_environment(self):
+ before = self.processes()
+ after = copy.deepcopy(before)
+ for item in after.values(): item["pid"] += 100; item["startTicks"] += 100
+ self.assertTrue(restart.continuity(before, after)["allFourOriginalProcessesRestarted"])
+ for field in ("pid", "environmentFileSha256", "binarySha256", "bootId", "mountNamespace", "unit"):
+ with self.subTest(field=field):
+ bad = copy.deepcopy(after)
+ if field == "pid": bad["n2"] = before["n2"]
+ else: bad["n2"][field] = "changed"
+ with self.assertRaises(ValueError): restart.continuity(before, bad)
+
+ def test_all_peer_stop_is_bound_to_original_birth_identity_and_real_zero_exit(self):
+ processes, calls = self.processes(), []
+ def remote(node, action, **params):
+ calls.append((node, action, params))
+ if action == "process-graceful": return {"actualExitCode": 0, "signal": "SIGTERM", "pidfdBound": True}
+ return {}
+ fleet = types.SimpleNamespace(nodes=list(processes), frozen_processes={"n2": processes["n2"]},
+ remote=remote, record=lambda label, value: value)
+ exits = restart.graceful_all(fleet, processes, "n2")
+ self.assertEqual(list(exits), ["n2", "n0", "n1", "n3"])
+ self.assertFalse(fleet.frozen_processes)
+ for node, action, params in calls:
+ if action != "stop": self.assertEqual(params["expected"], processes[node])
+ fleet.remote = lambda *args, **kwargs: {"actualExitCode": 9, "signal": "SIGTERM", "pidfdBound": True}
+ with self.assertRaisesRegex(ValueError, "normal graceful exit"): restart.graceful_all(fleet, processes, "n0")
+ self.assertIn("n0", fleet.frozen_processes)
+
+ def test_every_source_branch_has_an_automatic_h2_entry(self):
+ recipe = select("F05")[0]
+ self.assertEqual(recipe["variants"], list(restart.BRANCHES))
+ self.assertEqual(recipe["contract"]["probes"]["topology"], "H2")
+ self.assertIn(restart.CASE_ID, AUTOMATIC_CASES)
+
+ def test_multi_bucket_physical_scope_preserves_the_original_run_boundary(self):
+ owner = {"bucket": "rv-heal-0123456789abcdef"}
+ prefix = owner["bucket"]
+ self.assertEqual(snapshot_buckets(owner, {}), [prefix])
+ self.assertEqual(snapshot_buckets(owner, {"buckets": [prefix, prefix + "-dx", prefix + "-versions"]}),
+ [prefix, prefix + "-dx", prefix + "-versions"])
+ for buckets in ([prefix, "other"], [prefix, prefix + "/../outside"], [prefix, prefix], [prefix + "-dx"], [prefix, 7], []):
+ with self.subTest(buckets=buckets), self.assertRaises(ValueError): snapshot_buckets(owner, {"buckets": buckets})
+
+ def test_secondary_bucket_streaming_proof_cannot_bind_to_primary_bucket_with_same_key(self):
+ prefix = "rv-heal-0123456789abcdef"
+ bucket = prefix + "-dx"
+ relative = bucket + "/objects/object.bin/generation/part.1"
+ request = {"disk": 0, "relativePath": relative, "versionId": "null", "partNumber": 1}
+ result = {"physicalBitrotVerified": True, "bucket": bucket, "key": "objects/object.bin", "versionId": "null", "partNumber": 1}
+ with tempfile.TemporaryDirectory() as folder:
+ root = Path(folder).resolve()
+ part = root / "data" / relative
+ part.parent.mkdir(parents=True)
+ part.write_bytes(b"original-protected-shard")
+ with patch("scripts.validation_heal.fleet_node.command", return_value=types.SimpleNamespace(stdout=json.dumps(result))) as command:
+ proof = verify_part_owned(request, [root], {"bucket": prefix}, root / "tool")
+ self.assertEqual(proof["bucket"], bucket)
+ self.assertEqual(command.call_args.args[3:5], (bucket, "objects/object.bin"))
+ result["bucket"] = prefix
+ with patch("scripts.validation_heal.fleet_node.command", return_value=types.SimpleNamespace(stdout=json.dumps(result))), self.assertRaisesRegex(ValueError, "identity differs"):
+ verify_part_owned(request, [root], {"bucket": prefix}, root / "tool")
+
+
+if __name__ == "__main__": unittest.main()
diff --git a/test_validation_heal_g02_source.py b/test_validation_heal_g02_source.py
new file mode 100644
index 00000000..947ec433
--- /dev/null
+++ b/test_validation_heal_g02_source.py
@@ -0,0 +1,390 @@
+"""Frozen G02 ACK/reference and decoded physical contracts; no live proof."""
+import copy
+import hashlib
+import json
+import os
+from pathlib import Path
+import tempfile
+import threading
+from types import SimpleNamespace
+import unittest
+from unittest.mock import Mock, patch
+import uuid
+
+from scripts.validation_heal import race, mpu_race_backups as backups, heal_fault, cursor
+from scripts.validation_heal.heal_workload import Recorder
+import test_validation_heal_mpu_race_backups as backup_fixtures
+
+
+class VersionPutSourceTests(unittest.TestCase):
+ bucket = 'rv-heal-0123456789abcdef'
+
+ def baseline(self):
+ entries = []
+ for number, key in enumerate([f'objects/{i:04d}.bin' for i in range(3)] + [race.TARGET_KEY]):
+ for generation in range(3 if key == race.TARGET_KEY else 1):
+ nonce = f'{self.bucket}/{key}/{generation}'
+ body = race.payload(nonce, race.SIZE)
+ entries.append({'key': key, 'versionId': str(uuid.UUID(int=1 + number * 10 + generation)),
+ 'isLatest': key != race.TARGET_KEY or generation == 2, 'deleteMarker': False,
+ 'payload': {'size': len(body), 'sha256': hashlib.sha256(body).hexdigest()},
+ 'properties': {'ETag': '"seed"', 'ContentLength': race.SIZE,
+ 'ContentType': 'application/octet-stream', 'Metadata': {'nonce': nonce}}, 'tags': []})
+ return {'entries': entries}
+
+ def test_original_three_puts_copy_distinct_immutable_control_payloads(self):
+ for branch in race.G02_BRANCHES:
+ baseline = self.baseline(); plan = race.operation_plan(self.bucket, branch, baseline['entries'])
+ self.assertEqual(len(plan), 3)
+ for i, step in enumerate(plan):
+ control = baseline['entries'][i]
+ self.assertEqual(step['referenceKey'], control['key'])
+ body = race.payload(step['nonce'], race.SIZE)
+ self.assertEqual({'size': len(body), 'sha256': hashlib.sha256(body).hexdigest()}, control['payload'])
+
+ def test_final_manifest_retains_every_original_and_serial_put_ack(self):
+ for branch in race.G02_BRANCHES:
+ baseline = self.baseline(); plan = race.operation_plan(self.bucket, branch, baseline['entries'])
+ acks = [{'VersionId': str(uuid.UUID(int=100 + i)), 'ETag': f'"new-{i}"'} for i in range(3)]
+ final = race.final_inventory(baseline, branch, plan, acks)
+ target = [r for r in final['entries'] if r['key'] == race.TARGET_KEY]
+ self.assertEqual([r['versionId'] for r in target],
+ [r['versionId'] for r in baseline['entries'] if r['key'] == race.TARGET_KEY] + [a['VersionId'] for a in acks])
+ self.assertEqual([r['isLatest'] for r in target], [False] * 5 + [True])
+ self.assertEqual([r['payload'] for r in target[-3:]], [r['payload'] for r in baseline['entries'][:3]])
+ self.assertEqual(baseline, self.baseline())
+
+ def test_final_manifest_refuses_duplicate_original_nil_or_missing_new_version_acks(self):
+ for kind in ('duplicate', 'original', 'nil', 'missing', 'reference-drift'):
+ with self.subTest(kind=kind):
+ baseline = self.baseline(); branch = race.G02_BRANCHES[0]
+ plan = race.operation_plan(self.bucket, branch, baseline['entries'])
+ acks = [{'VersionId': str(uuid.UUID(int=100 + i)), 'ETag': f'"new-{i}"'} for i in range(3)]
+ if kind == 'duplicate': acks[1]['VersionId'] = acks[0]['VersionId']
+ elif kind == 'original': acks[1]['VersionId'] = baseline['entries'][-1]['versionId']
+ elif kind == 'nil': acks[1]['VersionId'] = str(uuid.UUID(int=0))
+ elif kind == 'missing': del acks[1]['VersionId']
+ else: plan[1]['nonce'] = 'foreign payload'
+ with self.assertRaises(ValueError): race.final_inventory(baseline, branch, plan, acks)
+
+ def test_public_manifest_sorts_uuid_identity_without_reordering_the_serial_ack_or_latest(self):
+ baseline = self.baseline(); branch = race.G02_BRANCHES[0]
+ plan = race.operation_plan(self.bucket, branch, baseline['entries'])
+ acks = [{'VersionId': str(uuid.UUID(int=i)), 'ETag': f'"new-{i}"'} for i in (900, 700, 800)]
+ final = race.final_inventory(baseline, branch, plan, acks)
+ self.assertEqual(final['entries'], sorted(final['entries'], key=lambda r: (r['key'], r['versionId'])))
+ self.assertEqual(final['acknowledgedVersionAppendOrder'], [a['VersionId'] for a in acks])
+ latest = [r for r in final['entries'] if r['key'] == race.TARGET_KEY and r['isLatest']]
+ self.assertEqual([r['versionId'] for r in latest], [acks[-1]['VersionId']])
+
+ def scene(self, topology):
+ baseline = self.baseline(); old = [r for r in baseline['entries'] if r['key'] == race.TARGET_KEY]
+ new = [{**copy.deepcopy(baseline['entries'][i]), 'key': race.TARGET_KEY,
+ 'versionId': str(uuid.UUID(int=100 + i)), 'isLatest': i == 2} for i in range(3)]
+ final = {'entries': [copy.deepcopy(r) for r in baseline['entries']] + new,
+ 'acknowledgedVersionAppendOrder': [r['versionId'] for r in new]}
+ for r in final['entries']:
+ if r['key'] == race.TARGET_KEY: r['isLatest'] = r['versionId'] == new[-1]['versionId']
+ nodes = ['vm000'] if topology == 'H1' else [f'vm00{i}' for i in range(4)]
+ count = 4 * len(nodes); data = 2 if topology == 'H1' else 12
+ disk_ids = [str(uuid.UUID(int=1000 + i)) for i in range(count)]
+ distribution = list(range(count, 0, -1))
+ def build(entries):
+ result = {}
+ for node_number, node in enumerate(nodes):
+ value = result[node] = {'files': [], 'formats': []}
+ for disk in range(4):
+ slot = node_number * 4 + disk; index = distribution[slot]
+ value['formats'].append({'disk': disk, 'value': {'format': 'xl', 'id': 'pool',
+ 'xl': {'sets': [disk_ids], 'this': disk_ids[slot]}}})
+ for key in sorted({r['key'] for r in entries}):
+ versions = []
+ for row in (r for r in entries if r['key'] == key):
+ generation = str(uuid.UUID(int=10000 + uuid.UUID(row['versionId']).int))
+ version = {'versionId': row['versionId'], 'isLatest': row['isLatest'], 'deleted': False,
+ 'dataDir': generation, 'size': race.SIZE, 'inline': None, 'usesLegacyChecksum': False,
+ 'role': 'data' if index <= data else 'parity',
+ 'parts': [{'number': 1, 'size': race.SIZE, 'actualSize': race.SIZE}],
+ 'erasure': {'index': index, 'data_blocks': data, 'parity_blocks': count - data,
+ 'block_size': 1048576, 'distribution': distribution, 'algorithm': 'rs-vandermonde'}}
+ versions.append(version)
+ digest = hashlib.sha256((row['payload']['sha256'] + '/index/' + str(index)).encode()).hexdigest()
+ value['files'].append({'disk': disk, 'path': f'{self.bucket}/{key}/{generation}/part.1',
+ 'size': (race.SIZE + data - 1) // data + 32, 'sha256': digest})
+ metadata = {'bucket': self.bucket, 'key': key, 'versions': versions}
+ value['files'].append({'disk': disk, 'path': f'{self.bucket}/{key}/xl.meta', 'size': 1000,
+ 'sha256': hashlib.sha256(json.dumps(metadata, sort_keys=True).encode()).hexdigest(), 'metadata': metadata})
+ return result
+ before, after = build(baseline['entries']), build(final['entries'])
+ return before, after, race.map_fixture(before, self.bucket, baseline['entries']), final
+
+ def test_actual_transition_accepts_encoded_lengths_and_all_six_versions_in_h1_and_h2(self):
+ for topology in ('H1', 'H2'):
+ for branch in race.G02_BRANCHES:
+ with self.subTest(topology=topology, branch=branch):
+ before, after, mapped, final = self.scene(topology)
+ survivors, proof = race.physical_transition(before, after, self.bucket, mapped, final, branch)
+ target = [v for (key, _), v in survivors.items() if key == race.TARGET_KEY]
+ self.assertEqual(len(target), 6)
+ self.assertTrue(all(p['size'] != race.SIZE for v in target for p in v['cohort']))
+ self.assertTrue(proof['physicalProofBeforeGETOrRescue'])
+
+ def test_transition_rejects_wrong_new_or_retained_shards_metadata_and_unacknowledged_versions(self):
+ for topology in ('H1', 'H2'):
+ for fault in ('new-size', 'new-sha', 'old-sha', 'metadata-size', 'extra-version', 'missing-part', 'old-generation', 'ack-payload'):
+ with self.subTest(topology=topology, fault=fault):
+ before, after, mapped, final = self.scene(topology)
+ files = after['vm000']['files']
+ meta = next(r for r in files if r['path'] == f'{self.bucket}/{race.TARGET_KEY}/xl.meta')
+ old_dir = meta['metadata']['versions'][0]['dataDir']; new_dir = meta['metadata']['versions'][-1]['dataDir']
+ part = next(r for r in files if r['path'] == f'{self.bucket}/{race.TARGET_KEY}/{new_dir}/part.1')
+ if fault == 'new-size': part['size'] = race.SIZE
+ elif fault == 'new-sha': part['sha256'] = 'f' * 64
+ elif fault == 'old-sha': next(r for r in files if r['path'] == f'{self.bucket}/{race.TARGET_KEY}/{old_dir}/part.1')['sha256'] = 'e' * 64
+ elif fault == 'metadata-size':
+ for scene in after.values():
+ m = next(r for r in scene['files'] if r['path'] == f'{self.bucket}/{race.TARGET_KEY}/xl.meta')
+ m['metadata']['versions'][-1]['size'] += 1
+ elif fault == 'extra-version': meta['metadata']['versions'].append({**copy.deepcopy(meta['metadata']['versions'][0]), 'versionId': str(uuid.UUID(int=999))})
+ elif fault == 'missing-part': files.remove(part)
+ elif fault == 'old-generation': meta['metadata']['versions'][-1]['dataDir'] = old_dir
+ else: final['entries'][-1]['payload']['sha256'] = 'd' * 64
+ with self.assertRaises(ValueError): race.physical_transition(before, after, self.bucket, mapped, final, race.G02_BRANCHES[0])
+
+ def backup_scene(self, topology, prefix):
+ before, after, mapped, final = self.scene(topology)
+ old = [v['versionId'] for (key, _), v in mapped.items() if key == race.TARGET_KEY]
+ new = final['acknowledgedVersionAppendOrder']; observations = {}
+ for node in before:
+ common = {'hostname': 'host-' + node, 'binarySha256': 'a' * 64, 'quiescent': True}
+ before[node].update(common); after[node].update(common)
+ observations[node] = {**common, 'inspectorSha256': 'b' * 64, 'rows': [], 'readOnly': True,
+ 'sameHeldFileHashedAndDecoded': True, 'rawContentsExported': False, 'liveAuthorityClaimed': False}
+ for value in (before[node], after[node]):
+ for row in value['files']:
+ if 'metadata' in row: row['metadata']['physicalBitrotVerified'] = False
+ for disk in range(4):
+ path = f'{self.bucket}/{race.TARGET_KEY}/xl.meta'
+ primary = next(r['metadata'] for r in after[node]['files'] if r['disk'] == disk and r['path'] == path)
+ versions = {v['versionId']: v for v in primary['versions']}
+ ids = old + new[:prefix]
+ decoded = {**primary, 'versions': [{**copy.deepcopy(versions[vid]), 'isLatest': vid == ids[-1]} for vid in ids]}
+ for vid in old:
+ row = {'disk': disk, 'path': f'{self.bucket}/{race.TARGET_KEY}/{versions[vid]["dataDir"]}/xl.meta.bkp',
+ 'size': 100 + prefix, 'sha256': hashlib.sha256(f'{disk}/{vid}/{prefix}'.encode()).hexdigest()}
+ after[node]['files'].append(copy.deepcopy(row))
+ observations[node]['rows'].append({**row, 'metadata': copy.deepcopy(decoded)})
+ return before, after, observations, old, new
+
+ def test_all_three_original_generation_backups_require_a_contiguous_three_ack_prefix(self):
+ for topology in ('H1', 'H2'):
+ for count in range(4):
+ with self.subTest(topology=topology, prefix=count):
+ before, after, observed, old, new = self.backup_scene(topology, count)
+ result = backups.verify_versions(before, after, observed, list(before), self.bucket, race.TARGET_KEY,
+ old, new, 'a' * 64, 'b' * 64)
+ self.assertEqual(len(result['proofs']), 12 * len(before))
+ self.assertEqual({r['acknowledgedCompletePrefix'] for r in result['proofs']}, {count})
+ self.assertFalse(result['liveAuthorityClaimed'] or result['physicalRepairProved'] or result['canonicalCasePassClaimed'])
+ for fault in ('nonprefix', 'missing-old', 'descriptor', 'latest', 'sha', 'new-directory', 'omitted', 'foreign-host'):
+ with self.subTest(fault=fault):
+ before, after, observed, old, new = self.backup_scene('H1', 1)
+ row = observed['vm000']['rows'][0]
+ if fault == 'nonprefix': row['metadata']['versions'][-1]['versionId'] = new[1]
+ elif fault == 'missing-old': row['metadata']['versions'].pop(0)
+ elif fault == 'descriptor': row['metadata']['versions'][0]['parts'][0]['size'] += 1
+ elif fault == 'latest': row['metadata']['versions'][0]['isLatest'] = True
+ elif fault == 'sha': row['sha256'] = 'e' * 64
+ elif fault == 'new-directory':
+ old_path = row['path']; row['path'] = row['path'].replace(str(uuid.UUID(int=10031)), str(uuid.UUID(int=10100)))
+ next(r for r in after['vm000']['files'] if r['path'] == old_path and r['disk'] == row['disk'])['path'] = row['path']
+ elif fault == 'omitted': observed['vm000']['rows'].pop()
+ else: observed['vm000']['hostname'] = 'foreign'
+ with self.assertRaises(ValueError):
+ backups.verify_versions(before, after, observed, list(before), self.bucket, race.TARGET_KEY, old, new, 'a' * 64, 'b' * 64)
+
+ def test_nested_key_collector_binds_actual_open_fd_and_all_six_bounded_generations(self):
+ with tempfile.TemporaryDirectory() as directory:
+ helper = backup_fixtures.MultipartBackupTests()
+ request, node, files, _ = helper.fixture(directory)
+ for mount in node['MOUNTS']:
+ parent = mount / 'heal-validation' / request['runId'] / 'data' / self.bucket
+ (parent / 'objects').mkdir(); (parent / backups.KEY).rename(parent / race.TARGET_KEY)
+ for i in range(101, 106): (parent / race.TARGET_KEY / str(uuid.UUID(int=i))).mkdir()
+ request['action'] = 'version-race-backups'
+ def command(tool, path, bucket, key, **kwargs):
+ self.assertEqual((bucket, key), (self.bucket, race.TARGET_KEY))
+ fd = int(path.rsplit('/', 1)[1]); raw = os.pread(fd, backups.LIMIT + 1, 0)
+ return SimpleNamespace(returncode=0, stdout=json.dumps({'bucket': bucket, 'key': key,
+ 'physicalBitrotVerified': False, 'heldFileSha256': hashlib.sha256(raw).hexdigest()}))
+ node['command'] = command
+ with patch.object(heal_fault, 'mount_id', side_effect=lambda fd: os.fstat(fd).st_dev):
+ value = backups.run(request, node)
+ self.assertEqual(len(value['rows']), 4)
+ self.assertTrue(all(r['metadata']['heldFileSha256'] == r['sha256'] for r in value['rows']))
+ self.assertFalse(value['liveAuthorityClaimed'] or value['rawContentsExported'])
+
+ def test_actual_reader_keeps_durable_acks_and_final_round_for_all_old_and_new_versions(self):
+ for fault in (None, 'version', 'payload', 'property', 'unknown'):
+ with self.subTest(fault=fault), tempfile.TemporaryDirectory() as directory:
+ baseline = self.baseline(); original = [r for r in baseline['entries'] if r['key'] == race.TARGET_KEY]
+ plan = race.operation_plan(self.bucket, race.G02_BRANCHES[1], baseline['entries'])
+ acks = [{'VersionId': str(uuid.UUID(int=100 + i)), 'ETag': f'"new-{i}"'} for i in range(3)]
+ added = race.version_put_entries(baseline, plan, acks)
+ all_entries = {r['versionId']: r for r in original + added}
+ lock = threading.Lock(); done = threading.Event()
+ ledger = {'bucket': self.bucket, 'entries': copy.deepcopy(original)}; calls = []
+ def invoke(operation, params, body):
+ self.assertEqual(operation, 'get_object'); calls.append(params['VersionId'])
+ entry = all_entries[params['VersionId']]
+ value = {'operation': operation, 'params': copy.deepcopy(params), 'acknowledged': True,
+ 'resultUnknown': False, 'response': {'ResponseMetadata': {'HTTPStatusCode': 200},
+ 'VersionId': entry['versionId'], **copy.deepcopy(entry['properties'])}, 'payload': copy.deepcopy(entry['payload'])}
+ if fault == 'version': value['response']['VersionId'] = str(uuid.UUID(int=999))
+ elif fault == 'payload': value['payload']['sha256'] = 'e' * 64
+ elif fault == 'property': del value['response']['ContentLength']
+ elif fault == 'unknown': value.update(acknowledged=False, resultUnknown=True)
+ if len(calls) == 3:
+ with lock: ledger['entries'] = copy.deepcopy(original + added)
+ done.set()
+ return value
+ probe = SimpleNamespace(binding={'runId': 'gh-12345-1-h1', 'bucketPrefix': self.bucket,
+ 'maxInventoryBytes': 512 * 1024**2}, s3=invoke)
+ recorder = Recorder(probe, Path(directory) / 'reads')
+ if fault is None:
+ proofs = race.read_versions(recorder, ledger, lock, done)
+ self.assertEqual(len(proofs), 9)
+ self.assertEqual(set(calls), set(all_entries))
+ self.assertEqual(len(list(recorder.output.glob('*-ack.json'))), 9)
+ else:
+ with self.assertRaises(ValueError): race.read_versions(recorder, ledger, lock, done)
+ self.assertEqual(len(calls), 1)
+ self.assertEqual(len(list(recorder.output.glob('*-ack.json'))), 1)
+
+ def test_existing_part_verifier_streams_all_259_seed_or_262_final_versions_in_h1_h2(self):
+ for nodes in (['vm000'], [f'vm00{i}' for i in range(4)]):
+ for count in (259, 262):
+ with self.subTest(nodes=nodes, count=count):
+ cohorts = []; indexed = {}; calls = []
+ for number in range(count):
+ members = []
+ for node in nodes:
+ for disk in range(4):
+ row = {'node': node, 'disk': disk, 'relativePath': f'bucket/key/{number}/part.1',
+ 'versionId': str(uuid.UUID(int=number + 1)), 'partNumber': 1,
+ 'sha256': hashlib.sha256(f'{node}/{disk}/{number}'.encode()).hexdigest(), 'size': 131678}
+ members.append(row); indexed[node, disk, row['relativePath']] = row
+ cohorts.append({'cohort': members})
+ def remote(node, action, **params):
+ self.assertEqual(action, 'verify-parts'); calls.extend((node, r['disk'], r['relativePath']) for r in params['parts'])
+ return {'complete': True, 'parts': [{**indexed[node, r['disk'], r['relativePath']],
+ 'physicalSize': indexed[node, r['disk'], r['relativePath']]['size'], 'physicalBitrotVerified': True} for r in params['parts']]}
+ fleet = SimpleNamespace(nodes=nodes, remote=remote, record=lambda label, value: value)
+ result = cursor.verify_part_manifest(fleet, cohorts, 'bounded-version-manifest', count=count)
+ self.assertEqual(result['allPartsStreamingVerified'], count * 4 * len(nodes))
+ self.assertEqual(len(calls), len(set(calls)))
+ for wrong_count in (False, -1, cursor.OBJECTS + 1, count - 1):
+ with self.assertRaises(ValueError): cursor.verify_part_manifest(fleet, cohorts, 'refused', count=wrong_count)
+
+ def test_real_g02_caller_faults_the_original_oldest_version_in_both_public_branches(self):
+ class FaultBoundaryReached(BaseException):
+ pass
+ for branch in race.G02_BRANCHES:
+ with self.subTest(branch=branch), tempfile.TemporaryDirectory() as directory:
+ root = Path(directory); before, _, mapped, _ = self.scene('H1'); baseline = self.baseline()
+ candidate = root / 'candidate.json'; candidate.write_text(json.dumps({'source_sha': race.REFERENCE['candidateCommit'], 'package_sha256': 'b' * 64}))
+ rc = root / 'rc'; rc.write_bytes(b'fixture-rc')
+ fleet = Mock(); fleet.faults = []; fleet.cleanup.return_value = True
+ fleet.record.side_effect = lambda label, value: value
+ fleet.snapshot.return_value = before
+ chosen = mapped[race.TARGET_KEY, next(r['versionId'] for r in baseline['entries'] if r['key'] == race.TARGET_KEY)]
+ def remote(node, action, **params):
+ self.assertEqual(action, 'apply-fault')
+ if params['relativePath'].startswith(f'{self.bucket}/{race.TARGET_KEY}/'):
+ self.assertEqual(params['relativePath'], chosen['relativePath'])
+ self.assertEqual(params['disk'], 3)
+ raise FaultBoundaryReached()
+ return {'kind': 'missing'}
+ fleet.remote.side_effect = remote
+ probe = Mock(); probe.inventory.return_value = baseline
+ args = SimpleNamespace(evidence=root / 'evidence', candidate=candidate, rc_bin=rc,
+ tool_directory=root, testing_commit='c' * 40, binary_sha='a' * 64, case_id='heal.race-versioned-put')
+ with patch.object(race, 'Fleet', return_value=fleet), patch.object(race, 'verify_inspector', return_value={}), \
+ patch.object(race, 'Probe', return_value=probe), patch.object(race, 'validate_topology'), \
+ patch.object(race, 'seed_fixture', return_value=baseline['entries']), patch.object(race, 'verify_seed'), \
+ patch.object(race, 'map_fixture', return_value=mapped), patch.object(race, 'OBJECTS', 4), \
+ patch.object(race, 'verify_endpoint_resolution'), \
+ patch.object(race, 'verify_part_manifest') as verify, \
+ patch.dict(race.os.environ, {'GITHUB_RUN_ID': '12345', 'GITHUB_RUN_ATTEMPT': '1'}):
+ # The actual binding selects a fresh bucket; its read-only
+ # physical plan here remains the known synthetic baseline.
+ actual_plan = race.operation_plan(self.bucket, branch, baseline['entries'])
+ with patch.object(race, 'operation_plan', return_value=actual_plan), self.assertRaises(FaultBoundaryReached):
+ race.run_branch(args, 'H1', ['vm000'], branch)
+ self.assertEqual(verify.call_args.kwargs['count'], len(mapped))
+
+ def test_controller_requires_both_complete_six_version_reads_and_original_token_reader_overlap(self):
+ for complete, reader_overlap, writer_overlap in ((True, True, True), (True, False, True),
+ (False, True, True), (True, True, False)):
+ with self.subTest(complete=complete, reader_overlap=reader_overlap, writer_overlap=writer_overlap), tempfile.TemporaryDirectory() as directory:
+ baseline = self.baseline(); branch = race.G02_BRANCHES[1]
+ plan = race.operation_plan(self.bucket, branch, baseline['entries'])
+ for step in plan: step['branch'] = branch
+ output = Path(directory) / 'foreground'; output.mkdir()
+ writer = SimpleNamespace(output=output, mutations=SimpleNamespace(poison=lambda message: None))
+ sequence = 0
+ def call(operation, params, body):
+ nonlocal sequence
+ sequence += 1
+ ack = {'VersionId': str(uuid.UUID(int=100 + sequence)), 'ETag': f'"new-{sequence}"'}
+ (output / f'{sequence:06d}-ack.json').write_text(json.dumps({'operationCheck': 'PASS',
+ 'observation': {'operation': operation, 'params': params}}))
+ return ack
+ writer.call = call
+ sampled = threading.Event(); records = {}
+ def observe(*args):
+ sampled.set(); return {'originalTokenSample': True}
+ def read(recorder, ledger, lock, done):
+ self.assertTrue(done.wait(2)); self.assertTrue(sampled.wait(2))
+ with lock: entries = copy.deepcopy(ledger['entries'])
+ return [{'observation': {'operation': 'get_object', 'params': {'VersionId': row['versionId']}}}
+ for row in (entries if complete else entries[:3])]
+ fleet = SimpleNamespace(record=lambda label, value: records.setdefault(label, value))
+ probe = SimpleNamespace(binding={'runId': 'gh-12345-1-h1', 'bucketPrefix': self.bucket,
+ 'maxInventoryBytes': 512 * 1024**2})
+ def overlap(ack, *args):
+ return (writer_overlap and ack['observation']['params']['Metadata']['nonce'] == '1'
+ if ack['observation']['operation'] == 'put_object' else reader_overlap)
+ with patch.object(race, 'observe', side_effect=observe), patch.object(race, 'read_versions', side_effect=read), \
+ patch.object(race, 'sample_overlap', side_effect=overlap):
+ responses, accepted = race.race(writer, plan, probe, fleet, '/original/path', 'original-token', baseline=baseline)
+ self.assertEqual(len(responses), 3)
+ self.assertEqual(accepted, complete and reader_overlap and writer_overlap)
+ self.assertEqual(records['G02-historical-read-coverage']['complete'], complete)
+ self.assertEqual(records['G02-historical-read-coverage']['readerCallOverlapObserved'], reader_overlap)
+
+ def test_frozen_partial_window_requires_repaired_controls_32_pending_and_old_target_still_missing(self):
+ keys = [f'objects/{i:04d}.bin' for i in range(32)] + [race.TARGET_KEY]
+ targets = [{'key': key} for key in keys]
+ scene = {'vm000': {'files': [], 'formats': []}}
+ for kind in ('valid', '31-pending', 'no-repaired', 'target-repaired', 'all-repaired', 'root-finished', 'bool-progress'):
+ with self.subTest(kind=kind):
+ repaired = ({keys[0]} if kind not in {'31-pending', 'no-repaired', 'target-repaired', 'all-repaired'} else
+ set(keys[:2]) if kind == '31-pending' else set() if kind == 'no-repaired' else
+ {race.TARGET_KEY} if kind == 'target-repaired' else set(keys))
+ def validate(before, after, target):
+ if target['key'] not in repaired: raise ValueError('original missing part remains absent')
+ response = {'summary': 'finished' if kind == 'root-finished' else 'running',
+ 'outcome': {'counters': {'processed': True if kind == 'bool-progress' else 1}}}
+ fleet = SimpleNamespace(snapshot=lambda label: scene, record=lambda label, value: value)
+ with patch.object(race, 'observe', return_value={'response': response}), \
+ patch.object(race, 'validate_repaired_physical', side_effect=validate), \
+ patch.object(race.time, 'monotonic', side_effect=[0, 0, 121]), patch.object(race.time, 'sleep'):
+ if kind == 'valid':
+ self.assertEqual(race.wait_version_window(None, fleet, '/original', 'token', {}, scene, targets, self.bucket), scene)
+ else:
+ with self.assertRaises(ValueError): race.wait_version_window(None, fleet, '/original', 'token', {}, scene, targets, self.bucket)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/test_validation_heal_g03_source.py b/test_validation_heal_g03_source.py
new file mode 100644
index 00000000..01b0beb3
--- /dev/null
+++ b/test_validation_heal_g03_source.py
@@ -0,0 +1,83 @@
+"""Frozen serial-ACK deletion order and physical latest authority; no live proof."""
+import copy
+import unittest
+import uuid
+
+from scripts.validation_heal import race
+import test_validation_heal_race_physical as physical_fixtures
+
+
+class VersionDeleteSourceTests(unittest.TestCase):
+ bucket = 'rv-heal-0123456789abcdef'
+
+ def scene(self, branch):
+ before, _, _, _ = physical_fixtures.MarkerPhysicalTests().scene('batch-version-delete')
+ order = [str(uuid.UUID(int=i)) for i in (900, 700, 800)]
+ replacement = {str(uuid.UUID(int=i+1)): version for i, version in enumerate(order)}
+ for value in before.values():
+ for row in value['files']:
+ for version in row.get('metadata', {}).get('versions', []):
+ version['versionId'] = replacement[version['versionId']]
+ seed = [{'key':race.TARGET_KEY,'versionId':version,'isLatest':i==2,'deleteMarker':False}
+ for i,version in enumerate(order)]
+ mapped = race.map_fixture(before,self.bucket,seed)
+ plan = race.operation_plan(self.bucket,branch,seed)
+ selected = plan[0]['params']['VersionId']
+ final = [copy.deepcopy(row) for row in seed if row['versionId']!=selected]
+ for row in final:row['isLatest'] = row is final[-1]
+ after = copy.deepcopy(before)
+ selected_dir = mapped[race.TARGET_KEY,selected]['dataDir']
+ for value in after.values():
+ value['files'] = [row for row in value['files'] if selected_dir not in row['path']]
+ for row in value['files']:
+ metadata=row.get('metadata')
+ if metadata:
+ metadata['versions']=[version for version in metadata['versions'] if version['versionId']!=selected]
+ for version in metadata['versions']:
+ version['isLatest'] = version['versionId']==final[-1]['versionId']
+ baseline={'entries':sorted(copy.deepcopy(seed),key=lambda row:row['versionId'])}
+ return before,after,mapped,baseline,plan,{'entries':sorted(final,key=lambda row:row['versionId'])}
+
+ def test_deleted_current_promotes_last_retained_seed_ack_even_when_uuid_sort_disagrees(self):
+ for branch in race.G03_BRANCHES:
+ with self.subTest(branch=branch):
+ before,after,mapped,baseline,plan,wanted=self.scene(branch)
+ original=copy.deepcopy(baseline)
+ actual=race.final_inventory(baseline,branch,plan,[{'VersionId':plan[0]['params']['VersionId']}])
+ self.assertEqual(actual,wanted)
+ self.assertEqual(baseline,original)
+ self.assertEqual(plan[0]['seedVersionOrder'],[str(uuid.UUID(int=i)) for i in (900,700,800)])
+
+ def test_real_physical_oracle_allows_only_ack_defined_latest_promotion(self):
+ for branch in race.G03_BRANCHES:
+ with self.subTest(branch=branch):
+ before,after,mapped,baseline,plan,wanted=self.scene(branch)
+ survivors,proof=race.physical_transition(before,after,self.bucket,mapped,wanted,branch)
+ self.assertTrue(proof['physicalProofBeforeGETOrRescue'])
+ self.assertEqual(len(survivors),2)
+
+ def test_delete_manifest_rejects_missing_duplicate_or_foreign_ack_order(self):
+ for fault in ('missing','duplicate','foreign','wrong-delete','wrong-ack'):
+ with self.subTest(fault=fault):
+ _,_,_,baseline,plan,_=self.scene(race.G03_BRANCHES[1])
+ plan[0]['seedVersionOrder']=[str(uuid.UUID(int=i)) for i in (900,700,800)]
+ response={'VersionId':plan[0]['params']['VersionId']}
+ if fault=='missing':plan[0].pop('seedVersionOrder')
+ elif fault=='duplicate':plan[0]['seedVersionOrder'][1]=plan[0]['seedVersionOrder'][0]
+ elif fault=='foreign':plan[0]['seedVersionOrder'][1]=str(uuid.UUID(int=999))
+ elif fault=='wrong-delete':plan[0]['params']['VersionId']=str(uuid.UUID(int=700));response['VersionId']=str(uuid.UUID(int=700))
+ else:response['VersionId']=str(uuid.UUID(int=700))
+ with self.assertRaises(ValueError):race.final_inventory(baseline,race.G03_BRANCHES[1],plan,[response])
+
+ def test_retained_generation_payload_unknown_marker_or_latest_drift_is_refused(self):
+ for fault in ('bytes','directory','latest','marker','duplicate'):
+ with self.subTest(fault=fault):
+ before,after,mapped,_,_,wanted=self.scene(race.G03_BRANCHES[1])
+ value=after['node1']
+ meta=next(row['metadata'] for row in value['files'] if row['path'].endswith('/xl.meta'))
+ if fault=='bytes':next(row for row in value['files'] if row['path'].endswith('part.1'))['sha256']='changed'
+ elif fault=='directory':meta['versions'][0]['dataDir']=str(uuid.UUID(int=999))
+ elif fault=='latest':meta['versions'][0]['isLatest']=not meta['versions'][0]['isLatest']
+ elif fault=='marker':meta['versions'].append({'versionId':str(uuid.UUID(int=999)),'isLatest':False,'deleted':True})
+ else:meta['versions'].append(copy.deepcopy(meta['versions'][0]))
+ with self.assertRaises(ValueError):race.physical_transition(before,after,self.bucket,mapped,wanted,race.G03_BRANCHES[1])
diff --git a/test_validation_heal_g06_source.py b/test_validation_heal_g06_source.py
new file mode 100644
index 00000000..f424866f
--- /dev/null
+++ b/test_validation_heal_g06_source.py
@@ -0,0 +1,327 @@
+"""Frozen DS and COPY direction contracts; synthetic scenes are not live proof."""
+import copy
+from datetime import datetime, timedelta, timezone
+from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
+import json
+from pathlib import Path
+import tempfile
+import threading
+from types import SimpleNamespace
+import unittest
+from unittest.mock import Mock, patch
+import uuid
+
+import test_validation_heal as physical_fixtures
+from scripts.validation_heal import race
+
+
+class G06DispatchTests(unittest.TestCase):
+ def server(self, *, summary='running', hold_copy=True, first_only=False, allow_warmup=False):
+ copy_seen, sampled = threading.Event(), threading.Event()
+ failures, calls = [], [0]
+ class Handler(BaseHTTPRequestHandler):
+ protocol_version = 'HTTP/1.1'
+ def log_message(self, *args):
+ pass
+ def reply(self, raw, kind):
+ self.send_response(200); self.send_header('Content-Type', kind)
+ self.send_header('Content-Length', str(len(raw))); self.end_headers(); self.wfile.write(raw)
+ def do_PUT(self):
+ self.rfile.read(int(self.headers.get('Content-Length', '0')))
+ calls[0] += 1
+ copy_seen.set()
+ if hold_copy and (not first_only or calls[0] == 1) and not sampled.wait(3):
+ failures.append('SDK response did not observe Admin completion')
+ self.reply(b'' if '?tagging' in self.path else
+ b'"copy-ack"2026-10-26T00:00:00Z', 'application/xml')
+ def do_POST(self):
+ if not (allow_warmup and calls[0] == 0) and not copy_seen.wait(3):
+ failures.append('Admin request preceded an actual COPY')
+ state = summary if not first_only or calls[0] == 1 else 'finished'
+ self.reply(json.dumps({'summary': state, 'settings': race.COPY_REFERENCE['effective'],
+ 'outcome': {'counters': {'processed': 1}}}).encode(), 'application/json')
+ server = ThreadingHTTPServer(('127.0.0.1', 0), Handler)
+ worker = threading.Thread(target=server.serve_forever, daemon=True); worker.start()
+ self.addCleanup(server.server_close); self.addCleanup(server.shutdown)
+ binding = {'schemaVersion': 1, 'runId': 'gh-12345-1-h1', 'jumpHostOnly': True,
+ 'exclusiveHealService': True, 'leaseExpiresAt': (datetime.now(timezone.utc) + timedelta(minutes=5)).isoformat(),
+ 'bucketPrefix': 'rv-heal-0123456789abcdef', 'endpoints': {'node1': f'http://127.0.0.1:{server.server_port}'},
+ 'defaultEndpoint': 'node1', 'candidateCommit': race.COPY_CANDIDATE, 'harnessCommit': '2' * 40,
+ 'candidatePackageSha256': '3' * 64, 'candidateBinarySha256': '4' * 64, 'rcBinarySha256': '5' * 64,
+ 'maxInventoryObjects': 512, 'maxInventoryBytes': 512 * 1024**2}
+ return binding, sampled, failures
+
+ def test_g06_reuses_the_original_status_connection_without_changing_sdk_intervals(self):
+ with tempfile.TemporaryDirectory() as directory:
+ binding, sampled, failures = self.server(allow_warmup=True)
+ admin_probe = race.KeepAliveStatusProbe(binding, credential_pair=('test-access', 'test-secret'))
+ writer = race.RaceRecorder(race.DispatchProbe(binding, credential_pair=('test-access', 'test-secret')),
+ Path(directory) / 'foreground-ACKs', versioned=False)
+ plan = race.operation_plan(binding['bucketPrefix'], 'copy-source', [])
+ plan[0]['branch'] = 'copy-source'
+ path, token = '/rustfs/admin/v3/heal/' + binding['bucketPrefix'], str(uuid.uuid4())
+ records = {}; fleet = SimpleNamespace(record=lambda label, value: records.setdefault(label, value))
+ with race.foreground_status_session(admin_probe, fleet, 'copy-source', path, token):
+ warm = admin_probe.admin(path, [('clientToken', token)], b'')
+ original = admin_probe.admin
+ def admin(*args):
+ result = original(*args); sampled.set(); return result
+ admin_probe.admin = admin
+ _, accepted = race.race(writer, plan, admin_probe, fleet, path, token, race.COPY_REFERENCE)
+ self.assertTrue(accepted)
+ self.assertFalse(failures)
+ state = records['G06-status-transport']
+ self.assertIsNone(admin_probe._status)
+ self.assertEqual((state['requests'], state['connectionsOpened']), (2, 1))
+ first, second = state['observations']
+ self.assertEqual(first['transport']['connectionId'], second['transport']['connectionId'])
+ self.assertFalse(second['transport']['newConnection'])
+ ack = json.loads(next(writer.output.glob('*-ack.json')).read_text())
+ self.assertLess(warm['callFinishedMonotonicNs'], ack['observation']['callStartedMonotonicNs'])
+ self.assertTrue(race.sample_overlap(ack, records['original-token-status'], path, token, race.COPY_REFERENCE))
+
+ def test_status_session_failure_preserves_transport_diagnostics_and_closes_connection(self):
+ binding, _, _ = self.server(hold_copy=False)
+ probe = race.KeepAliveStatusProbe(binding, credential_pair=('test-access', 'test-secret'))
+ records = {}; fleet = SimpleNamespace(record=lambda label, value: records.setdefault(label, value))
+ with self.assertRaisesRegex(ValueError, 'fixture failure'):
+ with race.foreground_status_session(probe, fleet, 'replace-metadata',
+ '/rustfs/admin/v3/heal/' + binding['bucketPrefix'], str(uuid.uuid4())):
+ raise ValueError('fixture failure')
+ self.assertIsNone(probe._status)
+ self.assertIn('G06-status-transport', records)
+
+ def test_real_sdk_dispatch_schedules_original_status_and_keeps_strict_interval_oracle(self):
+ for summary in ('running', 'finished'):
+ with self.subTest(summary=summary), tempfile.TemporaryDirectory() as directory:
+ binding, sampled, failures = self.server(summary=summary)
+ writer_probe = race.DispatchProbe(binding, credential_pair=('test-access', 'test-secret'))
+ admin_probe = race.Probe(binding, credential_pair=('test-access', 'test-secret'))
+ original_admin = admin_probe.admin
+ def admin(*args):
+ result = original_admin(*args)
+ sampled.set() # Server-only fixture barrier; production notification never waits.
+ return result
+ admin_probe.admin = admin
+ writer = race.RaceRecorder(writer_probe, Path(directory) / 'foreground-ACKs', versioned=False)
+ plan = race.operation_plan(binding['bucketPrefix'], 'copy-destination', [])
+ plan[0]['branch'] = 'copy-destination'
+ records = {}
+ fleet = SimpleNamespace(record=lambda label, value: records.setdefault(label, value))
+ responses, accepted = race.race(writer, plan, admin_probe, fleet,
+ '/rustfs/admin/v3/heal/' + binding['bucketPrefix'], 'original-token', race.COPY_REFERENCE)
+ self.assertEqual(responses[0]['CopyObjectResult']['ETag'], '"copy-ack"')
+ self.assertEqual(accepted, summary == 'running')
+ self.assertFalse(failures)
+ ack = json.loads(next(writer.output.glob('*-ack.json')).read_text())
+ dispatch = records['G06-request-dispatch-observation']
+ sent = dispatch['requestSendMonotonicNs'][0]
+ observation = ack['observation']; status = records['original-token-status']
+ self.assertLess(observation['callStartedMonotonicNs'], sent)
+ self.assertLess(sent, status['callStartedMonotonicNs'])
+ self.assertLess(status['callFinishedMonotonicNs'], observation['callFinishedMonotonicNs'])
+ self.assertFalse(dispatch['SDKCallDelayed'] or dispatch['publicOverlapProved'] or dispatch['canonicalCasePassClaimed'])
+
+ def test_notification_never_waits_for_an_observer_or_changes_an_ack(self):
+ binding, _, failures = self.server(hold_copy=False)
+ probe = race.DispatchProbe(binding, credential_pair=('test-access', 'test-secret'))
+ plan = race.operation_plan(binding['bucketPrefix'], 'copy-source', [])
+ result = probe.s3(plan[0]['operation'], plan[0]['params'])
+ self.assertTrue(result['acknowledged'])
+ sent = probe.dispatched.get_nowait()
+ self.assertLess(result['callStartedMonotonicNs'], sent)
+ self.assertLess(sent, result['callFinishedMonotonicNs'])
+ self.assertEqual(result['response']['CopyObjectResult']['ETag'], '"copy-ack"')
+ self.assertFalse(failures)
+
+ def test_frozen_sixteen_tag_writes_require_all_acks_and_one_nested_running_sample(self):
+ for summary in ('running', 'finished'):
+ with self.subTest(summary=summary), tempfile.TemporaryDirectory() as directory:
+ binding, sampled, failures = self.server(summary=summary, first_only=True)
+ writer_probe = race.DispatchProbe(binding, credential_pair=('test-access', 'test-secret'))
+ admin_probe = race.Probe(binding, credential_pair=('test-access', 'test-secret'))
+ original_admin = admin_probe.admin
+ def admin(*args):
+ value = original_admin(*args); sampled.set(); return value
+ admin_probe.admin = admin
+ writer = race.RaceRecorder(writer_probe, Path(directory) / 'foreground-ACKs', versioned=False)
+ plan = race.operation_plan(binding['bucketPrefix'], 'update-tags', [])
+ for step in plan: step['branch'] = 'update-tags'
+ records = {}; fleet = SimpleNamespace(record=lambda label, value: records.setdefault(label, value))
+ responses, accepted = race.race(writer, plan, admin_probe, fleet,
+ '/rustfs/admin/v3/heal/' + binding['bucketPrefix'], 'original-token', race.COPY_REFERENCE)
+ self.assertEqual(len(responses), 16)
+ self.assertEqual(len(list(writer.output.glob('*-request.json'))), 16)
+ acks = [json.loads(p.read_text()) for p in writer.output.glob('*-ack.json')]
+ self.assertEqual(len(acks), 16)
+ self.assertTrue(all(row['operationCheck'] == 'PASS' for row in acks))
+ self.assertEqual(accepted, summary == 'running')
+ proof = records['public-call-overlap-proof']
+ self.assertEqual(proof['overlapRequirement'], 'at-least-one-of-frozen-16-tag-updates')
+ self.assertEqual(proof['requiredCallOverlapObserved'], summary == 'running')
+ self.assertFalse(proof['everyMutationOverlapped'])
+ self.assertFalse(failures)
+
+
+class G06SourceTests(unittest.TestCase):
+ bucket = 'rv-heal-0123456789abcdef'
+
+ def baseline(self):
+ return {'entries': [
+ {'key': key, 'versionId': 'null', 'isLatest': True, 'deleteMarker': False,
+ 'payload': {'size': size, 'sha256': role},
+ 'properties': {'ETag': '"' + role + '"', 'ContentLength': size,
+ 'ContentType': 'application/' + role, 'Metadata': {'role': role}},
+ 'tags': [{'Key': 'role', 'Value': role}]}
+ for key, role, size in [(race.G06_TARGET_KEY, 'selected', 17),
+ (race.G06_REFERENCE_KEY, 'other', 23)]]}
+
+ def test_frozen_ds_keys_and_seed_use_the_existing_workload(self):
+ self.assertEqual(race.G06_TARGET_KEY, '中文/文件/0252.bin')
+ self.assertEqual(race.G06_REFERENCE_KEY, 'literal%2Fslash/0254.bin')
+ recorder = Mock()
+ recorder.probe = SimpleNamespace(binding={'bucketPrefix': self.bucket})
+ recorder.call.return_value = {'ETag': '"ack"'}
+ with patch.object(race, 'SIZE', 31):
+ expected = race.seed_fixture(recorder, self.bucket, 'copy-source')
+ prefixes = ('中文/文件', 'space key', 'literal%2Fslash', 'actual/slash', 'a/child', 'ab/control', 'b/other')
+ keys = [f'{prefixes[i % 7]}/{i:04d}.bin' for i in range(257)]
+ self.assertEqual([r['key'] for r in expected], keys)
+ puts = [c for c in recorder.call.call_args_list if c.args[0] == 'put_object']
+ self.assertEqual(len(puts), 257)
+ self.assertEqual([c.args[1]['Key'] for c in puts], keys)
+ self.assertEqual(puts[252].args[1]['Metadata'], {'heal-fixture': 'DS', 'nonce': '252-0'})
+ self.assertEqual(puts[254].args[1]['Tagging'], 'suite=heal&fixture=DS')
+
+ def test_copy_source_or_destination_is_the_original_missing_selected_key(self):
+ for branch in ('copy-source', 'copy-destination'):
+ with self.subTest(branch=branch):
+ params = race.operation_plan(self.bucket, branch, self.baseline()['entries'])[0]['params']
+ source, destination = ((race.G06_TARGET_KEY, race.G06_REFERENCE_KEY) if branch == 'copy-source'
+ else (race.G06_REFERENCE_KEY, race.G06_TARGET_KEY))
+ self.assertEqual(params['Key'], destination)
+ self.assertEqual(params['CopySource'], {'Bucket': self.bucket, 'Key': source, 'VersionId': 'null'})
+
+ def test_copy_final_s3_follows_source_content_metadata_and_tags(self):
+ for branch in ('copy-source', 'copy-destination'):
+ with self.subTest(branch=branch):
+ before = self.baseline()
+ plan = race.operation_plan(self.bucket, branch, before['entries'])
+ source, destination = ((race.G06_TARGET_KEY, race.G06_REFERENCE_KEY) if branch == 'copy-source'
+ else (race.G06_REFERENCE_KEY, race.G06_TARGET_KEY))
+ original = {r['key']: r for r in before['entries']}
+ after = race.final_inventory(before, branch, plan, [{'CopyObjectResult': {'ETag': '"copied"'}}])
+ actual = {r['key']: r for r in after['entries']}
+ self.assertEqual(actual[source], original[source])
+ self.assertEqual(actual[destination]['payload'], original[source]['payload'])
+ self.assertEqual(actual[destination]['tags'], original[source]['tags'])
+ self.assertEqual(actual[destination]['properties'], {**original[source]['properties'], 'ETag': '"copied"'})
+
+ def scene(self, branch):
+ before = physical_fixtures.HealShardOracleTest().snapshot()
+ for value in before.values():
+ original = value['files']
+ value['files'] = []
+ for meta, part in zip(original[::3], original[1::3], strict=True):
+ for key, generation, role in [(race.G06_TARGET_KEY, 100, 'selected'),
+ (race.G06_REFERENCE_KEY, 200, 'other')]:
+ m, p = copy.deepcopy(meta), copy.deepcopy(part)
+ m['path'] = f'{self.bucket}/{key}/xl.meta'
+ m['metadata']['key'] = key
+ m['metadata']['versions'][0]['dataDir'] = str(uuid.UUID(int=generation))
+ p['path'] = f'{self.bucket}/{key}/{uuid.UUID(int=generation)}/part.1'
+ p['sha256'] += '-' + role
+ value['files'] += [m, p]
+ expected = self.baseline()['entries']
+ mapped = race.map_fixture(before, self.bucket, expected)
+ source, destination = ((race.G06_TARGET_KEY, race.G06_REFERENCE_KEY) if branch == 'copy-source'
+ else (race.G06_REFERENCE_KEY, race.G06_TARGET_KEY))
+ after = copy.deepcopy(before)
+ for value in after.values():
+ by_path = {(f['disk'], f['path']): f for f in value['files']}
+ for f in value['files']:
+ if not f['path'].startswith(f'{self.bucket}/{destination}/'): continue
+ if f['path'].endswith('/xl.meta'):
+ f['metadata']['versions'][0]['dataDir'] = str(uuid.UUID(int=300))
+ else:
+ slot = 100 if source == race.G06_TARGET_KEY else 200
+ f['sha256'] = by_path[f['disk'], f'{self.bucket}/{source}/{uuid.UUID(int=slot)}/part.1']['sha256']
+ f['path'] = f'{self.bucket}/{destination}/{uuid.UUID(int=300)}/part.1'
+ return before, after, mapped, {'entries': expected}, destination
+
+ def test_real_copy_transition_accepts_both_directions_before_get(self):
+ for branch in ('copy-source', 'copy-destination'):
+ with self.subTest(branch=branch):
+ before, after, mapped, final, _ = self.scene(branch)
+ _, proof = race.physical_transition(before, after, self.bucket, mapped, final, branch)
+ self.assertTrue(proof['physicalProofBeforeGETOrRescue'])
+
+ def test_copy_rejects_wrong_source_shards_and_resurrected_destination_generation(self):
+ for branch in ('copy-source', 'copy-destination'):
+ for fault in ('wrong-shard', 'old-generation', 'source-shard'):
+ with self.subTest(branch=branch, fault=fault):
+ before, after, mapped, final, destination = self.scene(branch)
+ part = next(f for f in after['node1']['files'] if f['path'].startswith(f'{self.bucket}/{destination}/')
+ and f['path'].endswith('/part.1'))
+ if fault == 'wrong-shard': part['sha256'] = 'wrong-copy'
+ elif fault == 'old-generation':
+ old = next(f for f in before['node1']['files'] if f['path'].startswith(f'{self.bucket}/{destination}/')
+ and f['path'].endswith('/part.1'))
+ after['node1']['files'].append(copy.deepcopy(old))
+ else:
+ source = race.G06_TARGET_KEY if branch == 'copy-source' else race.G06_REFERENCE_KEY
+ original = next(f for f in after['node1']['files'] if f['path'].startswith(f'{self.bucket}/{source}/')
+ and f['path'].endswith('/part.1'))
+ original['sha256'] = 'changed-copy-source'
+ with self.assertRaises(ValueError):
+ race.physical_transition(before, after, self.bucket, mapped, final, branch)
+
+ def metadata_scene(self):
+ before, after, mapped, final, _ = self.scene('copy-destination')
+ original = {(f['disk'], f['path']): f for f in before['node1']['files']}
+ for f in after['node1']['files']:
+ if f['path'].startswith(f'{self.bucket}/{race.G06_TARGET_KEY}/') and f['path'].endswith('/part.1'):
+ old_path = f'{self.bucket}/{race.G06_TARGET_KEY}/{uuid.UUID(int=100)}/part.1'
+ f['sha256'] = original[f['disk'], old_path]['sha256']
+ return before, after, mapped, final
+
+ def test_metadata_and_tags_allow_one_current_directory_with_original_coding_bytes(self):
+ # The frozen generation_proof requires replacement only for COPY and
+ # overwrite. Metadata/tag mutations still require the complete coding set.
+ for branch in ('replace-metadata', 'update-tags'):
+ for replace in (False, True):
+ with self.subTest(branch=branch, replace=replace):
+ before, after, mapped, final = self.metadata_scene()
+ if not replace: after = copy.deepcopy(before)
+ _, proof = race.physical_transition(before, after, self.bucket, mapped, final, branch)
+ self.assertTrue(proof['physicalProofBeforeGETOrRescue'])
+
+ def test_metadata_and_tags_reject_incomplete_mixed_or_wrong_coding_generations(self):
+ for branch in ('replace-metadata', 'update-tags'):
+ for fault in ('wrong-shard', 'wrong-index', 'missing-part', 'mixed-generation', 'old-leaf', 'control'):
+ with self.subTest(branch=branch, fault=fault):
+ before, after, mapped, final = self.metadata_scene()
+ rows = after['node1']['files']
+ parts = [f for f in rows if f['path'].startswith(f'{self.bucket}/{race.G06_TARGET_KEY}/')
+ and f['path'].endswith('/part.1')]
+ if fault == 'wrong-shard': parts[0]['sha256'] = 'wrong'
+ elif fault == 'wrong-index':
+ parts[0]['sha256'], parts[1]['sha256'] = parts[1]['sha256'], parts[0]['sha256']
+ elif fault == 'missing-part': rows.remove(parts[0])
+ elif fault == 'mixed-generation':
+ meta = next(f for f in rows if f['disk'] == 0
+ and f['path'] == f'{self.bucket}/{race.G06_TARGET_KEY}/xl.meta')
+ meta['metadata']['versions'][0]['dataDir'] = str(uuid.UUID(int=100))
+ elif fault == 'old-leaf':
+ rows.append(copy.deepcopy(next(f for f in before['node1']['files']
+ if f['path'].startswith(f'{self.bucket}/{race.G06_TARGET_KEY}/')
+ and f['path'].endswith('/part.1'))))
+ else:
+ next(f for f in rows if f['path'].startswith(f'{self.bucket}/{race.G06_REFERENCE_KEY}/')
+ and f['path'].endswith('/part.1'))['sha256'] = 'changed-control'
+ with self.assertRaises(ValueError):
+ race.physical_transition(before, after, self.bucket, mapped, final, branch)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/test_validation_heal_incarnation.py b/test_validation_heal_incarnation.py
new file mode 100644
index 00000000..977ba93b
--- /dev/null
+++ b/test_validation_heal_incarnation.py
@@ -0,0 +1,190 @@
+"""C11 must prove real S3 deletion and preserve successor responsibility."""
+
+import copy
+import json
+import os
+from pathlib import Path
+import tempfile
+import unittest
+from unittest.mock import patch
+import uuid
+
+from scripts.validation_heal import incarnation as suite
+
+
+def version(number):
+ return str(uuid.UUID(int=number))
+
+
+def manifest(base=1):
+ return [
+ {"key": "objects/a", "versionId": version(base), "deleteMarker": False, "isLatest": False,
+ "payload": {"size": 1, "sha256": "a" * 64}},
+ {"key": "objects/a", "versionId": version(base + 1), "deleteMarker": True, "isLatest": True},
+ ]
+
+
+class DeletionRecorder:
+ def __init__(self, *, wrong_version=False, remaining=False, unknown_absence=False, pending_upload=False):
+ self.probe = self
+ self.calls = []
+ self.inventory_calls = 0
+ self.wrong_version, self.remaining = wrong_version, remaining
+ self.unknown_absence, self.pending_upload = unknown_absence, pending_upload
+
+ def inventory(self, bucket):
+ self.inventory_calls += 1
+ return {"entries": manifest() if self.inventory_calls == 1 or self.remaining else []}
+
+ def s3(self, op, params):
+ return {"acknowledged": False, "resultUnknown": self.unknown_absence,
+ "response": {"ResponseMetadata": {"HTTPStatusCode": 404}}}
+
+ def call(self, op, params):
+ self.calls.append((op, copy.deepcopy(params)))
+ if op == "delete_object":
+ return {"VersionId": version(99) if self.wrong_version else params["VersionId"],
+ "DeleteMarker": params["VersionId"] == version(2),
+ "ResponseMetadata": {"HTTPStatusCode": 204}}
+ if op == "list_multipart_uploads":
+ return {"IsTruncated": False, "Uploads": [{"UploadId": "still-pending"}] if self.pending_upload else []}
+ if op == "delete_bucket": return {"ResponseMetadata": {"HTTPStatusCode": 204}}
+ raise AssertionError("unexpected mutation " + op)
+
+
+class IncarnationTests(unittest.TestCase):
+ def test_normal_deletion_accounts_for_every_data_version_and_marker(self):
+ recorder = DeletionRecorder()
+ proof = suite.delete_old_bucket(recorder, "bucket", manifest())
+ self.assertFalse(proof["internalNamespaceRemovalUsed"])
+ self.assertEqual(proof["deleted"], [{k: row[k] for k in ("key", "versionId", "deleteMarker")} for row in manifest()])
+ self.assertEqual([op for op, _ in recorder.calls],
+ ["delete_object", "delete_object", "list_multipart_uploads", "delete_bucket"])
+
+ def test_wrong_identity_remaining_members_and_unknown_absence_fail_closed(self):
+ for kwargs in ({"wrong_version": True}, {"remaining": True}, {"unknown_absence": True}, {"pending_upload": True}):
+ with self.subTest(kwargs=kwargs), self.assertRaises(ValueError):
+ suite.delete_old_bucket(DeletionRecorder(**kwargs), "bucket", manifest())
+
+ def test_duplicate_null_nil_and_wrong_kind_cannot_be_old_responsibility(self):
+ bad = [manifest() + [manifest()[0]], [*manifest(), {**manifest()[0], "versionId": "null"}],
+ [{**manifest()[0], "versionId": version(0)}, manifest()[1]],
+ [{**manifest()[0], "deleteMarker": 0}, manifest()[1]]]
+ for value in bad:
+ with self.subTest(value=value), self.assertRaises(ValueError): suite.old_manifest(value)
+
+ def snapshot(self, expected):
+ versions = [{"versionId": row["versionId"], "deleted": row["deleteMarker"], "isLatest": row["isLatest"]}
+ for row in expected]
+ return {"vm000": {"formats": ["fixed-disk-identities"], "files": [
+ {"disk": disk, "path": "bucket/objects/a/xl.meta", "size": 10, "sha256": "b" * 64,
+ "metadata": {"bucket": "bucket", "key": "objects/a", "versions": copy.deepcopy(versions)}}
+ for disk in range(4)]}}
+
+ def test_rejoined_metadata_cannot_resurrect_an_old_version(self):
+ expected = manifest(3)
+ before = self.snapshot(expected)
+ after = copy.deepcopy(before)
+ after["vm000"]["files"][0]["metadata"]["versions"].append(
+ {"versionId": version(1), "deleted": False, "isLatest": False})
+ with self.assertRaisesRegex(ValueError, "resurrected"):
+ suite.successor_physical(before, after, "bucket", expected, manifest(), ("vm000", 0))
+
+ def test_missing_successor_marker_and_duplicate_version_are_rejected(self):
+ expected = manifest(3)
+ before = self.snapshot(expected)
+ for change in ("missing", "duplicate"):
+ after = copy.deepcopy(before)
+ rows = after["vm000"]["files"][1]["metadata"]["versions"]
+ rows.pop() if change == "missing" else rows.append(copy.deepcopy(rows[0]))
+ with self.subTest(change=change), self.assertRaises(ValueError):
+ suite.successor_physical(before, after, "bucket", expected, manifest(), ("vm000", 0))
+
+ def test_original_gated_member_bytes_and_control_bucket_cannot_change(self):
+ before = self.snapshot(manifest())
+ after = copy.deepcopy(before)
+ after["vm000"]["files"][0]["sha256"] = "c" * 64
+ with self.assertRaisesRegex(ValueError, "gated old metadata"):
+ suite.stale_member_unchanged(before, after, ("vm000", 0))
+ before = self.snapshot(manifest(3))
+ before["vm000"]["files"].append({"disk": 0, "path": "bucket-control/a/part.1", "sha256": "a" * 64, "size": 1})
+ after = copy.deepcopy(before)
+ after["vm000"]["files"][-1]["sha256"] = "b" * 64
+ with self.assertRaisesRegex(ValueError, "control bucket"):
+ suite.successor_physical(before, after, "bucket", manifest(3), manifest(), ("vm000", 0))
+
+ def test_successor_configuration_requires_known_responses_and_its_exact_tag(self):
+ class ConfigProbe:
+ def s3(self, op, params):
+ return {"acknowledged": True, "resultUnknown": False, "response": {
+ "Status": "Enabled", "TagSet": [{"Key": "validation-generation", "Value": "successor"}]}}
+ probe = ConfigProbe()
+ self.assertIn("tagging", suite.config(probe, "bucket"))
+ with patch.object(probe, "s3", return_value={"acknowledged": True, "resultUnknown": True, "response": {"Status": "Enabled"}}):
+ with self.assertRaises(ValueError): suite.config(probe, "bucket")
+
+ def test_partial_metadata_branch_never_promotes_full_canonical_case(self):
+ with tempfile.TemporaryDirectory() as temp:
+ root = Path(temp)
+ candidate = root / "candidate.json"
+ candidate.write_text(json.dumps({"source_sha": "a" * 40}))
+ args = ["incarnation.py", "--case-id", suite.CASE_ID, "--candidate", str(candidate),
+ "--evidence", str(root), "--rc-bin", str(root / "rc"), "--tool-directory", str(root / "tools"),
+ "--testing-commit", "a" * 40, "--binary-sha", "b" * 64]
+ def passed(args, topology, nodes, candidate):
+ return {"topology": topology, "fixture": "DV", "branch": "old-metadata-rejoin", "verdict": "PASS", "cleanup": "PASS"}
+ with patch("sys.argv", args), patch.dict(os.environ, {"GITHUB_RUN_ID": "12345"}), \
+ patch.object(suite, "verify_manager", return_value={"canonicalCasePassClaimed": False}), \
+ patch.object(suite, "verify_storage", return_value={"exactStorageCommitBarrierObserved": True,
+ "canonicalFleetBranchesExecuted": False, "canonicalCasePassClaimed": False}), \
+ patch.object(suite, "run_metadata_rejoin", side_effect=passed), \
+ patch.object(suite, "run_replay", side_effect=lambda args, topology, nodes, branch, candidate: {
+ "topology": topology, "fixture": "DV", "branch": branch, "verdict": "PASS", "cleanup": "PASS"}):
+ self.assertEqual(suite.main(), 1)
+ result = json.loads((root / "healthy-summary.json").read_text())
+ self.assertFalse(result["complete"])
+ self.assertFalse(result["canonicalCasePassClaimed"])
+ self.assertTrue(result["diagnosticBranchesPassed"])
+ self.assertEqual(len(result["results"]), 8)
+ self.assertEqual(sum(row["verdict"] == "PASS" for row in result["results"]), 6)
+ native = [row for row in result['results'] if row['branch'] == 'native-incarnation-fence']
+ self.assertEqual(len(native), 2)
+ self.assertTrue(all(row['verdict'] == 'PREREQUISITE_PASS' and row['topology'] is None
+ and not row['fleetBranchExecuted'] for row in native))
+ self.assertTrue((root / 'independent-native-storage-commit.json').is_file())
+
+ def test_clean_finalization_cannot_hide_a_failed_replay_branch(self):
+ with tempfile.TemporaryDirectory() as temp:
+ root = Path(temp); candidate = root / "candidate.json"
+ candidate.write_text(json.dumps({"source_sha": "a" * 40}))
+ args = ["incarnation.py", "--case-id", suite.CASE_ID, "--candidate", str(candidate),
+ "--evidence", str(root), "--rc-bin", str(root / "rc"), "--tool-directory", str(root / "tools"),
+ "--testing-commit", "a" * 40, "--binary-sha", "b" * 64]
+ with patch("sys.argv", args), patch.dict(os.environ, {"GITHUB_RUN_ID": "12345"}), \
+ patch.object(suite, "verify_manager", return_value={}), patch.object(suite, "verify_storage", return_value={}), \
+ patch.object(suite, "run_metadata_rejoin", side_effect=lambda args, topology, nodes, candidate: {
+ "topology": topology, "branch": "old-metadata-rejoin", "verdict": "PASS", "cleanup": "PASS"}), \
+ patch.object(suite, "run_replay", side_effect=lambda args, topology, nodes, branch, candidate: {
+ "topology": topology, "branch": branch, "verdict": "FAIL", "cleanup": "PASS"}):
+ self.assertEqual(suite.main(), 1)
+ result = json.loads((root / "healthy-summary.json").read_text())
+ self.assertEqual(result["cleanup"], "PASS")
+ self.assertFalse(result["diagnosticBranchesPassed"])
+ self.assertFalse(result["complete"])
+ self.assertFalse(result["canonicalCasePassClaimed"])
+
+ def test_unverified_storage_artifact_refuses_before_fleet_mutation(self):
+ with tempfile.TemporaryDirectory() as temp:
+ root = Path(temp); candidate = root / 'candidate.json'
+ candidate.write_text(json.dumps({'source_sha': 'a' * 40}))
+ args = ['incarnation.py', '--case-id', suite.CASE_ID, '--candidate', str(candidate),
+ '--evidence', str(root), '--rc-bin', str(root / 'rc'), '--tool-directory', str(root / 'tools'),
+ '--testing-commit', 'a' * 40, '--binary-sha', 'b' * 64]
+ with patch('sys.argv', args), patch.object(suite, 'verify_manager', return_value={}), \
+ patch.object(suite, 'verify_storage', side_effect=ValueError('unverified artifact')), \
+ patch.object(suite, 'run_metadata_rejoin') as mutate:
+ with self.assertRaisesRegex(ValueError, 'unverified artifact'): suite.main()
+ mutate.assert_not_called()
+
+
+if __name__ == "__main__": unittest.main()
diff --git a/test_validation_heal_incarnation_native.py b/test_validation_heal_incarnation_native.py
new file mode 100644
index 00000000..c9e1f446
--- /dev/null
+++ b/test_validation_heal_incarnation_native.py
@@ -0,0 +1,45 @@
+"""C11's hosted native proof cannot be promoted to fleet or replay coverage."""
+
+import json
+from pathlib import Path
+import tempfile
+import unittest
+
+from scripts.validation_heal import incarnation_native as suite
+import test_validation_heal_native as fixtures
+
+
+class IncarnationNativeTests(unittest.TestCase):
+ def fixture(self, root):
+ fixtures.evidence_fixture(root)
+ api = {'id': 12345, 'run_attempt': 1, 'status': 'completed', 'conclusion': 'success',
+ 'head_sha': 'b' * 40, 'path': '.github/workflows/rustfs-validation-tools.yml'}
+ (root / 'tool-run.json').write_text(json.dumps(api))
+ return {'source_sha': suite.suite.INCARNATION_CANDIDATE}, api
+
+ def test_full_hosted_storage_suite_is_scoped_to_native_prerequisites(self):
+ with tempfile.TemporaryDirectory() as temp:
+ root = Path(temp); candidate, _ = self.fixture(root)
+ proof = suite.verify(root, candidate, 'a' * 40)
+ self.assertEqual(proof['exactTests'], 9)
+ self.assertEqual(len(proof['casePrerequisiteTests']), 3)
+ self.assertTrue(proof['exactStorageCommitBarrierObserved'])
+ for field in ('canonicalFleetBranchesExecuted', 'checkpointMrfReplayClaimed',
+ 'physicalPowerLossClaimed', 'fleetPassClaimed', 'canonicalCasePassClaimed'):
+ self.assertFalse(proof[field])
+
+ def test_hosted_api_attempt_candidate_and_external_helper_must_match(self):
+ with tempfile.TemporaryDirectory() as temp:
+ root = Path(temp); candidate, api = self.fixture(root)
+ path = root / 'tool-run.json'
+ for field, value in [('id', True), ('id', 12346), ('run_attempt', 2), ('status', 'in_progress'),
+ ('conclusion', 'failure'), ('head_sha', 'main'), ('path', '.github/workflows/ci.yml')]:
+ path.write_text(json.dumps({**api, field: value}))
+ with self.subTest(field=field, value=value), self.assertRaises(ValueError):
+ suite.verify(root, candidate, 'a' * 40)
+ path.write_text(json.dumps(api))
+ for wrong_candidate, helper in [({'source_sha': suite.suite.CANDIDATE}, 'a' * 40), (candidate, 'c' * 40)]:
+ with self.assertRaises(ValueError): suite.verify(root, wrong_candidate, helper)
+
+
+if __name__ == '__main__': unittest.main()
diff --git a/test_validation_heal_incarnation_replay.py b/test_validation_heal_incarnation_replay.py
new file mode 100644
index 00000000..f8ff0fbd
--- /dev/null
+++ b/test_validation_heal_incarnation_replay.py
@@ -0,0 +1,480 @@
+"""C11 original journal replay cannot fabricate or overwrite responsibility."""
+
+import base64
+import copy
+import hashlib
+import json
+import os
+from pathlib import Path
+import tempfile
+from types import SimpleNamespace
+import unittest
+from unittest.mock import patch
+from contextlib import ExitStack
+
+from scripts.validation_heal import incarnation as suite, incarnation_replay as backend
+from scripts.validation_heal import checkpoint, journal_fault as journal, heal_fault as fault, mrf_records, healthy, fleet_node
+import test_validation_heal_checkpoint as checkpoint_fixtures
+from test_validation_heal_checkpoint import BUCKET, TOKEN, INCARNATION, encoded
+import test_validation_heal_mrf as mrf_fixtures
+
+
+SUCCESSOR = "52345678-1234-4234-8234-123456789abc"
+
+
+class OriginalReplayTests(unittest.TestCase):
+ def setUp(self):
+ self.directory = tempfile.TemporaryDirectory()
+ self.addCleanup(self.directory.cleanup)
+ self.state = Path(self.directory.name).resolve() / "state"
+ self.state.mkdir()
+ self.roots = [self.state.parent / str(disk) for disk in range(4)]
+ for root in self.roots:
+ (root / "data/.rustfs.sys/buckets").mkdir(parents=True)
+ (root / "quarantine").mkdir()
+ self.mount = patch.object(fault, "mount_id", return_value=7)
+ self.mount.start()
+ self.addCleanup(self.mount.stop)
+ self.owner = {"runId": "gh-12345-1-h1", "binarySha256": "a" * 64,
+ "bucket": BUCKET, "runtimeProfile": "incarnation-checkpoint"}
+ self.request = {"runId": self.owner["runId"], "family": "checkpoint", "taskId": TOKEN,
+ "bucketIncarnationId": INCARNATION}
+ self.observed, _, _ = checkpoint_fixtures.AdminCheckpointTests().fixture()
+ roots, _ = checkpoint_fixtures.AdminCheckpointTests().root_fixture()
+ self.rows = self.observed["node"]["records"] + roots["node"]["records"]
+ for row in self.rows:
+ if row["present"]:
+ (self.roots[row["disk"]] / "data" / row["relativePath"]).write_bytes(base64.b64decode(row["rawBase64"]))
+ self.bind_expected()
+
+ def bind_expected(self):
+ binding = backend.identity(self.owner, self.request, journal)
+ self.request["expectedRecords"] = suite.record_digests(backend.census(self.roots, binding, journal, fault))
+
+ def capture(self):
+ return backend.capture(self.state, self.roots, self.owner, self.request, journal, fault, mrf_records, fleet_node.checked)
+
+ def install(self):
+ return backend.install(self.state, self.roots, {**self.owner, "incarnationPhase": "transition"},
+ self.request, journal, fault)
+
+ def test_exact_original_transaction_detaches_then_replays_and_exports_current_and_archived_bytes(self):
+ archived = self.capture()
+ self.assertTrue(archived["originalProductBytes"])
+ self.assertFalse(archived["checkpointCursorClaimed"])
+ self.assertTrue(all(not row["present"] for row in backend.census(self.roots, archived, journal, fault)))
+ self.assertTrue(self.install()["originalArchiveRetained"])
+ finalized = backend.finalize(self.state, self.roots, self.owner, self.request, journal, fault)
+ self.assertEqual(finalized["currentRecords"], archived["records"])
+ self.assertFalse(finalized["productRepairClaimed"])
+ self.assertFalse(finalized["canonicalCasePassClaimed"])
+
+ def test_archive_tampering_wrong_generation_and_existing_progress_cannot_be_installed(self):
+ self.capture()
+ for key, changed in (("bucketIncarnationId", SUCCESSOR), ("runId", "gh-99999-1-h1"), ("taskId", SUCCESSOR)):
+ with self.subTest(key=key), self.assertRaises(ValueError):
+ backend.install(self.state, self.roots, self.owner, {**self.request, key: changed}, journal, fault)
+ path = self.roots[1] / "data" / journal.names("root", TOKEN)[1]
+ path.write_bytes(b"product-terminal")
+ with self.assertRaisesRegex(ValueError, "overwrite"): self.install()
+ self.assertEqual(path.read_bytes(), b"product-terminal")
+ path.unlink()
+ archive = self.roots[1] / "quarantine" / backend.archive_name(journal.names("checkpoint", TOKEN)[0])
+ archive.write_bytes(b"tampered")
+ with self.assertRaisesRegex(ValueError, "archive changed"): self.install()
+
+ def test_incomplete_capture_installation_and_lost_ack_keep_namespace_dirty(self):
+ self.capture()
+ with self.assertRaises(FileNotFoundError): backend.finalize(self.state, self.roots, self.owner, self.request, journal, fault)
+ fleet = healthy.Fleet(["node"], self.state / "evidence", self.owner["runId"], runtime_profile="incarnation-checkpoint")
+ fleet.incarnation_replays = [("node", self.request)]
+ fleet.reserved = ["node"]
+ def remote(node, action, **params):
+ if action == "incarnation-finalize": raise FileNotFoundError("installation incomplete")
+ if action == "cleanup": self.fail("dirty original archive must not be deleted")
+ return {}
+ with patch.object(fleet, "stop"), patch.object(fleet, "remote", side_effect=remote):
+ self.assertFalse(fleet.cleanup())
+ self.assertEqual(len(fleet.incarnation_replays), 1)
+ self.assertTrue((self.state / "C11-replay.json").exists())
+
+ def test_bad_digest_foreign_terminal_and_incomplete_pair_refuse_before_detachment(self):
+ original = copy.deepcopy(self.request)
+ self.request["expectedRecords"][0]["sha256"] = "f" * 64
+ with self.assertRaisesRegex(ValueError, "digests"): self.capture()
+ self.request = original
+ terminal = self.roots[0] / "data" / journal.names("root", TOKEN)[1]
+ terminal.write_bytes(b"terminal")
+ self.bind_expected()
+ with self.assertRaisesRegex(ValueError, "terminal"): self.capture()
+ terminal.unlink()
+ resume = self.roots[1] / "data" / journal.names("checkpoint", TOKEN)[1]
+ resume.unlink()
+ self.bind_expected()
+ with self.assertRaisesRegex(ValueError, "incomplete"): self.capture()
+ self.assertFalse((self.state / "C11-replay.json").exists())
+
+ def test_symlinks_and_hardlinks_refuse_without_mutating_product_bytes(self):
+ path = self.roots[1] / "data" / journal.names("checkpoint", TOKEN)[0]
+ raw = path.read_bytes()
+ saved = self.state / "outside"
+ saved.write_bytes(raw)
+ path.unlink()
+ path.symlink_to(saved)
+ with self.assertRaises(OSError): self.capture()
+ path.unlink()
+ os.link(saved, path)
+ with self.assertRaises(ValueError): self.capture()
+ self.assertEqual(saved.read_bytes(), raw)
+
+ def test_real_committed_mrf_archive_keeps_payload_commit_and_lifecycle_on_all_original_replicas(self):
+ for root in self.roots:
+ for path in (root / "data/.rustfs.sys/buckets").iterdir(): path.unlink()
+ for path in (root / "data/.rustfs.sys").glob("*.json"): path.unlink()
+ fixture = mrf_fixtures.DurableMrfTests()
+ self.owner["runtimeProfile"] = "incarnation-mrf"
+ (self.state / "rustfs").write_bytes(b"candidate")
+ self.owner["binarySha256"] = hashlib.sha256(b"candidate").hexdigest()
+ self.request.update(family="mrf", taskId=None, bucketIncarnationId=fixture.incarnation)
+ for row in fixture.observed()["records"]:
+ if row["present"]:
+ (self.roots[row["disk"]] / "data" / row["relativePath"]).write_bytes(base64.b64decode(row["rawBase64"]))
+ for root in self.roots:
+ for relative in backend.LEGACY_MRF_PATHS:
+ path = root / "data" / relative
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_bytes(fixture.journal() if relative.endswith("journal-scoped.bin") else b"")
+ self.bind_expected()
+ archived = self.capture()
+ self.assertEqual(len([row for row in archived["records"] if row["present"]]), 14)
+ self.assertTrue(all(not row["present"] for row in backend.census(self.roots, archived, journal, fault)))
+ for root in self.roots:
+ for relative in backend.LEGACY_MRF_PATHS:
+ (root / "data" / relative).write_bytes(b"")
+ receipt = self.install()
+ self.assertEqual(len(receipt["discardedEmptyTransitionJournals"]), 8)
+ self.assertEqual(backend.census(self.roots, archived, journal, fault), archived["records"])
+ observed = mrf_records.read_records(self.state, self.roots, self.owner, fleet_node.checked)
+ value = mrf_records.checkpoint(observed)
+ self.assertEqual(value["intents"][0]["lifecycle"]["responsibility_id"], fixture.responsibility)
+ self.assertEqual({row["disk"] for row in value["replicas"]}, {0, 2})
+
+ def test_unbound_or_torn_fallback_refuses_before_archiving_or_detaching(self):
+ self.owner["runtimeProfile"] = "incarnation-mrf"
+ self.request.update(family="mrf", taskId=None)
+ (self.state / "rustfs").write_bytes(b"candidate")
+ self.owner["binarySha256"] = hashlib.sha256(b"candidate").hexdigest()
+ relative = backend.LEGACY_MRF_PATHS[1]
+ path = self.roots[0] / "data" / relative
+ path.parent.mkdir(parents=True)
+ fixture = mrf_fixtures.DurableMrfTests()
+ for raw in (fixture.journal(), fixture.journal()[:-1]):
+ path.write_bytes(raw)
+ self.bind_expected()
+ with self.assertRaises(ValueError): self.capture()
+ self.assertEqual(path.read_bytes(), raw)
+ self.assertFalse((self.state / "C11-replay.json").exists())
+
+ def test_partial_install_keeps_original_archive(self):
+ self.capture()
+ original = fault.new_file
+ def interrupted(fd, name, raw):
+ if name.endswith("ahm_resume_state.json"): raise OSError("installation interrupted")
+ original(fd, name, raw)
+ with patch.object(fault, "new_file", side_effect=interrupted), self.assertRaises(OSError): self.install()
+ self.assertFalse((self.state / "C11-replay-installed.json").exists())
+ with self.assertRaisesRegex(ValueError, "overwrite"): self.install()
+ self.assertTrue(backend.archived(self.state, self.roots, self.owner, self.request, journal, fault))
+
+ def test_unresolved_s3_mutation_keeps_namespaces_and_logs(self):
+ fleet = healthy.Fleet(["node"], self.state / "evidence", self.owner["runId"])
+ fleet.reserved = ["node"]
+ with patch.object(fleet, "remote", return_value={}) as remote:
+ self.assertFalse(fleet.cleanup(preserve_namespaces=True))
+ self.assertEqual([call.args[1] for call in remote.call_args_list], ["logs"])
+
+
+class ReplayOutcomeTests(unittest.TestCase):
+ def successor_fixture(self, size):
+ expected = [{"key": f"objects/{index // 2:04d}", "versionId": str(__import__('uuid').UUID(int=index + 1)),
+ "deleteMarker": index % 2 == 1, "isLatest": index % 2 == 1,
+ **({"payload": {"size": 1, "sha256": "a" * 64}} if index % 2 == 0 else {})} for index in range(size)]
+ objects = [{"identity": {"bucket": BUCKET, "object": row["key"], "versionId": row["versionId"],
+ "bucketIncarnationId": SUCCESSOR, "poolIndex": 0, "setIndex": 0},
+ "disposition": {"state": "metadata_healthy" if row["deleteMarker"] else "verified_healthy"}}
+ for row in expected[:128]]
+ current = {"summary": "finished", "truncated": size > 128, "outcome": {
+ "execution": {"state": "completed"}, "coverage": "complete", "objectsTruncated": size > 128,
+ "objects": objects, "counters": {"processed": size, "healed": 0, "unchanged": size,
+ "skipped": 0, "failed": 0, "unknown": 0, "attemptFailures": 0, "overflowed": False}}}
+ return current, BUCKET, expected
+
+ def test_successor_bounded_diagnostics_require_exact_independent_scope_and_typed_counters(self):
+ for size in (128, 129, 280):
+ self.assertEqual(suite.successor_receipts(*self.successor_fixture(size))[
+ "boundedDiagnosticsAreCompleteManifest"], size <= 128)
+ for change in ("counter", "truncated", "duplicate", "foreign", "old-generation", "unknown", "repair", "kind"):
+ current, bucket, expected = self.successor_fixture(280)
+ outcome = current["outcome"]
+ if change == "counter": outcome["counters"]["failed"] = False
+ elif change == "truncated": outcome["objectsTruncated"] = False
+ elif change == "duplicate": outcome["objects"][-1] = copy.deepcopy(outcome["objects"][0])
+ elif change == "foreign": outcome["objects"][0]["identity"]["bucket"] = "other"
+ elif change == "old-generation": outcome["objects"][0]["identity"]["bucketIncarnationId"] = INCARNATION
+ else: outcome["objects"][0]["disposition"]["state"] = {
+ "unknown": "unknown", "repair": "repaired", "kind": "metadata_healthy"}[change]
+ with self.subTest(change=change), self.assertRaises(ValueError):
+ suite.successor_receipts(current, bucket, expected)
+
+ def checkpoint_fixture(self):
+ observed, _, _ = checkpoint_fixtures.AdminCheckpointTests().fixture()
+ frozen = checkpoint.owned_checkpoint(observed, TOKEN, BUCKET)
+ root, _ = checkpoint_fixtures.AdminCheckpointTests().root_fixture()
+ frozen["rootAdmission"] = checkpoint.owned_root_admission(root, TOKEN)
+ outcome = copy.deepcopy(frozen["value"]["admin"]["outcome"])
+ outcome["execution"] = {"state": "aborted", "reason": "untraversable"}
+ current = {"summary": "stopped", "settings": checkpoint.OPTIONS, "truncated": False,
+ "detail": "stale_bucket_incarnation: old admission", "outcome": outcome}
+ names = journal.names("root", TOKEN)
+ terminal = {"schema": 1, "task_id": TOKEN, "heal_type": frozen["rootAdmission"]["value"]["heal_type"],
+ "options": frozen["rootAdmission"]["value"]["options"], "status": {"Failed": {"error": current["detail"]}}}
+ report = {"schema": 1, "terminal": terminal, "outcome": outcome}
+ root["node"]["records"][3] = {"disk": 1, "relativePath": names[0], "present": False}
+ root["node"]["records"][4].update(encoded(terminal))
+ root["node"]["records"][5].update(encoded(report))
+ return current, frozen, root
+
+ def test_checkpoint_rejection_keeps_exact_original_progress_and_durable_owner(self):
+ current, frozen, roots = self.checkpoint_fixture()
+ self.assertTrue(suite.checkpoint_rejection(current, frozen)["originalProgressRetained"])
+ self.assertTrue(suite.checkpoint_terminal(roots, frozen, current)["durableFailureRetained"])
+ for change in ("success", "generic-error", "counter", "foreign-object"):
+ current, frozen, _ = self.checkpoint_fixture()
+ if change == "success": current["summary"] = "finished"
+ elif change == "generic-error": current["detail"] = "InternalError"
+ elif change == "counter": current["outcome"]["counters"]["healed"] += 1
+ else: current["outcome"]["objects"] = [{"identity": {"bucketIncarnationId": SUCCESSOR}}]
+ with self.subTest(change=change), self.assertRaises(ValueError): suite.checkpoint_rejection(current, frozen)
+
+ def mrf_fixture(self):
+ fixture = mrf_fixtures.DurableMrfTests()
+ original = mrf_records.checkpoint(fixture.observed())
+ intent = original["intents"][0]
+ owner = {"node": "node", "checkpointOwner": original["owner"], "sequence": original["sequence"],
+ "responsibilityId": fixture.responsibility, "bucketIncarnationId": fixture.incarnation,
+ "intentDigest": intent["intentDigest"]}
+ value = copy.deepcopy(original)
+ value["owner"] = SUCCESSOR
+ value["sequence"] += 1
+ value["intents"][0]["lifecycle"]["state"] = {"state": "bucket_incarnation_changed",
+ "source_bucket_incarnation_id": fixture.incarnation, "observed_bucket_incarnation_id": SUCCESSOR, "detected_at_ms": 123}
+ obligation = fixture.obligation()
+ return {"node": value}, [obligation], {suite.mrf.identity(obligation): [owner]}, SUCCESSOR
+
+ def test_old_mrf_requires_durable_held_original_responsibility_without_operator_acceptance(self):
+ proof = suite.retained_mrf(*self.mrf_fixture())
+ self.assertFalse(proof["oldResponsibilityRetired"])
+ self.assertNotEqual(proof["retainedResponsibilities"][0]["originalCheckpointOwner"],
+ proof["retainedResponsibilities"][0]["currentCheckpointOwner"])
+ for change in ("missing", "active", "same-sequence", "same-owner", "nil-owner", "successor-source", "accepted", "wrong-id", "duplicate"):
+ value, obligations, bindings, incarnation = self.mrf_fixture()
+ row = value["node"]["intents"][0]
+ if change == "missing": value["node"]["intents"] = []
+ elif change == "active": row["lifecycle"]["state"] = {"state": "active"}
+ elif change == "same-sequence": value["node"]["sequence"] -= 1
+ elif change == "same-owner": value["node"]["owner"] = bindings[suite.mrf.identity(obligations[0])][0]["checkpointOwner"]
+ elif change == "nil-owner": value["node"]["owner"] = str(__import__('uuid').UUID(int=0))
+ elif change == "successor-source": row["lifecycle"]["source_bucket_incarnation_id"] = SUCCESSOR
+ elif change == "accepted": row["lifecycle"]["last_operator_acceptance"] = {"accepted": True}
+ elif change == "wrong-id": row["lifecycle"]["responsibility_id"] = SUCCESSOR
+ else: value["node"]["intents"].append(copy.deepcopy(row))
+ with self.subTest(change=change), self.assertRaises(ValueError):
+ suite.retained_mrf(value, obligations, bindings, incarnation)
+
+ def test_old_replay_cannot_change_any_successor_physical_bytes(self):
+ before = {"node": {"files": [{"sha256": "a" * 64}], "formats": []}}
+ after = copy.deepcopy(before)
+ after["node"]["files"][0]["sha256"] = "b" * 64
+ with self.assertRaisesRegex(ValueError, "persisted work changed"): suite.replay_isolation(before, after)
+
+
+class RuntimePhaseTests(unittest.TestCase):
+ def setUp(self):
+ self.directory = tempfile.TemporaryDirectory()
+ self.addCleanup(self.directory.cleanup)
+ self.state = Path(self.directory.name).resolve()
+ self.owner = {"runId": "gh-12345-1-h1", "unit": "owned-unit", "binarySha256": "a" * 64,
+ "runtimeProfile": "incarnation-mrf", "bucket": BUCKET}
+ self.roots = [self.state / str(disk) for disk in range(4)]
+ for root in self.roots: (root / "data").mkdir(parents=True)
+ self.binding = backend.identity(self.owner, {"runId": self.owner["runId"], "family": "mrf",
+ "taskId": None, "bucketIncarnationId": INCARNATION}, journal)
+ self.manifest = {**self.binding, "records": [{"disk": disk, "relativePath": relative, "present": False}
+ for disk in range(4) for relative in backend.paths(self.binding, journal)],
+ "originalProductBytes": True, "replacementBytesAccepted": False}
+ (self.state / "C11-replay.json").write_bytes(fault.encode(self.manifest))
+ self.env = self.state / "service.env"
+ self.env.write_text("\n".join(f"{key}={value}" for key, value in fleet_node.runtime_settings(self.owner).items()) + "\n")
+ original_stat = Path.stat
+ def root_stat(path, *args, **kwargs):
+ info = original_stat(path, *args, **kwargs)
+ if path != self.env: return info
+ values = list(info); values[4] = 0
+ return os.stat_result(values)
+ self.patches = [patch.object(Path, "stat", root_stat),
+ patch.object(fleet_node, "owned", side_effect=lambda _: (self.state, self.roots, self.owner.copy())),
+ patch.object(fleet_node, "stopped"), patch.object(fleet_node, "digest", return_value="a" * 64),
+ patch.dict(fleet_node.__dict__, {"incarnation_replay_backend": backend}),
+ patch.dict(fleet_node.__dict__, {"journal_fault_backend": journal}),
+ patch.object(fault, "mount_id", return_value=7)]
+ for item in self.patches:
+ item.start(); self.addCleanup(item.stop)
+
+ def phase(self, value):
+ return fleet_node.incarnation_operation({"runId": self.owner["runId"], "action": "incarnation-phase", "phase": value})
+
+ def test_only_complete_capture_transition_replay_changes_mrf_and_keeps_one_worker(self):
+ with self.assertRaisesRegex(ValueError, "complete"): self.phase("transition")
+ (self.state / "C11-replay-detached.json").write_bytes(fault.encode(self.binding))
+ receipt = self.phase("transition")
+ self.assertEqual((receipt["workerLimit"], receipt["mrfEnabled"]), ("1", "false"))
+ self.owner["incarnationPhase"] = "transition"
+ with self.assertRaisesRegex(ValueError, "refund"): self.phase("transition")
+ with self.assertRaisesRegex(ValueError, "complete"): self.phase("replay")
+ (self.state / "C11-replay-installed.json").write_bytes(fault.encode(self.binding))
+ self.assertEqual(self.phase("replay")["mrfEnabled"], "true")
+ self.owner["incarnationPhase"] = "replay"
+ fleet_node.runtime_configuration(self.state, self.owner)
+ with self.assertRaisesRegex(ValueError, "refund"): self.phase("transition")
+
+ def test_configuration_commit_interruption_refuses_process_start_before_systemd(self):
+ (self.state / "C11-replay-detached.json").write_bytes(fault.encode(self.binding))
+ original = fault.new_file
+ def interrupted(fd, name, value):
+ if name == "C11-phase.new": raise OSError("phase acknowledgement interrupted")
+ original(fd, name, value)
+ with patch.object(fault, "new_file", side_effect=interrupted), self.assertRaises(OSError): self.phase("transition")
+ self.assertFalse((self.state / "C11-phase.json").exists())
+ with patch.object(fleet_node, "command") as command, self.assertRaisesRegex(ValueError, "incomplete"):
+ fleet_node.start({"runId": self.owner["runId"]})
+ self.assertFalse(any(call.args[0] == "systemd-run" for call in command.call_args_list))
+
+ def test_checkpoint_phase_commit_interruption_refuses_start_even_when_product_knobs_are_unchanged(self):
+ self.owner["runtimeProfile"] = "incarnation-checkpoint"
+ self.binding = backend.identity(self.owner, {"runId": self.owner["runId"], "family": "checkpoint",
+ "taskId": TOKEN, "bucketIncarnationId": INCARNATION}, journal)
+ manifest = {**self.binding, "records": [{"disk": disk, "relativePath": relative, "present": False}
+ for disk in range(4) for relative in backend.paths(self.binding, journal)],
+ "originalProductBytes": True, "replacementBytesAccepted": False}
+ (self.state / "C11-replay.json").write_bytes(fault.encode(manifest))
+ (self.state / "C11-replay-detached.json").write_bytes(fault.encode(self.binding))
+ self.env.write_text("\n".join(f"{key}={value}" for key, value in fleet_node.runtime_settings(self.owner).items()) + "\n")
+ original = fault.new_file
+ def interrupted(fd, name, raw):
+ if name == "C11-phase.new": raise OSError("checkpoint phase interrupted")
+ original(fd, name, raw)
+ with patch.object(fault, "new_file", side_effect=interrupted), self.assertRaises(OSError): self.phase("transition")
+ self.assertIn("RUSTFS_HEAL_MRF_ENABLE=false", self.env.read_text())
+ self.assertIn("AUTO_TESTING_C11_PHASE=transition", self.env.read_text())
+ with patch.object(fleet_node, "command") as command, self.assertRaisesRegex(ValueError, "incomplete"):
+ fleet_node.start({"runId": self.owner["runId"]})
+ self.assertFalse(any(call.args[0] == "systemd-run" for call in command.call_args_list))
+
+ def test_substituted_markers_and_archive_refuse_before_changing_runtime(self):
+ original = self.env.read_bytes()
+ for change in ("runId", "binarySha256", "bucket", "family", "taskId", "bucketIncarnationId", "extra"):
+ bad = {**self.binding, change: "wrong"}
+ (self.state / "C11-replay-detached.json").write_bytes(fault.encode(bad))
+ with self.subTest(change=change), self.assertRaises(ValueError): self.phase("transition")
+ self.assertEqual(self.env.read_bytes(), original)
+ (self.state / "C11-replay-detached.json").write_bytes(fault.encode(self.binding))
+ (self.state / "C11-replay.json").write_bytes(fault.encode({**self.manifest, "bucketIncarnationId": SUCCESSOR}))
+ with self.assertRaises(ValueError): self.phase("transition")
+ self.assertEqual(self.env.read_bytes(), original)
+
+ def test_duplicate_config_and_caller_runtime_override_refuse(self):
+ with self.env.open("a") as stream: stream.write("RUSTFS_HEAL_MRF_ENABLE=true\n")
+ with self.assertRaises(ValueError): fleet_node.runtime_configuration(self.state, self.owner)
+ with self.assertRaisesRegex(ValueError, "arbitrary"):
+ fleet_node.incarnation_operation({"runId": self.owner["runId"], "action": "incarnation-phase",
+ "phase": "transition", "environment": {"RUSTFS_HEAL_MRF_ENABLE": "false"}})
+
+ def test_remote_record_operations_embed_all_real_backends_before_execution(self):
+ fleet = healthy.Fleet(["node"], self.state / "evidence", self.owner["runId"], runtime_profile="incarnation-mrf")
+ for action in ("incarnation-census", "incarnation-capture", "incarnation-install", "incarnation-finalize", "incarnation-phase"):
+ with self.subTest(action=action), patch.object(healthy.subprocess, "run",
+ return_value=SimpleNamespace(returncode=0, stdout="{}", stderr="")) as remote:
+ fleet.remote("node", action)
+ source = remote.call_args.kwargs["input"].rsplit("\nmain(", 1)[0]
+ namespace = {"__name__": "fleet-test"}
+ exec(compile(source, "remote-fleet", "exec"), namespace)
+ for name in ("mrf", "fault", "journal_fault_backend", "incarnation_replay_backend"):
+ self.assertIn(name, namespace)
+ self.assertTrue(callable(namespace["incarnation_replay_backend"].capture))
+
+
+class ReplayOrderingTests(unittest.TestCase):
+ def test_capture_and_install_lost_ack_are_registered_before_mutation_and_keep_incomplete_namespaces(self):
+ for failing_action in ("incarnation-capture", "incarnation-install"):
+ with self.subTest(action=failing_action), tempfile.TemporaryDirectory() as temp, ExitStack() as stack:
+ directory = Path(temp)
+ rc = directory / "rc"; rc.write_bytes(b"client")
+ args = SimpleNamespace(evidence=directory, rc_bin=rc, tool_directory=directory,
+ testing_commit="a" * 40, binary_sha="b" * 64)
+ events, fleets = [], []
+ old = [{"key": "old", "versionId": TOKEN, "deleteMarker": False, "payload": {"size": 1}},
+ {"key": "old", "versionId": SUCCESSOR, "deleteMarker": True}]
+ obligation = {"bucket": BUCKET, "key": "old", "versionId": TOKEN, "kind": 3, "poolIndex": 0, "setIndex": 0}
+ binding = {suite.mrf.identity(obligation): [{"bucketIncarnationId": INCARNATION}]}
+ class FakeFleet(healthy.Fleet):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs); fleets.append(self)
+ def deploy(self, *args): self.reserved = list(self.nodes)
+ def snapshot(self, *args, **kwargs): return {}
+ def stop(self): events.append("stop")
+ def start(self): events.append("start")
+ def remote(self, node, action, **params):
+ events.append((node, action, params))
+ if action == failing_action:
+ assert (node, {key: params[key] for key in ("family", "taskId", "bucketIncarnationId")}) in self.incarnation_replays
+ raise OSError("SSH acknowledgement lost after mutation")
+ if action == "incarnation-finalize": raise ValueError("original transaction incomplete")
+ if action == "incarnation-capture": return {"detached": True, "originalProductBytes": True, "replacementBytesAccepted": False, "records": []}
+ if action == "incarnation-census": return {"records": []}
+ if action == "drive-gate-probe": return {"readProbe": {"errno": 13}, "errno": 13, "dacBypassRemoved": True}
+ return {}
+ class FakeProbe:
+ def __init__(self, *args): pass
+ def inventory(self, *args): return {}
+ class FakeRecorder:
+ def __init__(self, probe, *args): self.probe = probe
+ def deletion(*args): events.append("delete-old-and-recreate"); return {}
+ for name, value in {"Fleet": FakeFleet, "Probe": FakeProbe, "Recorder": FakeRecorder,
+ "verify_inspector": lambda *args: {}, "verify_quorum_contract": lambda *args: None,
+ "verify_mrf_reference": lambda *args: {}, "validate_topology": lambda *args: {},
+ "seed": lambda *args: copy.deepcopy(old), "verify_seed": lambda *args: {},
+ "run_task": lambda *args, **kwargs: {}, "validate_version_receipts": lambda *args: {"bucketIncarnationId": INCARNATION},
+ "idle_fleet": lambda *args: None, "map_target": lambda *args, **kwargs: {"cohort": [{"node": "node2", "disk": 0}]},
+ "delete_old_bucket": deletion, "seed_successor": lambda *args: copy.deepcopy(old),
+ "config": lambda *args: {}, "successor_receipts": lambda *args: {"bucketIncarnationId": SUCCESSOR},
+ "successor_physical": lambda *args: []}.items():
+ stack.enter_context(patch.object(suite, name, value))
+ stack.enter_context(patch.dict(os.environ, {"GITHUB_RUN_ID": "12345"}))
+ stack.enter_context(patch.object(suite.mrf, "acknowledge", return_value=(old, [obligation], {})))
+ stack.enter_context(patch.object(suite.mrf, "read_checkpoints", return_value={}))
+ stack.enter_context(patch.object(suite.mrf, "pending_bindings", return_value=binding))
+ result = suite.run_replay(args, "H2", ["node1", "node2"], "old-mrf-replay", {"source_sha": "c" * 40, "package_sha256": "d" * 64})
+ self.assertEqual((result["verdict"], result["cleanup"]), ("FAIL", "FAIL"))
+ self.assertTrue(fleets[0].incarnation_replays, result.get("failure"))
+ self.assertFalse(any(isinstance(event, tuple) and event[1] == "cleanup" for event in events))
+ if failing_action == "incarnation-capture": self.assertNotIn("delete-old-and-recreate", events)
+ else:
+ captures = [i for i, event in enumerate(events) if isinstance(event, tuple) and event[1] == "incarnation-capture"]
+ released = next(i for i, event in enumerate(events) if isinstance(event, tuple) and event[1] == "drive-gate" and event[2]["mode"] == "clear")
+ self.assertLess(max(captures), released)
+ self.assertLess(released, events.index("delete-old-and-recreate"))
+ self.assertEqual(events[released - 1][1], "incarnation-phase")
+ self.assertEqual(events[released + 1], "start")
+
+
+if __name__ == "__main__": unittest.main()
diff --git a/test_validation_heal_interrupted_recovery.py b/test_validation_heal_interrupted_recovery.py
new file mode 100644
index 00000000..3b6c1f82
--- /dev/null
+++ b/test_validation_heal_interrupted_recovery.py
@@ -0,0 +1,275 @@
+"""Cancellation recovery rejects drift and retains every restored fixture byte."""
+import copy
+import hashlib
+import json
+from pathlib import Path
+import tempfile
+import types
+import unittest
+from unittest.mock import patch
+
+from scripts.validation_heal import interrupted_recovery as interrupted, retained_scene as retained
+from scripts.validation_heal import recover_environment as recovery
+from scripts.validation_heal.healthy import physical_content
+import test_validation_heal_retained_scene as scene_tests
+
+
+class InterruptedPlanTests(unittest.TestCase):
+ def fixture(self, root):
+ candidate = {"source_sha": "a" * 40, "package_sha256": "b" * 64}
+ artifact = root / "artifact"; case = artifact / "run" / interrupted.CASE
+ evidence = case / "evidence" / interrupted.CASE
+ result = case / "cases" / (interrupted.CASE + ".json")
+ result.parent.mkdir(parents=True)
+ result.write_text(json.dumps({"case_id": interrupted.CASE, "verdict": "RUNNING", "cleanup_status": "PENDING"}))
+ (artifact / "candidate.json").write_text(json.dumps(candidate))
+ for fixture in ("DB", "DV"):
+ for topology in ("H1", "H2"):
+ for branch in interrupted.BRANCHES:
+ if fixture == "DV" and topology == "H2" and branch in {"stale-generation", "journal-write-failure"}: continue
+ folder = evidence / fixture / topology / branch; folder.mkdir(parents=True)
+ (folder / "00001-checkpoint-branch-result.json").write_text(json.dumps({
+ "fixture": fixture, "topology": topology, "branch": branch, "verdict": "FAIL", "cleanup": "PASS"}))
+ nodes = ["vm000"] if topology == "H1" else [f"vm00{i}" for i in range(4)]
+ for i, node in enumerate(nodes):
+ (folder / f"{i+2:05d}-cleanup.json").write_text(json.dumps({"node": node, "cleanup": "PASS",
+ "unitStopped": True, "rootsRemoved": True}))
+ (folder / "00009-recovery.json").write_text(json.dumps({"cleanup": "PASS", "errors": []}))
+ folder = evidence / "DV/H2/stale-generation"; folder.mkdir(parents=True)
+ run_id, bucket = "gh-12345-1-h2", "rv-heal-0123456789abcdef"
+ binding = {"runId": run_id, "bucketPrefix": bucket, "candidateCommit": candidate["source_sha"],
+ "candidatePackageSha256": candidate["package_sha256"], "candidateBinarySha256": "c" * 64,
+ "harnessCommit": "d" * 40}
+ count = 0
+ def emit(label, value):
+ nonlocal count
+ count += 1; (folder / f"{count:05d}-{label}.json").write_text(json.dumps(value))
+ emit("binding", binding)
+ for label in ("candidate-bound-inspector", "candidate-terminal-options-contract", "preflight", "integrity-fleet-attestation"):
+ emit(label, {})
+ nodes = [f"vm00{i}" for i in range(4)]
+ owners = {n: {"runId": run_id, "bucket": bucket, "binarySha256": "c" * 64, "runtimeProfile": "root-restart",
+ "unit": "rustfs-validation-heal-" + run_id + ".service", "mounts": [{}] * 4} for n in nodes}
+ for node in nodes:
+ emit("reserved", {"node": node, "reserved": True, "owner": owners[node]}); emit("inspector-upload", {})
+ started = {n: {"unit": owners[n]["unit"], "started": True} for n in nodes}
+ emit("started", started)
+ for label in ("deployed-topology", "topology-proof", "ACK-bound-complete-S3", "complete-ACK-manifest"):
+ emit(label, {})
+ for node in nodes: emit("task-exit-background", {})
+ emit("quiescent", {})
+ before = {n: {"quiescent": True, "pid": 0, "binarySha256": "c" * 64, "formats": [{}] * 4, "files": []} for n in nodes}
+ for i in range(4):
+ before["vm000"]["files"].append({"disk": 0,
+ "path": f"{bucket}/objects/{i:04d}.bin/00000000-0000-0000-0000-00000000000{i}/part.1",
+ "size": 17, "sha256": str(i) * 64})
+ emit("healthy-physical-baseline", before)
+ current = copy.deepcopy(before)
+ for i in range(4):
+ for n in range(16): emit("baseline-full-cohort-bitrot", {})
+ entry = current["vm000"]["files"].pop(0)
+ emit("missing-original-shard", {"schemaVersion": 1, "runId": run_id, "faultId": f"F07-shard-{i}",
+ "relativePath": entry["path"], "kind": "missing", "fault": None, "offset": None,
+ "before": {k: entry[k] for k in ("size", "sha256")}, "backup": f"F07-shard-{i}.original",
+ "backend": "logical-file-fault", "requiresQuiescentService": True})
+ emit("actual-missing-shard", current)
+ emit("started", started)
+ fixture = folder / "fixture"; fixture.mkdir()
+ for seq in range(1, 283):
+ operation = "create_bucket" if seq == 1 else "put_bucket_versioning" if seq == 2 else "put_object" if seq <= 242 else "delete_object"
+ params = {"Bucket": bucket}
+ if seq > 2: params["Key"] = "objects/0001.bin"
+ request = {"runId": run_id, "sequence": seq, "operation": operation, "params": params}
+ request_path = fixture / f"{seq:06d}-request.json"; request_path.write_text(json.dumps(request))
+ ack = {"runId": run_id, "sequence": seq, "requestFile": request_path.name,
+ "requestSha256": hashlib.sha256(request_path.read_bytes()).hexdigest(), "operationCheck": "PASS",
+ "observation": {"operation": operation, "params": params, "acknowledged": True,
+ "clientCallInvoked": True, "resultUnknown": False}}
+ (fixture / f"{seq:06d}-ack.json").write_text(json.dumps(ack))
+ return candidate, folder, before
+
+ def test_exact_boundary_and_rescue_retry_only_allow_original_parts(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory); candidate, folder, before = self.fixture(root)
+ plan = recovery.recovery_plan(root, 12345, candidate)[0]
+ self.assertEqual(len(plan["faults"]), 4)
+ self.assertEqual({f["node"] for f in plan["faults"]}, {"vm000"})
+ partial = copy.deepcopy(plan["failure"])
+ partial["vm000"]["files"] = before["vm000"]["files"][:2]
+ self.assertTrue(recovery.recovery_scene_matches(partial, plan))
+ partial["vm000"]["files"][0]["sha256"] = "f" * 64
+ self.assertFalse(recovery.recovery_scene_matches(partial, plan))
+
+ def test_boundary_mutations_fail_closed(self):
+ for mutation in ("candidate", "pending", "prior-cleanup", "task", "unknown-dir", "unknown-fixture", "unknown-ack",
+ "receipt", "other-content", "format", "missing-root", "later-branch", "extra-case", "wrong-harness", "unfinished-extra-branch"):
+ with self.subTest(mutation=mutation), tempfile.TemporaryDirectory() as directory:
+ root = Path(directory); candidate, folder, before = self.fixture(root)
+ def edit(path, function):
+ value = json.loads(path.read_text()); function(value); path.write_text(json.dumps(value))
+ if mutation == "candidate": candidate["package_sha256"] = "e" * 64
+ elif mutation == "pending": edit(root / "artifact/run" / interrupted.CASE / "cases" / (interrupted.CASE + ".json"), lambda x: x.update(cleanup_status="FAIL"))
+ elif mutation == "prior-cleanup": edit(next((folder.parents[1] / "H1/checkpoint-truncate").glob("*-cleanup.json")), lambda x: x.update(unitStopped=False))
+ elif mutation == "task": (folder / "00098-trigger.json").write_text("{}")
+ elif mutation == "unknown-dir": (folder / "admin").mkdir()
+ elif mutation == "unknown-fixture": (folder / "fixture/unknown.json").write_text("{}")
+ elif mutation == "unknown-ack": edit(folder / "fixture/000010-ack.json", lambda x: x["observation"].update(resultUnknown=True))
+ elif mutation == "receipt": edit(next(folder.glob("*-missing-original-shard.json")), lambda x: x.update(relativePath=".rustfs.sys/config/iam"))
+ elif mutation == "other-content": edit(folder / "00042-actual-missing-shard.json", lambda x: x["vm000"]["files"][0].update(size=19))
+ elif mutation == "format": edit(folder / "00042-actual-missing-shard.json", lambda x: x["vm001"].update(formats=[{"foreign": True}] * 4))
+ elif mutation == "missing-root": edit(folder / "00024-healthy-physical-baseline.json", lambda x: x.pop("vm003"))
+ elif mutation == "later-branch": (folder.parent / "journal-write-failure").mkdir()
+ elif mutation == "extra-case":
+ extra = root / "artifact/run/other/cases/other.json"; extra.parent.mkdir(parents=True); extra.write_text("{}")
+ elif mutation == "unfinished-extra-branch": (folder.parents[2] / "DX/H2/other").mkdir(parents=True)
+ elif mutation == "wrong-harness": edit(folder / "00001-binding.json", lambda x: x.update(harnessCommit="invalid"))
+ with self.assertRaises((ValueError, KeyError)):
+ recovery.recovery_plan(root, 12345, candidate)
+
+
+class RestoredArchiveTests(unittest.TestCase):
+ def test_original_scene_bound_before_first_rename_and_each_partial_move_is_retryable(self):
+ helper = scene_tests.RetainedSceneTests()
+ for lost_after in (1, 2, 3, 4, 5):
+ with self.subTest(lost_after=lost_after), tempfile.TemporaryDirectory() as directory:
+ state, roots, node, request = helper.scene(directory)
+ roots[0].mkdir(parents=True)
+ owner = request["expectedOwner"]; owner["runtimeProfile"] = "root-restart"
+ for folder in (*roots, state): (folder / "owner.json").write_text(json.dumps(owner))
+ snapshot = {"files": [{"path": "original", "sha256": "a" * 64}], "formats": [{}] * 4,
+ "pid": 0, "quiescent": True, "binarySha256": owner["binarySha256"]}
+ node["snapshot"] = unittest.mock.Mock(return_value=snapshot)
+ request["expectedOriginalSceneSha256"] = interrupted.digest({k: snapshot[k] for k in ("files", "formats")})
+ with helper.root_stats(), patch.object(retained, "rename_new", side_effect=helper.local_rename):
+ baseline = retained.run(request, node)["baseline"]
+ change = {**request, "action": "retained-scene-isolate", "expectedBaseline": baseline}
+ count = 0
+ def interrupted_move(source, target):
+ nonlocal count
+ helper.local_rename(source, target)
+ if target.is_dir():
+ count += 1
+ if count == lost_after: raise RuntimeError("lost ACK after rename")
+ with patch.object(retained, "rename_new", side_effect=interrupted_move), self.assertRaises(RuntimeError):
+ retained.run(change, node)
+ node["snapshot"].side_effect = AssertionError("must use durable intent after partial archive")
+ result = retained.run(change, node)
+ self.assertEqual(result["locations"], ["retained"] * 5)
+ self.assertEqual(result["baseline"], baseline)
+ self.assertFalse(result["canonicalPassClaimed"])
+ with self.assertRaisesRegex(ValueError, "durable isolation intent"):
+ retained.run({**request, "expectedOriginalSceneSha256": "b" * 64}, node)
+
+ def test_unrestored_or_missing_scene_cannot_be_archived(self):
+ helper = scene_tests.RetainedSceneTests()
+ for mutation in ("digest", "missing-root", "live-pid", "unknown-pid"):
+ with self.subTest(mutation=mutation), tempfile.TemporaryDirectory() as directory:
+ state, roots, node, request = helper.scene(directory)
+ if mutation != "missing-root": roots[0].mkdir(parents=True)
+ request["expectedOwner"]["runtimeProfile"] = "root-restart"
+ for folder in (*roots, state):
+ if folder.exists(): (folder / "owner.json").write_text(json.dumps(request["expectedOwner"]))
+ snapshot = {"files": [], "formats": [], "pid": 0, "quiescent": True,
+ "binarySha256": request["expectedOwner"]["binarySha256"]}
+ request["expectedOriginalSceneSha256"] = interrupted.digest({"files": [], "formats": []})
+ if mutation == "digest": request["expectedOriginalSceneSha256"] = "b" * 64
+ elif mutation == "live-pid": snapshot["pid"] = 7
+ elif mutation == "unknown-pid": snapshot["pid"] = False
+ node["snapshot"] = lambda request: snapshot
+ with helper.root_stats(), patch.object(retained, "rename_new") as move, self.assertRaises(ValueError):
+ retained.run(request, node)
+ move.assert_not_called()
+
+
+class InterruptedFleetTests(unittest.TestCase):
+ def test_all_stops_precede_restore_and_all_audits_precede_moves(self):
+ for variant in ("fresh", "retry-restored", "retry-partial-archive", "drift", "lost-root", "archive-drift"):
+ with self.subTest(variant=variant), tempfile.TemporaryDirectory() as directory:
+ root = Path(directory); candidate, folder, before = InterruptedPlanTests().fixture(root)
+ plan = recovery.recovery_plan(root, 12345, candidate)[0]
+ states = [{"node": n, "state": {"exists": True}, "roots": [{"exists": True}] * 4,
+ "retained": {"state": False, "roots": [False] * 4}} for n in plan["nodes"]]
+ current = copy.deepcopy(before if variant == "retry-restored" else plan["failure"])
+ if variant == "retry-partial-archive":
+ states[0]["retained"]["roots"][0] = True
+ elif variant == "drift": current["vm001"]["formats"] = [{"foreign": True}] * 4
+ elif variant == "lost-root": states[2]["roots"] = [{"exists": False}] * 4
+ events = []
+ def remote(node, action, **params):
+ events.append((node, action))
+ if action == "stop": return {"quiescent": True}
+ if action == "snapshot": return copy.deepcopy(current[node])
+ if action == "recover-fault":
+ fault = next(f for f in plan["faults"] if f["faultId"] == params["faultId"])
+ original = next(entry for entry in before[node]["files"] if entry["path"] == fault["receipt"]["relativePath"])
+ if original not in current[node]["files"]: current[node]["files"].append(copy.deepcopy(original))
+ current[node]["files"].sort(key=lambda row: (row["disk"], row["path"]))
+ return {"runId": plan["runId"], "faultId": params["faultId"], "physicalRepairPassAllowed": False}
+ if action == "retained-scene-audit" and variant == "archive-drift" and node == "vm003":
+ raise ValueError("archive drift")
+ baseline = {"originalSceneSha256": params["expectedOriginalSceneSha256"], "node": node}
+ return {"baseline": baseline, "inspectionOnly": action == "retained-scene-audit", "mutated": False,
+ "activeNamespaceIsolated": True, "canonicalPassClaimed": False}
+ fleet = types.SimpleNamespace(remote=remote, record=lambda label, value: value)
+ if variant in {"drift", "lost-root", "archive-drift"}:
+ with self.assertRaises(ValueError):
+ interrupted.restore_and_isolate(fleet, plan, states, recovery.recovery_scene_matches)
+ self.assertFalse(any(action == "retained-scene-isolate" for _, action in events))
+ if variant != "archive-drift": self.assertFalse(any(action == "recover-fault" for _, action in events))
+ continue
+ result = interrupted.restore_and_isolate(fleet, plan, states, recovery.recovery_scene_matches)
+ self.assertEqual(result["cleanup"], "ISOLATED")
+ self.assertTrue(result["originalBusinessFixtureRestored"])
+ self.assertFalse(result["originalSceneRestored"])
+ first_move = next(i for i, (_, action) in enumerate(events) if action == "retained-scene-isolate")
+ self.assertEqual(sum(action == "retained-scene-audit" for _, action in events[:first_move]), 4)
+ if variant == "retry-partial-archive":
+ self.assertFalse(any(n == "vm000" and a in {"stop", "snapshot", "recover-fault"} for n, a in events))
+ else:
+ first_rescue = next(i for i, (_, action) in enumerate(events) if action == "recover-fault")
+ self.assertEqual(sum(action == "stop" for _, action in events[:first_rescue]), 4)
+ self.assertEqual(physical_content(current), physical_content(before))
+ self.assertFalse(any(a in {"start", "cleanup", "journal-recover"} for _, a in events))
+
+
+class InterruptedAdapterTests(unittest.TestCase):
+ def test_retained_remote_has_the_real_snapshot_encryption_dependency(self):
+ from scripts.validation_heal.healthy import Fleet
+ def execute(*args, **kwargs):
+ source = kwargs["input"].rsplit("\nmain(", 1)[0]
+ scope = {"__name__": "__node_test__"}
+ exec(compile(source, "restored-archive-node", "exec"), scope)
+ module = scope["encryption_module"]()
+ with tempfile.TemporaryDirectory() as state:
+ self.assertIsNone(module.attest(Path(state).resolve(), {"runtimeProfile": "root-restart"}, scope["checked"], None))
+ self.assertTrue(callable(scope["snapshot"]))
+ return types.SimpleNamespace(returncode=0, stdout="{}", stderr="")
+ with tempfile.TemporaryDirectory() as directory, patch("scripts.validation_heal.healthy.subprocess.run", side_effect=execute):
+ Fleet(["vm000"], Path(directory), "gh-12345-1-h2").remote("vm000", "retained-scene-audit", expectedOwner={})
+
+
+class ArchivedFaultBindingTests(unittest.TestCase):
+ def test_mismatched_intent_cannot_write_backup_and_exact_intent_is_retryable(self):
+ from scripts.validation_heal import heal_fault
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory).resolve(); (root / "quarantine").mkdir()
+ bucket = "rv-heal-0123456789abcdef"
+ relative = bucket + "/objects/0001.bin/version/part.1"
+ target = root / "data" / relative; target.parent.mkdir(parents=True)
+ original = b"original-acknowledged-shard"
+ receipt = {"runId": "gh-12345-1-h2", "faultId": "F07-shard-0", "backup": "F07-shard-0.original",
+ "relativePath": relative, "kind": "missing", "fault": None,
+ "before": {"size": len(original), "sha256": hashlib.sha256(original).hexdigest()}}
+ (root / "quarantine/F07-shard-0.original").write_bytes(original)
+ (root / "quarantine/F07-shard-0.intent.json").write_bytes(heal_fault.encode(receipt))
+ owner = {"runId": receipt["runId"], "bucketPrefix": bucket}
+ with patch.object(heal_fault, "mount_id", return_value=1), heal_fault.directory(root) as fd:
+ with self.assertRaisesRegex(ValueError, "archived fault receipt"):
+ heal_fault.recover_fault(fd, owner, receipt["faultId"], expected_receipt_sha256="0" * 64)
+ self.assertFalse(target.exists())
+ proof = heal_fault.recover_fault(fd, owner, receipt["faultId"], expected_receipt_sha256=interrupted.digest(receipt))
+ self.assertTrue(proof["restoredFromBackup"])
+ self.assertEqual(target.read_bytes(), original)
+ proof = heal_fault.recover_fault(fd, owner, receipt["faultId"], expected_receipt_sha256=interrupted.digest(receipt))
+ self.assertFalse(proof["restoredFromBackup"])
+ self.assertFalse(proof["physicalRepairPassAllowed"])
diff --git a/test_validation_heal_journal_fault.py b/test_validation_heal_journal_fault.py
new file mode 100644
index 00000000..e8a4a5c9
--- /dev/null
+++ b/test_validation_heal_journal_fault.py
@@ -0,0 +1,976 @@
+"""Exact durable-record corruption preserves ownership and refuses stale rescue."""
+
+import base64
+import copy
+import hashlib
+import contextlib
+import io
+import json
+import os
+from pathlib import Path
+import struct
+import subprocess
+import sys
+import time
+import tempfile
+import types
+import unittest
+import uuid
+from unittest.mock import patch
+import zlib
+
+from scripts.validation_heal import journal_fault as journal, heal_fault as fault, mrf_records as mrf
+from scripts.validation_heal.healthy import Fleet
+from scripts.validation_heal import fleet_node
+from scripts.validation_heal import healthy
+from scripts.validation_heal.checkpoint import OPTIONS
+
+
+class DurableRecordFaultTests(unittest.TestCase):
+ token = "12345678-1234-4234-8234-123456789abc"
+ incarnation = "22345678-1234-4234-8234-123456789abc"
+ bucket = "rv-heal-0123456789abcdef"
+
+ def test_atomic_replace_after_open_rejects_detached_bytes_as_read_churn(self):
+ with tempfile.TemporaryDirectory() as temporary:
+ root = Path(temporary)
+ current, successor = root / 'checkpoint', root / 'successor'
+ current.write_bytes(b'earlier')
+ successor.write_bytes(b'later')
+ original_open = os.open
+ observed = []
+ def replace_after_open(name, flags, **kwargs):
+ descriptor = original_open(name, flags, **kwargs)
+ if name == current.name:
+ os.replace(successor, current)
+ observed.append(os.fstat(descriptor).st_nlink)
+ return descriptor
+ descriptor = original_open(root, os.O_RDONLY | os.O_DIRECTORY)
+ try:
+ with patch.object(fault.os, 'open', side_effect=replace_after_open), \
+ patch.object(fault, 'mount_id', return_value=1):
+ with self.assertRaisesRegex(ValueError, '^file changed while reading$'):
+ fault.read_file(descriptor, current.name)
+ self.assertEqual(observed, [0])
+ with patch.object(fault, 'mount_id', return_value=1):
+ self.assertEqual(fault.read_file(descriptor, current.name), b'later')
+ self.assertEqual(current.stat().st_nlink, 1)
+ finally:
+ os.close(descriptor)
+
+ def test_detached_inode_does_not_hide_type_size_or_mount_refusals(self):
+ for poison in ('oversized', 'fifo', 'mount'):
+ with self.subTest(poison=poison), tempfile.TemporaryDirectory() as temporary:
+ root = Path(temporary)
+ current, successor = root / 'checkpoint', root / 'successor'
+ if poison == 'fifo': os.mkfifo(current)
+ else: current.write_bytes(b'12345' if poison == 'oversized' else b'old')
+ successor.write_bytes(b'new')
+ original_open = os.open
+ def replace_after_open(name, flags, **kwargs):
+ descriptor = original_open(name, flags, **kwargs)
+ if name == current.name:
+ os.replace(successor, current)
+ self.assertEqual(os.fstat(descriptor).st_nlink, 0)
+ return descriptor
+ descriptor = original_open(root, os.O_RDONLY | os.O_DIRECTORY)
+ try:
+ with patch.object(fault.os, 'open', side_effect=replace_after_open), \
+ patch.object(fault, 'mount_id', side_effect=[2, 1] if poison == 'mount' else None, return_value=1):
+ message = 'file bind-mount escape' if poison == 'mount' else 'expected bounded regular non-hardlinked file'
+ with self.assertRaisesRegex(ValueError, '^' + message + '$'):
+ fault.read_file(descriptor, current.name, limit=4)
+ finally:
+ os.close(descriptor)
+
+ def test_live_read_race_is_retryable_but_stopped_read_and_path_failures_are_not(self):
+ for message in ('file changed while reading', 'file bind-mount escape', 'expected bounded regular non-hardlinked file'):
+ with self.subTest(message=message), patch.object(journal, 'census', side_effect=ValueError(message)):
+ expected = 'NOT_OBSERVED:' if message == 'file changed while reading' else message
+ with self.assertRaisesRegex(ValueError, expected): journal.stable_census([], {}, fault)
+ with self.assertRaisesRegex(ValueError, message): journal.census([], {}, fault)
+
+ def test_only_live_observation_refusals_survive_the_remote_boundary_as_retryable(self):
+ refusal = 'NOT_OBSERVED: exact durable records changed between the two bounded reads'
+ for action in ('journal-observe', 'journal-capture', 'journal-capture-advance', 'journal-apply', 'journal-census'):
+ output = io.StringIO()
+ with self.subTest(action=action), patch.object(fleet_node.os, 'geteuid', return_value=0), \
+ patch.object(fleet_node, 'journal_operation', side_effect=ValueError(refusal)), contextlib.redirect_stdout(output):
+ if action in ('journal-observe', 'journal-capture'):
+ fleet_node.main({'action': action})
+ self.assertEqual(json.loads(output.getvalue()), {'observationUnavailable': refusal})
+ else:
+ with self.assertRaisesRegex(ValueError, 'NOT_OBSERVED:'): fleet_node.main({'action': action})
+ with patch.object(fleet_node.os, 'geteuid', return_value=0), \
+ patch.object(fleet_node, 'journal_operation', side_effect=ValueError('service restarted while observing exact durable records')):
+ with self.assertRaisesRegex(ValueError, 'service restarted'): fleet_node.main({'action': 'journal-observe'})
+ with tempfile.TemporaryDirectory() as temp:
+ fleet = Fleet(['node'], Path(temp), 'gh-12345-1-h1')
+ for action in ('journal-observe', 'journal-capture'):
+ response = types.SimpleNamespace(returncode=0, stdout=json.dumps({'observationUnavailable': refusal}), stderr='')
+ with self.subTest(action=action), patch.object(healthy.subprocess, 'run', return_value=response):
+ with self.assertRaisesRegex(ValueError, '^NOT_OBSERVED:'): fleet.remote('node', action)
+ for body in ({'observationUnavailable': 'other'}, {'observationUnavailable': refusal, 'records': []}, {'observationUnavailable': None}):
+ response = types.SimpleNamespace(returncode=0, stdout=json.dumps(body), stderr='')
+ with self.subTest(body=body), patch.object(healthy.subprocess, 'run', return_value=response):
+ with self.assertRaisesRegex(ValueError, 'invalid durable-record'): fleet.remote('node', 'journal-observe')
+ response = types.SimpleNamespace(returncode=1, stdout=json.dumps({'observationUnavailable': refusal}), stderr='failed')
+ with patch.object(healthy.subprocess, 'run', return_value=response):
+ with self.assertRaisesRegex(ValueError, '^Heal node'): fleet.remote('node', 'journal-observe')
+
+ def test_atomic_advance_uses_one_held_pidfd_and_refreezes_on_observation_failure(self):
+ for change in (None, 'timeout', 'birth', 'corrupt', 'incarnation', 'options', 'counter', 'read-error', 'unstable', 'unlinked-open', 'unlinked-open-timeout', 'post-birth', 'late-progress', 'continue-interrupted', 'stop-pending', 'stop-timeout', 'stop-birth'):
+ with self.subTest(change=change), tempfile.TemporaryDirectory() as temp, contextlib.ExitStack() as stack:
+ base = Path(temp).resolve(); roots = tuple(base / str(i) for i in range(4))
+ for root in roots: root.mkdir()
+ owner, request, path, raw = self.fixture(roots[0]); request.pop('kind')
+ for root in roots[1:]:
+ (root / 'data/.rustfs.sys/buckets').mkdir(parents=True); (root / 'quarantine').mkdir()
+ request['action'] = 'journal-capture-advance'
+ identity = {name: name for name in fleet_node.PROCESS_FIELDS}
+ identity.update(pid=50, startTicks=20, binarySha256=owner['binarySha256'])
+ request['expected'] = identity
+ next_raw = self.checkpoint(processed=3, time=101)
+ value = json.loads(next_raw)
+ if change == 'incarnation': value['admin']['buckets'][self.bucket] = str(uuid.uuid4())
+ elif change == 'options': value['admin']['options']['nolock'] = True
+ elif change == 'counter': value['admin']['outcome']['counters']['processed'] = True
+ if change in ('incarnation', 'options', 'counter'): next_raw = self.signed(value)
+ if change == 'corrupt': next_raw = b'invalid checkpoint'
+ events = []; running = [False]; raced = [False]
+ def signal_pidfd(fd, signum, *args):
+ self.assertEqual(fd, 55); events.append(signum)
+ running[0] = signum == fleet_node.signal.SIGCONT
+ if running[0] and change != 'timeout': path.write_bytes(next_raw)
+ if running[0] and change == 'continue-interrupted': raise KeyboardInterrupt('lost CONT receipt')
+ def process(request):
+ if len(process_calls) >= 2: self.assertEqual(events[-1], fleet_node.signal.SIGSTOP)
+ process_calls.append(True)
+ return {**identity, 'state': 'S' if change == 'stop-pending' and len(process_calls) == 3
+ and not stop_ready[0] else 'T',
+ **({'startTicks': 21} if change == 'post-birth' and len(process_calls) == 3 else {})}
+ process_calls = []; stop_ready = [False]; stopped_reads = [0]
+ old_read_text = Path.read_text
+ def read_text(p, *a, **kw):
+ if str(p) == '/proc/50/stat':
+ state = 'S' if running[0] else 'T'
+ if not running[0] and events:
+ stopped_reads[0] += 1
+ if change == 'stop-timeout' or change == 'stop-pending' and stopped_reads[0] == 1: state = 'S'
+ stop_ready[0] = state == 'T'
+ changed_birth = change == 'birth' or change == 'stop-birth' and not running[0] and events
+ return '50 (owned candidate) ' + state + ' ' + ' '.join(['0'] * 18 + ['21' if changed_birth else '20'])
+ return old_read_text(p, *a, **kw)
+ real_read = journal.read
+ def read(*args, **kwargs):
+ if running[0] and not raced[0] and change in ('read-error', 'unstable'):
+ raced[0] = True
+ if change == 'read-error': raise PermissionError('unexpected access denial')
+ raise ValueError('file changed while reading')
+ if running[0] and not raced[0] and change in ('unlinked-open', 'unlinked-open-timeout'):
+ raced[0] = True
+ original_open = os.open
+ def replace_after_open(name, flags, **params):
+ descriptor = original_open(name, flags, **params)
+ if name == path.name:
+ successor = path.with_name('natural-checkpoint-successor')
+ successor.write_bytes(next_raw)
+ os.replace(successor, path)
+ self.assertEqual(os.fstat(descriptor).st_nlink, 0)
+ return descriptor
+ with patch.object(fault.os, 'open', side_effect=replace_after_open):
+ return real_read(*args, **kwargs)
+ return real_read(*args, **kwargs)
+ for context in (patch.object(fleet_node, 'owned', return_value=(base, roots, owner)),
+ patch.object(fleet_node, 'digest', return_value=owner['binarySha256']),
+ patch.object(fleet_node, 'fault_module', return_value=fault),
+ patch.dict(fleet_node.__dict__, journal_fault_backend=journal),
+ patch.object(fleet_node, 'process', side_effect=process)):
+ stack.enter_context(context)
+ opened = stack.enter_context(patch.object(fleet_node.os, 'pidfd_open', create=True, return_value=55))
+ sent = stack.enter_context(patch.object(fleet_node.signal, 'pidfd_send_signal', create=True, side_effect=signal_pidfd))
+ real_close = os.close
+ closed = []
+ def close(fd):
+ if fd == 55: closed.append(fd)
+ else: real_close(fd)
+ stack.enter_context(patch.object(fleet_node.os, 'close', side_effect=close))
+ stack.enter_context(patch.object(fault, 'mount_id', return_value=1))
+ stack.enter_context(patch.object(Path, 'read_text', read_text))
+ stack.enter_context(patch.object(journal, 'read', side_effect=read))
+ if change == 'timeout': stack.enter_context(patch.object(fleet_node.time, 'monotonic', side_effect=[0, 61]))
+ elif change == 'unlinked-open-timeout': stack.enter_context(patch.object(fleet_node.time, 'monotonic', side_effect=[0, .01, 61]))
+ elif change == 'late-progress': stack.enter_context(patch.object(fleet_node.time, 'monotonic', side_effect=[0, .01, 61]))
+ elif change == 'stop-timeout': stack.enter_context(patch.object(fleet_node.time, 'monotonic', side_effect=[0, .01, .02, 61]))
+ if change in (None, 'unstable', 'unlinked-open', 'stop-pending'):
+ result = fleet_node.journal_operation(request)
+ self.assertTrue(result['checkpointAdvancedAndFrozen'])
+ self.assertTrue(result['pidfdBound'])
+ self.assertEqual(result['observedProcessed'], 3)
+ self.assertEqual(path.read_bytes(), next_raw)
+ self.assertEqual(len(process_calls), 3)
+ elif change == 'unlinked-open-timeout':
+ with self.assertRaisesRegex(ValueError, 'actual checkpoint advance exceeded its frozen 60-second budget'):
+ fleet_node.journal_operation(request)
+ else:
+ with self.assertRaises((ValueError, OSError, KeyboardInterrupt)): fleet_node.journal_operation(request)
+ self.assertEqual(events, [fleet_node.signal.SIGCONT, fleet_node.signal.SIGSTOP])
+ self.assertEqual(closed, [55])
+ opened.assert_called_once_with(50, 0)
+ self.assertEqual((roots[0] / ('quarantine/' + request['faultId'] + '.early.checkpoint')).read_bytes(), raw)
+
+ def test_atomic_advance_rejects_unpaused_foreign_or_unbounded_requests_before_capture_or_signal(self):
+ for change in ('runtime', 'live', 'birth', 'input', 'budget', 'path', 'disk', 'family'):
+ with self.subTest(change=change), tempfile.TemporaryDirectory() as temp, contextlib.ExitStack() as stack:
+ root = Path(temp).resolve(); owner, request, path, raw = self.fixture(root); request.pop('kind')
+ request['action'] = 'journal-capture-advance'
+ identity = {name: name for name in fleet_node.PROCESS_FIELDS}
+ identity.update(pid=50, startTicks=20, binarySha256=owner['binarySha256']); request['expected'] = identity.copy()
+ if change == 'runtime': owner['runtimeProfile'] = 'mrf-crash'
+ elif change == 'birth': request['expected']['startTicks'] = 21
+ elif change == 'input': request['command'] = 'arbitrary'
+ elif change == 'budget': request['timeout'] = 600
+ elif change == 'path': request['relativePath'] = '.rustfs.sys/foreign.json'
+ elif change == 'disk': request['disk'] = True
+ elif change == 'family': request['family'] = 'mrf'
+ for context in (patch.object(fleet_node, 'owned', return_value=(root, [root] * 4, owner)),
+ patch.object(fleet_node, 'digest', return_value=owner['binarySha256']),
+ patch.object(fleet_node, 'fault_module', return_value=fault),
+ patch.dict(fleet_node.__dict__, journal_fault_backend=journal),
+ patch.object(fleet_node, 'process', return_value={**identity, 'state': 'S' if change == 'live' else 'T'}),
+ patch.object(fleet_node.os, 'pidfd_open', create=True, return_value=55),
+ patch.object(fleet_node.signal, 'pidfd_send_signal', create=True)):
+ stack.enter_context(context)
+ captured = stack.enter_context(patch.object(journal, 'capture'))
+ with self.assertRaises(ValueError): fleet_node.journal_operation(request)
+ captured.assert_not_called()
+ self.assertEqual(path.read_bytes(), raw)
+ self.assertEqual(list((root / 'quarantine').iterdir()), [])
+
+ def test_advance_rejects_reopened_capture_device_or_mount_before_resuming(self):
+ for key in ('device', 'mountId'):
+ with self.subTest(key=key), tempfile.TemporaryDirectory() as temp, contextlib.ExitStack() as stack:
+ root = Path(temp).resolve(); owner, request, path, raw = self.fixture(root); request.pop('kind')
+ request['action'] = 'journal-capture-advance'
+ identity = {name: name for name in fleet_node.PROCESS_FIELDS}
+ identity.update(pid=50, startTicks=20, binarySha256=owner['binarySha256']); request['expected'] = identity
+ real_capture = journal.capture
+ def capture(*args): return {**real_capture(*args), key: -1}
+ for context in (patch.object(fleet_node, 'owned', return_value=(root, [root] * 4, owner)),
+ patch.object(fleet_node, 'digest', return_value=owner['binarySha256']),
+ patch.object(fleet_node, 'fault_module', return_value=fault),
+ patch.dict(fleet_node.__dict__, journal_fault_backend=journal),
+ patch.object(fleet_node, 'process', return_value={**identity, 'state': 'T'}),
+ patch.object(fleet_node.os, 'pidfd_open', create=True, return_value=55),
+ patch.object(fault, 'mount_id', return_value=1), patch.object(journal, 'capture', side_effect=capture)):
+ stack.enter_context(context)
+ signals = stack.enter_context(patch.object(fleet_node.signal, 'pidfd_send_signal', create=True))
+ real_close = os.close; closed = []
+ def close(fd):
+ if fd == 55: closed.append(fd)
+ else: real_close(fd)
+ stack.enter_context(patch.object(fleet_node.os, 'close', side_effect=close))
+ with self.assertRaisesRegex(ValueError, 'root device or mount changed'): fleet_node.journal_operation(request)
+ signals.assert_not_called(); self.assertEqual(closed, [55]); self.assertEqual(path.read_bytes(), raw)
+
+ @unittest.skipUnless(sys.platform == 'linux' and hasattr(os, 'pidfd_open')
+ and hasattr(fleet_node.signal, 'pidfd_send_signal'), 'actual Linux pidfd required')
+ def test_actual_linux_pidfd_stops_the_same_child_after_real_checkpoint_file_progress(self):
+ with tempfile.TemporaryDirectory() as temp:
+ base = Path(temp).resolve(); roots = tuple(base / str(i) for i in range(4))
+ for root in roots: root.mkdir()
+ owner, request, path, early = self.fixture(roots[0]); request.pop('kind')
+ request['action'] = 'journal-capture-advance'
+ for root in roots[1:]: (root / 'data/.rustfs.sys/buckets').mkdir(parents=True)
+ resume_path = roots[0] / 'data' / journal.names('checkpoint', self.token)[1]
+ resume = json.dumps({'schema_version': 7, 'task_id': self.token, 'task_type': 'admin_erasure_set',
+ 'set_disk_id': 'pool_0_set_0', 'completed': False, 'replacement_targets': [], 'replacement_generation': None}).encode()
+ resume_path.write_bytes(resume)
+ later = self.checkpoint(processed=3, time=101); next_bytes = base / 'next'; next_bytes.write_bytes(later)
+ child_source = ("import os,signal,sys,time; os.kill(os.getpid(),signal.SIGSTOP); "
+ "raw=open(sys.argv[2],'rb').read(); f=open(sys.argv[1]+'.next','xb'); "
+ "f.write(raw); f.flush(); os.fsync(f.fileno()); f.close(); os.replace(sys.argv[1]+'.next',sys.argv[1]); "
+ "time.sleep(120)")
+ child = subprocess.Popen([sys.executable, '-c', child_source, str(path), str(next_bytes)])
+ cleanup_fd = None
+ real_send = fleet_node.signal.pidfd_send_signal
+ def stat_fields(): return Path(f'/proc/{child.pid}/stat').read_text().rpartition(') ')[2].split()
+ try:
+ cleanup_fd = os.pidfd_open(child.pid, 0)
+ deadline = time.monotonic() + 5
+ while stat_fields()[0] != 'T':
+ self.assertIsNone(child.poll()); self.assertLess(time.monotonic(), deadline); time.sleep(.01)
+ birth = int(stat_fields()[19])
+ identity = {name: name for name in fleet_node.PROCESS_FIELDS}
+ identity.update(pid=child.pid, startTicks=birth, binarySha256=owner['binarySha256'])
+ request['expected'] = identity
+ snapshots, signals = [], []
+ def process(request):
+ fields = stat_fields(); self.assertEqual(int(fields[19]), birth)
+ if len(snapshots) >= 2: self.assertEqual(signals[-1], fleet_node.signal.SIGSTOP)
+ snapshots.append(fields[0]); return {**identity, 'state': fields[0]}
+ def send(fd, signum, *args):
+ signals.append(signum); return real_send(fd, signum, *args)
+ with patch.object(fleet_node, 'owned', return_value=(base, roots, owner)), \
+ patch.object(fleet_node, 'digest', return_value=owner['binarySha256']), \
+ patch.object(fleet_node, 'fault_module', return_value=fault), \
+ patch.dict(fleet_node.__dict__, journal_fault_backend=journal), \
+ patch.object(fleet_node, 'process', side_effect=process), \
+ patch.object(fleet_node.signal, 'pidfd_send_signal', side_effect=send):
+ result = fleet_node.journal_operation(request)
+ self.assertEqual(signals, [fleet_node.signal.SIGCONT, fleet_node.signal.SIGSTOP])
+ self.assertEqual(snapshots, ['T', 'T', 'T'])
+ self.assertEqual(result['observedProcessed'], 3)
+ self.assertEqual(stat_fields()[0], 'T')
+ self.assertEqual(path.read_bytes(), later)
+ self.assertEqual(resume_path.read_bytes(), resume)
+ self.assertEqual((roots[0] / ('quarantine/' + request['faultId'] + '.early.checkpoint')).read_bytes(), early)
+ print('ATOMIC_PIDFD_COMPONENT ' + json.dumps({'simulatedCheckpoint': True, 'actualLinuxPidfd': True,
+ 'sameProcessBirth': True, 'checkpointCounts': [2, 3], 'earlierBackupByteIdentical': True,
+ 'laterFileByteIdentical': True, 'companionUnchanged': True, 'childStoppedBeforeReturn': True,
+ 'rustfsCandidateExecuted': False, 'canonicalPassClaimed': False}))
+ finally:
+ try:
+ if cleanup_fd is not None: real_send(cleanup_fd, fleet_node.signal.SIGKILL, None, 0)
+ elif child.poll() is None: child.kill()
+ except ProcessLookupError: pass
+ finally:
+ if cleanup_fd is not None: os.close(cleanup_fd)
+ child.wait(timeout=5)
+
+ def test_full_server_checkpoint_fixture_preserves_feature_unified_json_order(self):
+ # Original bytes from candidate 1aa9be87, diagnostic 38261190390/1.
+ raw = (Path(__file__).parent / "testdata/validation/checkpoint-preserve-order.json").read_bytes()
+ self.assertEqual(hashlib.sha256(raw).hexdigest(), "af9f1217284fdf9516d111ae8f8cc28b264bec200ba640c5f06c696b2303982b")
+ value = json.loads(raw)
+ bucket = next(iter(value["admin"]["buckets"]))
+ observed = journal.checkpoint_value(raw, value["task_id"], bucket)
+ self.assertEqual(observed["admin"]["outcome"]["counters"]["processed"], 8)
+ for mutate in ("counter", "cursor", "incarnation", "digest"):
+ changed = copy.deepcopy(value)
+ if mutate == "counter": changed["admin"]["outcome"]["counters"]["processed"] = 9
+ elif mutate == "cursor": changed["current_object_index"] += 1
+ elif mutate == "incarnation": changed["admin"]["buckets"][bucket] = self.incarnation
+ else: changed["integrity_digest"] = "A" * 44
+ with self.subTest(mutate=mutate), self.assertRaisesRegex(ValueError, "integrity digest"):
+ journal.checkpoint_value(json.dumps(changed,separators=(",", ":")).encode(), value["task_id"], bucket)
+
+ def test_original_mixed_mrf_payload_retains_only_registered_internal_companions(self):
+ raw = (Path(__file__).parent / "testdata/validation/mrf-mixed-payload.bin").read_bytes()
+ self.assertEqual(hashlib.sha256(raw).hexdigest(), "3a6db05b46bb7e3a66a971bb4cc4759b3cd4700d2f31d55f0a19fd023635eb80")
+ intents = mrf.decode_journal(raw)
+ bucket = "rv-heal-084db3ab418d55ba"
+ owned = journal.owned_mrf_intents(intents, bucket)
+ self.assertEqual(len(owned), 3)
+ self.assertEqual(len(intents), 6)
+ request = {"family": "mrf", "kind": "checksum", "relativePath": ".rustfs.sys/.heal-mrf-snapshot.0.bin"}
+ self.assertEqual(journal.transform(raw, request, {"bucket": bucket}, mrf), raw[:-1] + bytes([raw[-1] ^ 1]))
+ system_bucket, system_key = b".rustfs.sys", b"buckets/.rustfs.sys/.metadata.bin"
+ body = struct.pack("when retrnsmt uid timeout inode'
+
+
+def event(state, sent=SENT):
+ return {'target': observer.TARGET, 'level': 'TRACE', 'event': 'object_namespace_lock',
+ 'component': 'ecstore', 'subsystem': 'object_lock', 'state': state, 'op': 'get_object',
+ 'bucket': BUCKET, 'object': 'target.bin', 'namespace': 'namespace-one', 'owner': 'owner-one',
+ 'requested_version_id': VERSION,
+ 'lock_attempt_id': ATTEMPT, 'request_id': REQUEST, 'mode': 'read', 'timeout_ms': 1000,
+ 'acquire_ms': 1, 'hold_ms': 1, 'span': {'name': 'http-request', 'user_agent': sent['callerMarker'],
+ 'request_id': REQUEST, 'method': 'GET', 'uri': sent['requestTarget']}}
+
+
+def identity():
+ return {**dict.fromkeys(fleet_node.PROCESS_FIELDS, 'unchanged'), 'pid': 123, 'startTicks': 456,
+ 'bootId': 'original-boot', 'binarySha256': 'd' * 64,
+ 'settings': fleet_node.runtime_config('lock-timeout')}
+
+
+class LockEventTests(unittest.TestCase):
+ def test_incomplete_or_non_json_journal_page_cannot_complete_i05_observation(self):
+ from scripts.validation_heal.scanner_repair import JournalWindow
+ class Fleet:
+ nodes = ['vm000']
+ journal = 'truncated candidate JSON'
+ complete = True
+ def record(self, label, value): return value
+ def remote(self, node, action, **params):
+ return {'complete': self.complete, 'beforeCursor': params.get('afterCursor'),
+ 'afterCursor': 'cursor-next', 'journal': self.journal, 'hasMore': False}
+ for raw, complete in [('truncated candidate JSON', True), ('[]', True), ('{}', False)]:
+ with self.subTest(raw=raw):
+ fleet = Fleet()
+ window = JournalWindow(fleet, 'I05', event_targets={observer.TARGET}, strict_json=True)
+ fleet.journal, fleet.complete = raw, complete
+ with self.assertRaises(ValueError): window.capture()
+ def test_flattened_production_event_binds_server_id_and_exact_version_uri(self):
+ rows = [event('acquiring'), event('acquired')]
+ rows[0]['spans'] = [rows[0]['span']]
+ proof = observer.caller_attempt(rows, SENT, BUCKET, 'target.bin')
+ self.assertEqual(proof['request_id'], REQUEST)
+ self.assertTrue(proof['admitted'] and proof['observedAcquiredWithoutDrop'])
+ rows.append(event('guard_dropped'))
+ proof = observer.caller_attempt(rows, SENT, BUCKET, 'target.bin')
+ self.assertTrue(proof['diagnosticGuardDropped'])
+ self.assertFalse(proof['distributedReleaseProven'] or proof['heldAtDisconnectProven'])
+
+ def test_optional_diagnostic_version_must_agree_with_exact_http_selector(self):
+ rows = [event('acquiring'), event('acquired')]
+ for row in rows: row.pop('requested_version_id')
+ self.assertTrue(observer.caller_attempt(rows, SENT, BUCKET, 'target.bin')['observedAcquiredWithoutDrop'])
+ for version in ('', ATTEMPT):
+ with self.subTest(version=version):
+ rows = [event('acquiring'), event('acquired')]
+ rows[0]['requested_version_id'] = version
+ with self.assertRaisesRegex(ValueError, 'exact GET identity'):
+ observer.caller_attempt(rows, SENT, BUCKET, 'target.bin')
+
+ def test_repeated_or_missing_http_version_selector_refuses_before_events(self):
+ for target in ('/bucket/key', SENT['requestTarget'] + '&versionId=' + VERSION):
+ with self.subTest(target=target), self.assertRaisesRegex(ValueError, 'one exact historical'):
+ observer.caller_attempt([], {**SENT, 'requestTarget': target}, BUCKET, 'target.bin')
+
+ def test_original_live_historical_get_is_correlated_but_ended_window_stays_closed(self):
+ path = Path(__file__).with_name('testdata') / 'i05-historical-get-39711742431.ndjson'
+ rows = [json.loads(line) for line in path.read_text().splitlines()]
+ span = rows[0]['span']
+ sent = {'callerMarker': span['user_agent'], 'requestTarget': span['uri']}
+ proof = observer.caller_attempt(rows, sent, rows[0]['bucket'], rows[0]['object'])
+ self.assertTrue(proof['diagnosticGuardDropped'])
+ self.assertFalse(proof['observedAcquiredWithoutDrop'] or proof['heldAtDisconnectProven'])
+ self.assertEqual(proof['request_id'], span['request_id'])
+ # The complete earlier lifecycle cannot be trimmed to claim a live holder.
+ rows[1]['requested_version_id'] = ATTEMPT
+ with self.assertRaisesRegex(ValueError, 'exact GET identity'):
+ observer.caller_attempt(rows, sent, rows[0]['bucket'], rows[0]['object'])
+
+ def test_rejects_forged_request_id_foreign_key_version_owner_or_incomplete_events(self):
+ mutations = [lambda e: e.update(request_id=ATTEMPT), lambda e: e.update(object='foreign.bin'),
+ lambda e: e.update(owner='another-owner'), lambda e: e.update(timeout_ms=5000),
+ lambda e: e.update(acquire_ms=True), lambda e: e.update(hold_ms=-1),
+ lambda e: e.update(requested_version_id=ATTEMPT),
+ lambda e: e['span'].update(uri='/other-version'), lambda e: e['span'].update(method='PUT'),
+ lambda e: e.update(lock_attempt_id=REQUEST)]
+ for mutate in mutations:
+ with self.subTest(mutate=mutate):
+ rows = [event('acquiring'), event('acquired')]
+ mutate(rows[1])
+ with self.assertRaises(ValueError):
+ observer.caller_attempt(rows, SENT, BUCKET, 'target.bin')
+ for states in (['acquired'], ['acquiring', 'acquiring'], ['acquiring', 'disabled'],
+ ['acquiring', 'guard_dropped'], ['acquiring', 'acquired', 'acquired']):
+ with self.subTest(states=states), self.assertRaises(ValueError):
+ observer.caller_attempt([event(s) for s in states], SENT, BUCKET, 'target.bin')
+
+ def test_free_text_client_markers_and_native_nested_fields_do_not_prove_admission(self):
+ for row in ({'message': str(SENT)}, {'target': 'foreign', **{'fields': event('acquiring')}},
+ {'target': observer.TARGET, 'fields': event('acquiring')},
+ {**event('acquiring'), 'span': {'name': 'http-request', 'user_agent': 'different'}}):
+ self.assertIsNone(observer.caller_attempt([row], SENT, BUCKET, 'target.bin'))
+
+ def test_cancelled_and_failed_attempts_never_count_as_acquired_or_dropped(self):
+ for state in ('cancelled', 'failed'):
+ end = event(state)
+ if state == 'failed': end.update(level='ERROR', failure='timeout', acquire_ms=1001)
+ proof = observer.caller_attempt([event('acquiring'), end], SENT, BUCKET, 'target.bin')
+ self.assertFalse(proof['diagnosticGuardDropped'] or proof['observedAcquiredWithoutDrop'])
+
+ def test_timeout_proof_requires_server_request_id_and_exact_write_lock_timeout(self):
+ holder = {'request_id': REQUEST, 'lock_attempt_id': ATTEMPT, 'owner': 'owner-one', 'namespace': 'namespace-one'}
+ contender = '30000000-0000-4000-8000-000000000001'
+ response = {'ResponseMetadata': {'HTTPStatusCode': 503, 'RetryAttempts': 0, 'RequestId': contender,
+ 'HTTPHeaders': {'retry-after': '1'}},
+ 'Error': {'Code': 'ServiceUnavailable'}}
+ rows = [
+ {'target': observer.TARGET, 'request_id': contender, 'event': 'object_namespace_lock',
+ 'component': 'ecstore', 'subsystem': 'object_lock', 'state': 'acquiring', 'level': 'TRACE',
+ 'op': 'put_object_commit', 'mode': 'write', 'bucket': BUCKET, 'object': 'target.bin',
+ 'namespace': 'namespace-one', 'owner': 'owner-one', 'lock_attempt_id': '40000000-0000-4000-8000-000000000001',
+ 'requested_version_id': None, 'timeout_ms': 1000, 'acquire_ms': 0},
+ {'target': observer.TARGET, 'request_id': contender, 'event': 'object_namespace_lock',
+ 'component': 'ecstore', 'subsystem': 'object_lock', 'state': 'failed', 'level': 'ERROR',
+ 'op': 'put_object_commit', 'mode': 'write', 'bucket': BUCKET, 'object': 'target.bin',
+ 'namespace': 'namespace-one', 'owner': 'owner-one', 'lock_attempt_id': '40000000-0000-4000-8000-000000000001',
+ 'requested_version_id': None, 'timeout_ms': 1000, 'acquire_ms': 1001, 'failure': 'timeout'},
+ ]
+ proof = lock_timeout.timeout_proof(rows, response, holder, BUCKET, 'target.bin')
+ self.assertTrue(proof['typedTimeoutBeyondBudgetObserved'] and not proof['writeLockAcquired'])
+ for mutation in (
+ (lambda r, q: r[1].update(owner='foreign-owner')),
+ (lambda r, q: r[1].update(lock_attempt_id=ATTEMPT)),
+ (lambda r, q: q['ResponseMetadata'].update(RequestId=REQUEST)),
+ (lambda r, q: r[1].update(acquire_ms=999)),
+ (lambda r, q: r[1].update(namespace='other')),
+ (lambda r, q: q['ResponseMetadata']['HTTPHeaders'].clear()),
+ (lambda r, q: q['ResponseMetadata'].update(RetryAttempts=1)),
+ (lambda r, q: r[1].update(state='acquired')),
+ ):
+ copy_rows = json.loads(json.dumps(rows)); copy_response = json.loads(json.dumps(response))
+ mutation(copy_rows, copy_response)
+ with self.assertRaises(ValueError):
+ lock_timeout.timeout_proof(copy_rows, copy_response, holder, BUCKET, 'target.bin')
+
+ def test_timeout_inventory_requires_complete_cross_checked_exact_target(self):
+ entry = {'key': 'target.bin', 'versionId': VERSION, 'deleteMarker': False, 'isLatest': True,
+ 'payload': {'size': 5, 'sha256': 'a' * 64}}
+ inventory = {'bucket': BUCKET, 'entries': [entry], 'objectsAndMarkers': 1,
+ 'bytesRead': 5, 'pagination': {'versionPageSizes': [1], 'currentPageSizes': [1]}}
+ self.assertEqual(lock_timeout.require_exact_inventory_entry(inventory, BUCKET, entry), entry)
+ mutations = [
+ lambda value: value.pop('pagination'),
+ lambda value: value.update(objectsAndMarkers=0),
+ lambda value: value.update(bytesRead=4),
+ lambda value: value['pagination'].update(currentPageSizes=[0]),
+ lambda value: value['entries'][0].update(versionId='foreign'),
+ lambda value: value['entries'][0].update(payload={'size': 5, 'sha256': 'b' * 64}),
+ lambda value: value['pagination'].update(versionPageSizes=[True]),
+ lambda value: value['pagination'].update(currentPageSizes=[-1, 2]),
+ lambda value: value['entries'][0].update(isLatest=1),
+ lambda value: value['entries'].append(dict(value['entries'][0])),
+ lambda value: value['entries'][0].update(payload={'size': True, 'sha256': 'a' * 64}),
+ ]
+ for mutate in mutations:
+ broken = json.loads(json.dumps(inventory))
+ mutate(broken)
+ with self.subTest(mutate=mutate), self.assertRaises(ValueError):
+ lock_timeout.require_exact_inventory_entry(broken, BUCKET, entry)
+
+ def test_timeout_ack_requires_exact_durable_put_receipt_and_endpoint(self):
+ body = b'contender'
+ request = {'schemaVersion': 2, 'runId': 'gh-12345-1-h1', 'sequence': 1,
+ 'operation': 'put_object', 'params': {'Bucket': BUCKET, 'Key': 'target.bin',
+ 'Metadata': {'heal-fixture': 'I05-timeout-contender'}}, 'identity': None,
+ 'expectHttp': 503, 'casePassClaimed': False,
+ 'clockDomain': lock_timeout.CLOCK_DOMAIN, 'preparedMonotonicNs': 50,
+ 'payload': {'size': len(body), 'sha256': hashlib.sha256(body).hexdigest()}}
+ request_bytes = json.dumps(request, sort_keys=True).encode()
+ response = {'ResponseMetadata': {'HTTPStatusCode': 503, 'RetryAttempts': 0,
+ 'RequestId': '30000000-0000-4000-8000-000000000001',
+ 'HTTPHeaders': {'retry-after': '1'}}, 'Error': {'Code': 'ServiceUnavailable'}}
+ ack = {'schemaVersion': 2, 'runId': request['runId'], 'sequence': 1,
+ 'requestFile': '000001-request.json',
+ 'requestSha256': hashlib.sha256(request_bytes).hexdigest(), 'operationCheck': 'PASS',
+ 'casePassClaimed': False,
+ 'clockDomain': lock_timeout.CLOCK_DOMAIN, 'collectorCallStartedMonotonicNs': 75,
+ 'collectorCallFinishedMonotonicNs': 225, 'recordedMonotonicNs': 250,
+ 'observation': {'endpoint': 'node1', 'clientCallInvoked': True,
+ 'operation': 'put_object', 'params': request['params'], 'payload': request['payload'],
+ 'clockDomain': lock_timeout.CLOCK_DOMAIN,
+ 'acknowledged': False, 'resultUnknown': False, 'response': response,
+ 'callStartedMonotonicNs': 100, 'callFinishedMonotonicNs': 200}}
+ self.assertEqual(lock_timeout.validate_timeout_ack(ack, request, request_bytes,
+ request['runId'], 'node1', BUCKET, 'target.bin', body), response)
+ for mutation in (
+ lambda a, r: a.update(requestSha256='0' * 64),
+ lambda a, r: a['observation'].update(endpoint='node2'),
+ lambda a, r: a['observation'].update(resultUnknown=True),
+ lambda a, r: r['params'].update(Key='foreign.bin'),
+ lambda a, r: r.update(expectHttp=200),
+ lambda a, r: a['observation'].update(clientCleanupError={'type': 'Error'}),
+ lambda a, r: a['observation'].update(operation='copy_object'),
+ lambda a, r: a['observation'].update(payload={'size': 1, 'sha256': '0' * 64}),
+ lambda a, r: a['observation'].update(clockDomain='foreign'),
+ lambda a, r: a.update(collectorCallStartedMonotonicNs=150),
+ lambda a, r: a.update(recordedMonotonicNs=150),
+ ):
+ broken_ack = json.loads(json.dumps(ack)); broken_request = json.loads(json.dumps(request))
+ mutation(broken_ack, broken_request)
+ broken_bytes = json.dumps(broken_request, sort_keys=True).encode()
+ if broken_request != request:
+ broken_ack['requestSha256'] = hashlib.sha256(broken_bytes).hexdigest()
+ with self.subTest(mutate=mutation), self.assertRaises(ValueError):
+ lock_timeout.validate_timeout_ack(broken_ack, broken_request, broken_bytes,
+ request['runId'], 'node1', BUCKET, 'target.bin', body)
+
+
+class CallerSocketTests(unittest.TestCase):
+ def request(self):
+ return {'runId': 'gh-12345-1-h1', 'action': 'caller-socket', 'expectedProcess': identity(),
+ 'clientAddress': SENT['localAddress'], 'serverAddress': SENT['peerAddress']}
+
+ def node(self):
+ return {'owned': lambda run: (None, None, {'runtimeProfile': 'lock-timeout'}),
+ 'process': lambda request: identity(), 'PROCESS_FIELDS': fleet_node.PROCESS_FIELDS}
+
+ def test_descriptor_census_distinguishes_owned_tcp_and_hashes_private_paths_without_opening_them(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ (root / 'fd').mkdir(); (root / 'net').mkdir()
+ private = '/private/test-secret/credentials.env'
+ for fd, target in ((3, 'socket:[678]'), (4, 'pipe:[999]'),
+ (5, private), (6, 'anon_inode:[eventpoll]')):
+ (root / f'fd/{fd}').symlink_to(target)
+ (root / 'net/tcp').write_text(HEADER
+ + '\n0: 0100007F:2328 0100007F:9859 01 0:0 00:0 0 1000 0 678'
+ + '\n1: 0100007F:2328 0100007F:985A 08 0:0 00:0 0 1000 0 777\n')
+ (root / 'net/tcp6').write_text(HEADER + '\n')
+ proof = sockets.descriptor_census(root)
+ self.assertEqual(proof['fileDescriptors'], 4)
+ self.assertEqual([row['kind'] for row in proof['descriptors']], ['socket', 'pipe', 'path', 'anonymous'])
+ self.assertEqual(proof['descriptors'][2]['targetSha256'], hashlib.sha256(os.fsencode(private)).hexdigest())
+ self.assertNotIn(private, json.dumps(proof))
+ self.assertEqual(len(proof['ownedTcpConnections']), 1)
+ self.assertEqual(proof['ownedTcpConnections'][0]['descriptors'], [3])
+ self.assertFalse(proof['descriptorContentsRead'] or proof['rawDescriptorPathsIncluded'] or proof['noLeakProven'])
+ readlink = os.readlink
+ def vanish(path):
+ if Path(path).name == '4': raise FileNotFoundError('closed during census')
+ return readlink(path)
+ with patch.object(sockets.os, 'readlink', side_effect=vanish):
+ changed = sockets.descriptor_census(root)
+ self.assertEqual(changed['vanishedDescriptors'], 1)
+ self.assertEqual(changed['fileDescriptors'], 4)
+ self.assertEqual(len(changed['descriptors']), 3)
+
+ def test_frozen_linux_tables_include_zero_port_listeners_and_mapped_ipv6(self):
+ ipv4 = HEADER + '\n 0: 0100007F:2328 0100007F:9859 01 0:0 00:0 0 1000 0 678\n'
+ ipv4 += ' 1: 00000000:2328 00000000:0000 0A 0:0 00:0 0 1000 0 123\n'
+ self.assertEqual(sockets.connections(ipv4, False, ('127.0.0.1', 39001), ('127.0.0.1', 9000)),
+ [{'inode': 678, 'state': '01', 'txQueueBytes': 0, 'rxQueueBytes': 0}])
+ self.assertEqual(sockets.proc_address('0000000000000000FFFF00000100007F:2328', True), ('127.0.0.1', 9000))
+ self.assertEqual(sockets.proc_address('00000000000000000000000001000000:0000', True), ('::1', 0))
+ for raw in ('', HEADER + '\n truncated', ipv4 + ipv4.splitlines()[1]):
+ with self.assertRaises(ValueError):
+ sockets.connections(raw, False, ('127.0.0.1', 39001), ('127.0.0.1', 9000))
+
+ def test_namespace_tcp_entry_without_owned_fd_does_not_count_as_owned_socket(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ (root / 'fd').mkdir(); (root / 'net').mkdir()
+ (root / 'ns').mkdir(); (root / 'ns/net').symlink_to('net:[123]')
+ (root / 'net/tcp').write_text(HEADER + '\n0: 0100007F:2328 0100007F:9859 01 0:0 00:0 0 1000 0 678\n')
+ (root / 'net/tcp6').write_text(HEADER + '\n')
+ (root / 'fd/3').symlink_to('socket:[999]')
+ with patch.object(sockets, 'Path', return_value=root):
+ proof = sockets.run(self.request(), self.node())
+ self.assertTrue(all(not row['ownedMatches'] for row in proof['samples']))
+ (root / 'fd/4').symlink_to('socket:[678]')
+ proof = sockets.run(self.request(), self.node())
+ self.assertTrue(all(row['ownedMatches'][0]['descriptors'] == [4] for row in proof['samples']))
+ changed = self.node(); calls = [identity(), identity(), {**identity(), 'startTicks': 999}]
+ changed['process'] = lambda req: calls.pop(0)
+ with self.assertRaisesRegex(ValueError, 'process birth'):
+ sockets.run(self.request(), changed)
+
+ def test_path_pid_port_and_profile_injection_fail_before_proc_access(self):
+ for change in ({'path': '/etc/shadow'}, {'pid': 1}, {'serverAddress': ['127.0.0.1', 22]},
+ {'clientAddress': ['hostname', 39001]}, {'clientAddress': ['127.0.0.1', True]}):
+ with self.subTest(change=change), patch.object(sockets, 'Path', side_effect=AssertionError('no proc read')):
+ with self.assertRaises(ValueError): sockets.run({**self.request(), **change}, self.node())
+ node = self.node(); node['owned'] = lambda run: (None, None, {'runtimeProfile': 'manual'})
+ with self.assertRaises(ValueError): sockets.run(self.request(), node)
+
+ def test_fleet_transport_embeds_the_actual_read_only_socket_backend(self):
+ def execute(*args, **kwargs):
+ source = kwargs['input'].rsplit('\nmain(', 1)[0]
+ scope = {'__name__': '__i05_socket_test__'}
+ exec(compile(source, 'i05-socket-node', 'exec'), scope)
+ self.assertTrue(callable(scope['caller_socket_backend'].run))
+ settings = scope['runtime_config']('lock-timeout')
+ self.assertEqual(settings['RUSTFS_OBJECT_LOCK_ACQUIRE_TIMEOUT'], '1')
+ self.assertEqual(settings['RUSTFS_OBJECT_LOCK_OPTIMIZATION_ENABLE'], 'false')
+ return SimpleNamespace(returncode=0, stdout='{}', stderr='')
+ with tempfile.TemporaryDirectory() as tmp, \
+ patch('scripts.validation_heal.healthy.subprocess.run', side_effect=execute):
+ fleet = observer.Fleet(['vm000'], Path(tmp), 'gh-12345-1-h1', runtime_profile='lock-timeout')
+ self.assertEqual(fleet.remote('vm000', 'caller-socket', **{k: v for k, v in self.request().items()
+ if k not in {'runId', 'action'}}), {})
+
+ def test_fleet_metrics_embed_the_same_descriptor_backend(self):
+ def execute(*args, **kwargs):
+ source = kwargs['input'].rsplit('\nmain(', 1)[0]
+ scope = {'__name__': '__i05_metrics_test__'}
+ exec(compile(source, 'i05-metrics-node', 'exec'), scope)
+ self.assertTrue(callable(scope['caller_socket_backend'].descriptor_census))
+ return SimpleNamespace(returncode=0, stdout='{}', stderr='')
+ with tempfile.TemporaryDirectory() as tmp, \
+ patch('scripts.validation_heal.healthy.subprocess.run', side_effect=execute):
+ from scripts.validation_heal.healthy import Fleet
+ fleet = Fleet(['vm000'], Path(tmp), 'gh-12345-1-h1', runtime_profile='lock-timeout')
+ self.assertEqual(fleet.remote('vm000', 'process-metrics'), {})
+
+ @unittest.skipUnless(sys.platform == 'linux', 'requires actual Linux /proc descriptor tables')
+ def test_metrics_keep_same_process_fd_fingerprints_for_live_file_and_socket(self):
+ actual = {**identity(), 'pid': os.getpid(),
+ 'startTicks': int(Path('/proc/self/stat').read_text().rpartition(') ')[2].split()[19])}
+ with tempfile.TemporaryFile() as file, socket.socket() as sock, \
+ patch.object(fleet_node, 'process', return_value=actual), \
+ patch.object(fleet_node, 'owned', return_value=(None, None, {'runtimeProfile': 'lock-timeout'})), \
+ patch.object(fleet_node, 'i05_filter_policy', return_value={'snapshotOnly': True}) as policy, \
+ patch.object(fleet_node, 'caller_socket_backend', sockets, create=True):
+ proof = fleet_node.process_metrics({'runId': 'gh-12345-1-h1', 'action': 'process-metrics'})
+ census = proof['descriptorCensus']
+ by_fd = {row['descriptor']: row for row in census['descriptors']}
+ self.assertEqual(by_fd[file.fileno()]['kind'], 'path')
+ self.assertEqual(by_fd[sock.fileno()]['kind'], 'socket')
+ self.assertEqual(proof['networkNamespace'], os.readlink('/proc/self/ns/net'))
+ self.assertEqual(proof['fileDescriptors'], census['fileDescriptors'])
+ self.assertTrue(census['snapshotOnly'])
+ policy.assert_called_once_with({'runtimeProfile': 'lock-timeout'}, actual['pid'])
+ self.assertEqual(proof['platformNetworkPolicy'], {'snapshotOnly': True})
+ with patch.object(fleet_node, 'process', side_effect=[actual, {**actual, 'startTicks': 1}]):
+ with self.assertRaisesRegex(ValueError, 'identity change'):
+ fleet_node.process_metrics({'runId': 'gh-12345-1-h1', 'action': 'process-metrics'})
+ with patch.object(fleet_node.os, 'readlink', side_effect=['net:[1]', 'net:[2]']), \
+ patch.object(sockets, 'descriptor_census', return_value=census):
+ with self.assertRaisesRegex(ValueError, 'network namespace change'):
+ fleet_node.process_metrics({'runId': 'gh-12345-1-h1', 'action': 'process-metrics'})
+
+ @unittest.skipUnless(sys.platform == 'linux', 'requires actual Linux /proc TCP tables')
+ def test_actual_linux_backpressured_response_has_an_owned_nonempty_send_queue(self):
+ with socket.socket() as listener, socket.socket() as client:
+ listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ listener.bind(('127.0.0.1', 9000)); listener.listen(1)
+ client.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 4096)
+ client.connect(listener.getsockname())
+ accepted, _ = listener.accept()
+ try:
+ accepted.setblocking(False)
+ remaining = memoryview(b'x' * (8 * 1024**2))
+ deadline = time.monotonic() + 3
+ while remaining:
+ try:
+ remaining = remaining[accepted.send(remaining):]
+ except BlockingIOError:
+ break
+ self.assertLess(time.monotonic(), deadline)
+ self.assertTrue(remaining, 'the unread response must hit actual TCP backpressure')
+ node = self.node()
+ actual = {**identity(), 'pid': os.getpid()}
+ node['process'] = lambda req: actual
+ req = {**self.request(), 'expectedProcess': actual, 'clientAddress': list(client.getsockname())}
+ proof = sockets.run(req, node)
+ for sample in proof['samples']:
+ match = sample['ownedMatches'][0]
+ self.assertEqual(match['descriptors'], [accepted.fileno()])
+ self.assertGreater(match['txQueueBytes'], 0)
+ finally:
+ accepted.close()
+
+ @unittest.skipUnless(sys.platform == 'linux', 'requires actual Linux /proc TCP tables')
+ def test_actual_linux_socket_is_bound_to_accepted_descriptor_and_disappears_after_close(self):
+ with socket.socket() as listener, socket.socket() as client:
+ listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ listener.bind(('127.0.0.1', 9000)); listener.listen(1)
+ client.connect(listener.getsockname())
+ accepted, _ = listener.accept()
+ node = self.node()
+ actual = {**identity(), 'pid': os.getpid()}
+ node['process'] = lambda req: actual
+ req = {**self.request(), 'expectedProcess': actual, 'clientAddress': list(client.getsockname())}
+ try:
+ proof = sockets.run(req, node)
+ self.assertTrue(all(row['ownedMatches'][0]['descriptors'] == [accepted.fileno()] for row in proof['samples']))
+ finally:
+ accepted.close()
+ proof = sockets.run(req, node)
+ self.assertTrue(all(not row['ownedMatches'] for row in proof['samples']))
+
+
+class ObserverFlowTests(unittest.TestCase):
+ def setUp(self):
+ self.tmp = tempfile.TemporaryDirectory()
+ self.root = Path(self.tmp.name)
+ # Reuse the existing real local wire fixture without inheriting its tests.
+ self.wire = wire_fixture.CallerAbortTests('test_one_signed_exact_version_get_closes_real_socket_and_retains_unknown')
+ self.wire.setUp()
+ self.addCleanup(self.wire.tearDown); self.addCleanup(self.tmp.cleanup)
+ self.probe = self.wire.probe
+ self.probe.binding.update(candidateCommit=observer.CANDIDATE)
+ self.probe.binding['endpoints']['node1'] = 'http://rustfs-node1:9000'
+ self.probe.endpoint = 'http://rustfs-node1:9000'
+ connect = socket.create_connection
+ def local_wire(address, *args, **kwargs):
+ self.assertEqual(address, ('rustfs-node1', 9000))
+ return connect(('127.0.0.1', self.wire.server.server_port), *args, **kwargs)
+ self.connect_patch = patch('scripts.validation_heal.caller_abort.socket.create_connection', side_effect=local_wire)
+ self.connect_patch.start(); self.addCleanup(self.connect_patch.stop)
+ self.entry = {'key': 'target.bin', 'versionId': VERSION, 'deleteMarker': False,
+ 'payload': {'size': 1, 'sha256': hashlib.sha256(b'x').hexdigest()}}
+ self.stage = 'before'
+ self.sent = None
+ self.change = None
+ self.labels = []
+ self.contender_rows = []
+ owner = self
+ class Fleet:
+ nodes = ['vm000']; run_id = owner.probe.binding['runId']; runtime_profile = 'lock-timeout'
+ def record(self, label, value):
+ owner.labels.append(label)
+ return value
+ def remote(self, node, action, **params):
+ if action == 'process-metrics':
+ return {'identity': identity(), 'fileDescriptors': 10 if owner.change != 'leak' else
+ 11 if owner.stage == 'after' else 10}
+ if action == 'caller-socket':
+ matches = [{'state': '01', 'inode': 678, 'descriptors': [4],
+ 'txQueueBytes': 4096}] if owner.stage == 'before' else []
+ return {'process': identity(), 'clockDomain': identity()['bootId'], 'readOnly': True,
+ 'networkNamespace': 'net:[2]' if owner.change == 'namespace' and owner.stage == 'after' else 'net:[1]',
+ 'samples': [{'ownedMatches': matches, 'vanishedDescriptors': 0},
+ {'ownedMatches': matches, 'vanishedDescriptors': 0}]}
+ raise AssertionError(action)
+ class Window:
+ def __init__(self, *args, **kwargs): pass
+ def capture(self):
+ rows = [event('acquiring', owner.sent), event('acquired', owner.sent)]
+ if owner.stage == 'after' or owner.change == 'early-drop' or (
+ owner.change == 'timeout-drop' and owner.contender_rows):
+ rows.append(event('guard_dropped', owner.sent))
+ if owner.change == 'owner' and owner.stage == 'after': rows[-1]['owner'] = 'rebound'
+ return {node: rows + owner.contender_rows if node == 'vm000' else []
+ for node in owner.fleet.nodes}
+ self.fleet = Fleet()
+ self.window_patch = patch.object(observer, 'JournalWindow', Window); self.window_patch.start()
+ self.addCleanup(self.window_patch.stop)
+ original = observer.disconnect_get
+ def transport(probe, bucket, key, version, output, *, before_disconnect, **options):
+ def on_sent(sent):
+ owner.sent = sent
+ before_disconnect(sent)
+ value = original(probe, bucket, key, version, output, before_disconnect=on_sent, **options)
+ self.stage = 'after'
+ return value
+ self.transport_patch = patch.object(observer, 'disconnect_get', side_effect=transport); self.transport_patch.start()
+ self.addCleanup(self.transport_patch.stop)
+ self.probe.s3 = lambda *args: {'acknowledged': True, 'resultUnknown': False,
+ 'response': {'VersionId': VERSION}, 'payload': self.entry['payload']}
+
+ def invoke(self):
+ return observer.observe_disconnect(self.probe, self.fleet, BUCKET, self.entry, self.root / 'observe')
+
+ def test_complete_component_keeps_original_unknown_and_all_missing_case_proofs(self):
+ proof = self.invoke()
+ self.assertTrue(proof['observationComplete'] and proof['exactHistoricalContentPreserved'])
+ for name in ('heldAtDisconnectProven', 'distributedReleaseProven', 'noGlobalFdLeakProven', 'canonicalCasePassClaimed'):
+ self.assertFalse(proof[name])
+ transport = json.loads((self.root / 'observe/transport/disconnected.json').read_text())
+ self.assertTrue(transport['resultUnknown'])
+ self.assertFalse(transport['acknowledged'] or transport['serverOwnershipProven'])
+ self.assertTrue((self.root / 'observe/admission.json').is_file())
+
+ def test_closed_before_disconnect_is_not_observed_and_closes_original_socket(self):
+ self.change = 'early-drop'
+ with self.assertRaisesRegex(ValueError, 'NOT_OBSERVED'):
+ self.invoke()
+ self.assertTrue(self.wire.server.disconnected.wait(3))
+ self.assertFalse(json.loads((self.root / 'observe/failure.json').read_text())['observationComplete'])
+ self.assertEqual(len(self.wire.server.requests), 1)
+
+ def test_rebound_owner_fd_leak_or_wrong_final_content_cannot_complete_observation(self):
+ self.change = 'owner'
+ with self.assertRaises(ValueError): self.invoke()
+ self.assertFalse((self.root / 'observe/observation.json').exists())
+
+ def test_network_namespace_move_cannot_hide_original_socket_during_drain(self):
+ self.change = 'namespace'
+ with self.assertRaisesRegex(ValueError, 'network namespace between'):
+ self.invoke()
+ self.assertFalse((self.root / 'observe/observation.json').exists())
+
+ def test_fd_count_must_return_to_baseline_and_original_failure_is_kept(self):
+ self.change = 'leak'
+ clock = [0]
+ def advance(seconds): clock[0] += observer.DRAIN_SECONDS + 1
+ with patch.object(observer.time, 'monotonic', side_effect=lambda: clock[0]), \
+ patch.object(observer.time, 'sleep', side_effect=advance), \
+ self.assertRaisesRegex(ValueError, 'caller drain observation exceeded its budget'):
+ self.invoke()
+ self.assertEqual(self.labels.count('I05-caller-drain-resources'), 2)
+ self.assertFalse((self.root / 'observe/observation.json').exists())
+
+ def test_late_admission_cannot_turn_an_expired_observation_into_completion(self):
+ with patch.object(observer, 'ADMISSION_SECONDS', 0), self.assertRaisesRegex(ValueError, 'exceeded its budget'):
+ self.invoke()
+ self.assertTrue(self.wire.server.disconnected.wait(3))
+ self.assertFalse((self.root / 'observe/observation.json').exists())
+
+ def test_final_version_must_match_the_original_ack_content(self):
+ self.probe.s3 = lambda *args: {'acknowledged': True, 'resultUnknown': False,
+ 'response': {'VersionId': VERSION}, 'payload': {'size': 1, 'sha256': '0' * 64}}
+ with self.assertRaisesRegex(ValueError, 'historical version'):
+ self.invoke()
+
+ def test_unreviewed_source_cannot_read_or_send_any_request(self):
+ self.probe.binding['candidateCommit'] = 'a' * 40
+ with patch.object(self.fleet, 'remote', side_effect=AssertionError('no remote call')):
+ with self.assertRaisesRegex(ValueError, 'reviewed candidate'):
+ self.invoke()
+ self.assertFalse(self.wire.server.requests)
diff --git a/test_validation_heal_lock_source.py b/test_validation_heal_lock_source.py
new file mode 100644
index 00000000..7be4ffee
--- /dev/null
+++ b/test_validation_heal_lock_source.py
@@ -0,0 +1,184 @@
+"""I05 source orchestration and links to real caller transport/ACK evidence."""
+
+import copy
+import json
+import subprocess
+import sys
+from pathlib import Path
+import tempfile
+from types import SimpleNamespace
+import unittest
+from unittest.mock import patch
+
+from scripts.validation_heal import lock_source as source, lock_timeout_runner as runner
+from scripts.validation_heal import race_native as native
+import test_validation_heal_lock_timeout as timeout_tests
+import test_validation_heal_lock_successor as successor_tests
+import test_validation_heal_race_native as native_tests
+
+
+class SourceNativeTests(unittest.TestCase):
+ def setUp(self):
+ self.tmp = tempfile.TemporaryDirectory()
+ self.addCleanup(self.tmp.cleanup)
+ self.root = Path(self.tmp.name)
+ native_tests.RaceNativeTests().fixture(self.root, source=runner.CANDIDATE)
+ self.args = SimpleNamespace(native_directory=self.root, testing_commit=native_tests.HELPER)
+ self.candidate = {'source_sha': runner.CANDIDATE, 'package_sha256': 'a' * 64}
+
+ def test_real_readonly_verifier_composes_owner_and_full_stale_authority(self):
+ with patch.object(native.subprocess, 'run', side_effect=AssertionError('never execute downloaded binary')):
+ proof = source.verify_source_native(self.args, self.candidate)
+ self.assertEqual(set(proof['groups']), set(source.GROUPS))
+ self.assertTrue(proof['nativeOwnerAndStaleFencesPassed'])
+ self.assertFalse(proof['fleetExecutionClaimed'] or proof['canonicalCasePassClaimed'])
+ for case, label, count in (
+ ('heal.race-versioned-put', 'explicit-version-append-final-rename', 6),
+ ('heal.race-version-delete', 'explicit-version-delete-final-rename', 12)):
+ regression = proof['groups'][case]['observedRegressions'][label]
+ self.assertEqual(regression['witnessKind'], 'ordered-barrier-milestones')
+ self.assertEqual(len(regression['milestones']), count)
+ self.assertIn('full_bodies_verified', regression['milestones'][-1])
+
+ def test_workflow_cli_verifies_all_native_subsets_without_running_binary(self):
+ candidate = self.root / 'candidate.json'
+ candidate.write_text(json.dumps(self.candidate))
+ result = subprocess.run([sys.executable, '-B', str(Path(source.__file__)),
+ '--directory', str(self.root), '--candidate', str(candidate),
+ '--testing-commit', self.args.testing_commit], capture_output=True, text=True)
+ self.assertEqual(result.returncode, 0, result.stderr)
+ proof = json.loads(result.stdout)
+ self.assertEqual(set(proof['groups']), set(source.GROUPS))
+ self.assertTrue(proof['nativeOwnerAndStaleFencesPassed'])
+ self.assertFalse(proof['canonicalCasePassClaimed'])
+
+ def test_mixed_workflow_attempt_binary_or_source_cannot_form_one_proof(self):
+ proofs = source.verify_source_native(self.args, self.candidate)['groups']
+ for key, value in (('runId', 999), ('runAttempt', 99), ('binarySha256', 'f' * 64),
+ ('candidateSourceTree', 'f' * 40), ('selectedContractSha256', 'f' * 64)):
+ with self.subTest(key=key):
+ changed = copy.deepcopy(proofs)
+ changed['heal.race-version-delete'][key] = value
+ with patch.object(source, 'verify_native', side_effect=lambda d, c, h, case: changed[case]):
+ with self.assertRaisesRegex(ValueError, 'mix candidates'):
+ source.verify_source_native(self.args, self.candidate)
+
+ def test_missing_stale_body_milestone_or_original_log_blocks_before_fleet(self):
+ log = self.root / 'native-evidence/explicit-version-append-final-rename.log'
+ log.write_text(log.read_text().replace('full_bodies_verified', 'etag_only'))
+ with self.assertRaisesRegex(ValueError, 'original native log bytes differ'):
+ source.verify_source_native(self.args, self.candidate)
+
+ def test_missing_artifact_does_not_execute_a_partial_source_plan(self):
+ self.args.native_directory = None
+ with self.assertRaisesRegex(ValueError, 'requires exact-source native evidence'):
+ source.verify_source_native(self.args, self.candidate)
+
+
+class SourceEvidenceTests(unittest.TestCase):
+ def scene(self, branch):
+ case = (timeout_tests.TimeoutFlowTests() if branch == 'timeout' else successor_tests.SuccessorFlowTests())
+ case.setUp()
+ self.addCleanup(case.doCleanups)
+ diagnostic = case.invoke()
+ # Components execute inside a topology directory in the production runner.
+ parent = case.output.parent
+ desired = parent / branch
+ if case.output != desired:
+ case.output.rename(desired)
+ return parent, case.probe.binding, diagnostic
+
+ def test_actual_tcp_timeout_and_queued_successor_bytes_form_separate_chains(self):
+ # Each test fixture supplies a real single-use HTTP connection; server
+ # lock observations are controlled test data, never live native proof.
+ for branch in source.LIVE_BRANCHES:
+ with self.subTest(branch=branch):
+ parent, binding, diagnostic = self.scene(branch)
+ chain = source.link_fleet_evidence(parent, branch, binding, diagnostic)
+ self.assertTrue(chain['sourceEvidenceComplete'])
+ self.assertFalse(chain['heldAtDisconnectProven'] or chain['distributedReleaseProven'])
+ self.assertTrue(chain['nativeOwnerProofRequiredSeparately'])
+ self.assertEqual(chain['coordinatorTimes'], sorted(chain['coordinatorTimes']))
+ self.assertIn(branch + '/caller/transport/request.json', chain['originalEvidenceSha256'])
+ self.assertNotEqual(chain['callerRequestId'], chain['foregroundRequestId'])
+ # Prevent shared fixture patches from nesting across subtests.
+ self.doCleanups()
+
+ def test_foreign_binding_and_changed_request_digest_are_rejected(self):
+ parent, binding, diagnostic = self.scene('timeout')
+ foreign = {**binding, 'candidateCommit': 'f' * 40}
+ with self.assertRaisesRegex(ValueError, 'changed binding'):
+ source.link_fleet_evidence(parent, 'timeout', foreign, diagnostic)
+ path = parent / 'timeout/caller/transport/request.json'
+ intent = json.loads(path.read_text())
+ intent['preparedMonotonicNs'] += 1
+ path.write_text(json.dumps(intent))
+ with self.assertRaisesRegex(ValueError, 'single unretried TCP'):
+ source.link_fleet_evidence(parent, 'timeout', binding, diagnostic)
+
+ def test_clock_inversion_and_invented_disconnect_ownership_are_rejected(self):
+ parent, binding, diagnostic = self.scene('queued-successor')
+ path = parent / 'queued-successor/queued.json'
+ original = path.read_text()
+ queued = json.loads(original)
+ queued['observedMonotonicNs'] = 1
+ path.write_text(json.dumps(queued))
+ with self.assertRaisesRegex(ValueError, 'clock order'):
+ source.link_fleet_evidence(parent, 'queued-successor', binding, diagnostic)
+ path.write_text(original)
+ path = parent / 'queued-successor/caller/observation.json'
+ caller = json.loads(path.read_text())
+ caller['heldAtDisconnectProven'] = True
+ path.write_text(json.dumps(caller))
+ with self.assertRaisesRegex(ValueError, 'invent.*ownership'):
+ source.link_fleet_evidence(parent, 'queued-successor', binding, diagnostic)
+
+
+class SourcePlanTests(unittest.TestCase):
+ def setUp(self):
+ self.tmp = tempfile.TemporaryDirectory()
+ self.addCleanup(self.tmp.cleanup)
+ self.args = SimpleNamespace(evidence=Path(self.tmp.name), topologies=['H1', 'H2'])
+ self.calls = []
+ self.failure = None
+ self.proof = {'nativeOwnerAndStaleFencesPassed': True}
+
+ def topology(self, args, candidate, inspector, topology):
+ label = (args.diagnostic_branch, topology)
+ self.calls.append((label, args.evidence, args.source_plan))
+ return {'branch': label[0], 'topology': topology, 'diagnosticComplete': label != self.failure,
+ 'cleanup': 'FAIL' if label == self.failure else 'PASS',
+ 'sourceEvidenceChain': {'sourceEvidenceComplete': label != self.failure},
+ 'canonicalCasePassClaimed': False}
+
+ def test_serial_source_plan_preserves_each_controller_and_native_scope(self):
+ summary = source.run_source_plan(self.args, {}, {}, self.topology, self.proof)
+ self.assertEqual([row[0] for row in self.calls], [
+ ('timeout', 'H1'), ('timeout', 'H2'), ('queued-successor', 'H1'), ('queued-successor', 'H2')])
+ self.assertTrue(all(row[2] for row in self.calls))
+ self.assertNotEqual(self.calls[0][1], self.calls[-1][1])
+ self.assertTrue(summary['sourceProceduresComplete'] and summary['diagnosticComplete'])
+ self.assertEqual(summary['remainingLiveInstances'], [])
+ self.assertFalse(summary['complete'] or summary['wholeCasePassClaimed'] or summary['canonicalCasePassClaimed'])
+ self.assertEqual(set(summary['sourceBranches']), {
+ 'object-lock-timeout', 'caller-disconnect', 'stale-read-repair', 'native-lock-owner-fence'})
+ self.assertEqual(summary['sourceBranches']['stale-read-repair']['scope'], 'independent-native')
+ self.assertEqual(json.loads((self.args.evidence / 'healthy-summary.json').read_text()), summary)
+
+ def test_product_or_cleanup_failure_retains_first_scene_and_never_runs_next(self):
+ self.failure = ('timeout', 'H2')
+ summary = source.run_source_plan(self.args, {}, {}, self.topology, self.proof)
+ self.assertEqual([row[0] for row in self.calls], [('timeout', 'H1'), ('timeout', 'H2')])
+ self.assertFalse(summary['sourceProceduresComplete'])
+ self.assertEqual(summary['cleanup'], 'FAIL')
+ self.assertEqual(summary['remainingLiveInstances'], ['timeout/H2', 'queued-successor/H1', 'queued-successor/H2'])
+
+ def test_h1_only_cannot_claim_h2_source_procedures_complete(self):
+ self.args.topologies = ['H1']
+ summary = source.run_source_plan(self.args, {}, {}, self.topology, self.proof)
+ self.assertTrue(summary['diagnosticComplete'])
+ self.assertFalse(summary['sourceProceduresComplete'])
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/test_validation_heal_lock_successor.py b/test_validation_heal_lock_successor.py
new file mode 100644
index 00000000..3c738a34
--- /dev/null
+++ b/test_validation_heal_lock_successor.py
@@ -0,0 +1,351 @@
+"""I05 successor admission, one ACK, physical generations and failure retention."""
+
+import copy
+from datetime import datetime, timedelta, timezone
+import hashlib
+import json
+from pathlib import Path
+import tempfile
+import threading
+import time
+import unittest
+from unittest.mock import patch
+import uuid
+
+from scripts.validation_heal import fleet_node, lock_successor as successor
+from scripts.validation_heal.heal_workload import Recorder
+import test_validation_heal_lock_observer as fixture
+import test_validation_heal as physical_fixture
+
+
+REQUEST = '30000000-0000-4000-8000-000000000001'
+ATTEMPT = '40000000-0000-4000-8000-000000000001'
+VERSION = '50000000-0000-4000-8000-000000000001'
+MARKER = 'auto-testing-i05-successor/' + REQUEST
+HOLDER = {'request_id': fixture.REQUEST, 'lock_attempt_id': fixture.ATTEMPT,
+ 'owner': 'owner-one', 'namespace': 'namespace-one'}
+
+
+def events(marker=MARKER, states=('acquiring',)):
+ return [{'target': fixture.observer.TARGET, 'event': 'object_namespace_lock',
+ 'component': 'ecstore', 'subsystem': 'object_lock', 'state': state, 'level': 'TRACE',
+ 'op': 'put_object_commit', 'mode': 'write', 'bucket': fixture.BUCKET, 'object': 'target.bin',
+ 'namespace': 'namespace-one', 'owner': 'owner-one', 'lock_attempt_id': ATTEMPT,
+ 'request_id': REQUEST, 'timeout_ms': successor.PUT_TIMEOUT_MS,
+ 'acquire_ms': 0 if state == 'acquiring' else 15, 'hold_ms': 0,
+ 'span': {'name': 'http-request', 'user_agent': 'SDK ' + marker,
+ 'request_id': REQUEST, 'method': 'PUT', 'uri': f'/{fixture.BUCKET}/target.bin'}}
+ for state in states]
+
+
+def observation(probe, params, body):
+ return {'operation': 'put_object', 'params': params, 'endpoint': probe.endpoint_name,
+ 'payload': {'size': len(body), 'sha256': hashlib.sha256(body).hexdigest()},
+ 'clientCallInvoked': True, 'acknowledged': True, 'resultUnknown': False,
+ 'clockDomain': successor.CLOCK_DOMAIN, 'callStartedMonotonicNs': time.monotonic_ns(),
+ 'callFinishedMonotonicNs': time.monotonic_ns(), 'response': {
+ 'ResponseMetadata': {'HTTPStatusCode': 200, 'RetryAttempts': 0, 'RequestId': REQUEST},
+ 'VersionId': VERSION, 'ETag': '"' + hashlib.md5(body, usedforsecurity=False).hexdigest() + '"'}}
+
+
+class SuccessorEvidenceTests(unittest.TestCase):
+ def test_exact_request_queue_and_complete_lifecycle(self):
+ for states in (('acquiring',), ('acquiring', 'acquired'), ('acquiring', 'acquired', 'guard_dropped')):
+ proof = successor.successor_attempt(events(states=states), MARKER, HOLDER, fixture.BUCKET, 'target.bin')
+ self.assertEqual(proof['states'], list(states))
+ self.assertFalse(proof['canonicalCasePassClaimed'])
+ self.assertIsNone(successor.successor_attempt([], MARKER, HOLDER, fixture.BUCKET, 'target.bin'))
+
+ def test_forged_request_owner_version_budget_and_duplicate_attempt_refuse(self):
+ mutations = [lambda r: r.update(request_id=fixture.REQUEST), lambda r: r.update(owner='foreign'),
+ lambda r: r.update(namespace='foreign'), lambda r: r.update(timeout_ms=1000),
+ lambda r: r.update(requested_version_id=VERSION), lambda r: r.update(object='other.bin'),
+ lambda r: r.update(state='failed'), lambda r: r.update(acquire_ms=True),
+ lambda r: r['span'].update(method='GET'), lambda r: r['span'].update(uri='/other'),
+ lambda r: r.update(lock_attempt_id=fixture.ATTEMPT)]
+ for mutate in mutations:
+ rows = events(); mutate(rows[0])
+ with self.subTest(mutate=mutate), self.assertRaises(ValueError):
+ successor.successor_attempt(rows, MARKER, HOLDER, fixture.BUCKET, 'target.bin')
+ for states in (('acquired',), ('acquiring', 'acquiring'), ('acquiring', 'guard_dropped')):
+ with self.subTest(states=states), self.assertRaises(ValueError):
+ successor.successor_attempt(events(states=states), MARKER, HOLDER, fixture.BUCKET, 'target.bin')
+ rows = events(); other = copy.deepcopy(rows[0]); other['request_id'] = VERSION
+ other['span']['request_id'] = VERSION; rows.append(other)
+ with self.assertRaisesRegex(ValueError, 'retried or reused'):
+ successor.successor_attempt(rows, MARKER, HOLDER, fixture.BUCKET, 'target.bin')
+
+ def test_original_live_metadata_reads_keep_one_exact_target_write_attempt(self):
+ path = Path(__file__).with_name('testdata') / 'i05-successor-metadata-39788321908.ndjson'
+ rows = [json.loads(line) for line in path.read_text().splitlines()]
+ marker = 'auto-testing-i05-successor/73a9d02c-6579-44ce-8152-011f367dc891'
+ holder = {'request_id': 'c10cb980-5924-4e9f-bbba-14d180d56436',
+ 'lock_attempt_id': 'd3e2e124-e6cc-46c4-897a-cbbc7b2dba66',
+ 'owner': ':::9000', 'namespace': 'pool-0'}
+ bucket = 'rv-heal-5dd2430e083c6140'
+ for length in range(1, len(rows) + 1):
+ proof = successor.successor_attempt(rows[:length], marker, holder, bucket, 'objects/0000.bin')
+ writes = [row for row in rows[:length] if row['mode'] == 'write']
+ if not writes:
+ self.assertIsNone(proof)
+ else:
+ self.assertEqual(proof['events'], writes)
+ self.assertEqual(proof['lockAttemptId'], 'b31b1a30-9431-425e-9a92-909b47ac6bfc')
+ self.assertFalse(proof['canonicalCasePassClaimed'])
+ metadata = next(i for i, row in enumerate(rows) if row['object'].endswith('/.metadata.bin'))
+ mutations = [lambda r: r.update(object='buckets/foreign/.metadata.bin'),
+ lambda r: r.update(object=f'buckets/{bucket}/unreviewed'),
+ lambda r: r.update(mode='write'), lambda r: r.update(op='put_object_commit'),
+ lambda r: r.update(owner='foreign'), lambda r: r.update(namespace='foreign'),
+ lambda r: r.update(timeout_ms=successor.PUT_TIMEOUT_MS),
+ lambda r: r.update(requested_version_id=VERSION),
+ lambda r: r.update(component='foreign'), lambda r: r.update(event='foreign'),
+ lambda r: r.update(state='failed'), lambda r: r.update(hold_ms=True),
+ lambda r: r.update(lock_attempt_id=holder['lock_attempt_id']),
+ lambda r: r.update(lock_attempt_id='b31b1a30-9431-425e-9a92-909b47ac6bfc')]
+ for mutate in mutations:
+ changed = copy.deepcopy(rows); mutate(changed[metadata])
+ with self.subTest(mutate=mutate), self.assertRaises(ValueError):
+ successor.successor_attempt(changed, marker, holder, bucket, 'objects/0000.bin')
+ for changed in [rows[:metadata] + rows[metadata + 1:],
+ rows[:metadata] + [rows[metadata]] + rows[metadata:],
+ rows[:metadata + 1] + [dict(rows[metadata + 1], object=f'buckets/{bucket}/.bucket-incarnation')]
+ + rows[metadata + 2:]]:
+ with self.subTest(rows=changed), self.assertRaises(ValueError):
+ successor.successor_attempt(changed, marker, holder, bucket, 'objects/0000.bin')
+
+ def test_real_slow_acquire_warning_never_replaces_target_lifecycle(self):
+ path = Path(__file__).with_name('testdata') / 'i05-successor-slow-acquire-39810035277.ndjson'
+ rows = [json.loads(line) for line in path.read_text().splitlines()]
+ marker = 'auto-testing-i05-successor/5a37a691-d1d8-4591-b5a4-f174b7fe4818'
+ holder = {'request_id': fixture.REQUEST, 'lock_attempt_id': fixture.ATTEMPT,
+ 'owner': 'rustfs-node1:9000', 'namespace': 'pool-0'}
+ bucket = 'rv-heal-e9da35d01389c2ae'
+ warning = next(i for i, row in enumerate(rows) if row['level'] == 'WARN')
+ for length in range(1, len(rows) + 1):
+ proof = successor.successor_attempt(rows[:length], marker, holder, bucket, 'objects/0000.bin')
+ writes = [row for row in rows[:length] if row.get('event') == 'object_namespace_lock'
+ and row['mode'] == 'write']
+ if writes:
+ self.assertEqual(proof['events'], writes)
+ self.assertEqual(proof['lockAttemptId'], 'de759fe0-8dfe-4b32-841a-047a23b8ed7c')
+ else:
+ self.assertIsNone(proof)
+ mutations = [lambda r: r.update(event='object_namespace_lock'),
+ lambda r: r.update(state='acquired'), lambda r: r.update(level='ERROR'),
+ lambda r: r.update(namespace='pool-0'), lambda r: r.update(owner='foreign'),
+ lambda r: r.update(object='foreign'), lambda r: r.update(bucket='foreign'),
+ lambda r: r.update(mode='read'), lambda r: r.update(op='get_object'),
+ lambda r: r.update(acquire_ms=True), lambda r: r.update(acquire_ms='499'),
+ lambda r: r.update(threshold_ms='0'), lambda r: r.update(threshold_ms='0500'),
+ lambda r: r.update(message='unreviewed lock warning')]
+ for mutate in mutations:
+ changed = copy.deepcopy(rows); mutate(changed[warning])
+ with self.subTest(mutate=mutate), self.assertRaises(ValueError):
+ successor.successor_attempt(changed, marker, holder, bucket, 'objects/0000.bin')
+ for changed in [rows[:warning - 1] + rows[warning:],
+ [rows[warning]], rows[:warning - 1] + [rows[warning]] + rows[warning - 1:]]:
+ with self.subTest(rows=changed), self.assertRaises(ValueError):
+ successor.successor_attempt(changed, marker, holder, bucket, 'objects/0000.bin')
+ # Removing the auxiliary warning leaves the same three target events.
+ plain = successor.successor_attempt(rows[:warning] + rows[warning + 1:], marker, holder, bucket, 'objects/0000.bin')
+ self.assertEqual(plain['states'], ['acquiring', 'acquired', 'guard_dropped'])
+ undrained = [row for row in rows if not (row.get('mode') == 'write' and row.get('state') == 'guard_dropped')]
+ proof = successor.successor_attempt(undrained, marker, holder, bucket, 'objects/0000.bin')
+ self.assertEqual(proof['states'], ['acquiring', 'acquired'])
+ self.assertNotIn('guard_dropped', proof['states'])
+
+ def test_real_recorder_ack_binding_rejects_unknown_retry_wrong_content_and_time(self):
+ with tempfile.TemporaryDirectory() as folder:
+ probe = type('Probe', (), {'binding': {
+ 'schemaVersion': 1, 'runId': 'gh-12345-1-h1', 'jumpHostOnly': True, 'exclusiveHealService': True,
+ 'leaseExpiresAt': (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat(),
+ 'candidateCommit': 'a' * 40, 'harnessCommit': 'b' * 40,
+ 'candidatePackageSha256': 'c' * 64, 'candidateBinarySha256': 'd' * 64, 'rcBinarySha256': 'e' * 64,
+ 'bucketPrefix': fixture.BUCKET, 'defaultEndpoint': 'node1',
+ 'endpoints': {'node1': 'http://127.0.0.1:9000'},
+ 'maxInventoryObjects': 32, 'maxInventoryBytes': 512 * 1024**2}, 'endpoint_name': 'node1'})()
+ probe.s3 = lambda op, params, body: observation(probe, params, body)
+ recorder = Recorder(probe, Path(folder) / 'receipt')
+ body = b'one-successor'
+ recorder.call('put_object', {'Bucket': fixture.BUCKET, 'Key': 'target.bin',
+ 'ContentType': 'application/octet-stream', 'Metadata': {'heal-fixture': 'I05-queued-successor'}}, body)
+ attempt = {'requestId': REQUEST}
+ proof = successor.validate_successor_ack(recorder.output, probe, fixture.BUCKET, 'target.bin', body, attempt)
+ self.assertEqual(proof['versionId'], VERSION)
+ ack_path = recorder.output / '000001-ack.json'; original = json.loads(ack_path.read_text())
+ for mutate in (lambda a: a['observation'].update(resultUnknown=True),
+ lambda a: a['observation']['response']['ResponseMetadata'].update(RetryAttempts=1),
+ lambda a: a['observation']['response'].update(ETag='"wrong"'),
+ lambda a: a['observation']['response'].update(VersionId='null'),
+ lambda a: a['observation']['response']['ResponseMetadata'].update(RequestId=fixture.REQUEST),
+ lambda a: a.update(requestSha256='0' * 64),
+ lambda a: a.update(recordedMonotonicNs=1)):
+ value = copy.deepcopy(original); mutate(value); ack_path.write_text(json.dumps(value))
+ with self.subTest(mutate=mutate), self.assertRaises(ValueError):
+ successor.validate_successor_ack(recorder.output, probe, fixture.BUCKET, 'target.bin', body, attempt)
+
+ def scene(self):
+ before = physical_fixture.HealShardOracleTest().snapshot()
+ original = {'key': 'target.bin', 'versionId': fixture.VERSION, 'deleteMarker': False, 'isLatest': True}
+ added = {'key': 'target.bin', 'versionId': VERSION, 'deleteMarker': False, 'isLatest': True,
+ 'payload': {'size': 1024, 'sha256': 'a' * 64}}
+ for value in before.values():
+ keep = []
+ for metadata, part in zip(value['files'][::3], value['files'][1::3], strict=True):
+ metadata = copy.deepcopy(metadata)
+ metadata['path'] = f'{fixture.BUCKET}/target.bin/xl.meta'
+ metadata['metadata']['key'] = 'target.bin'
+ version = metadata['metadata']['versions'][0]
+ version.update(versionId=fixture.VERSION, isLatest=True)
+ keep += [metadata, {**part, 'path': f"{fixture.BUCKET}/target.bin/{version['dataDir']}/part.1"}]
+ value['files'] = keep
+ after = copy.deepcopy(before)
+ for value in after.values():
+ new_parts = []
+ for metadata, part in zip(value['files'][::2], value['files'][1::2], strict=True):
+ old = metadata['metadata']['versions'][0]; old['isLatest'] = False
+ new = copy.deepcopy(old); new.update(versionId=VERSION, isLatest=True, size=1024,
+ dataDir=str(uuid.UUID(int=777)))
+ new['parts'] = [{**p, 'size': 1024, 'actualSize': 1024} for p in new['parts']]
+ metadata['metadata']['versions'].append(new)
+ new_parts.append({**part, 'path': f"{fixture.BUCKET}/target.bin/{new['dataDir']}/part.1",
+ 'sha256': part['sha256'] + '-new'})
+ value['files'] += new_parts
+ return before, after, {'entries': [original]}, added
+
+ def test_real_physical_map_preserves_old_generation_and_exact_version_population(self):
+ before, after, baseline, added = self.scene()
+ target = successor.physical_append(before, after, fixture.BUCKET, baseline, added)
+ self.assertEqual(target['versionId'], VERSION)
+ for mutate in (lambda s: s['files'].pop(),
+ lambda s: s['files'][1].update(sha256='wrong'),
+ lambda s: s['files'][0]['metadata']['versions'].pop(0),
+ lambda s: s['files'].append({'disk': 0, 'path': 'unacknowledged', 'sha256': 'wrong'})):
+ changed = copy.deepcopy(after); mutate(next(iter(changed.values())))
+ with self.subTest(mutate=mutate), self.assertRaises(ValueError):
+ successor.physical_append(before, changed, fixture.BUCKET, baseline, added)
+
+ def test_runtime_keeps_one_second_timeout_separate_from_sixty_second_put_queue(self):
+ timeout = fleet_node.runtime_config('lock-timeout')
+ queued = fleet_node.runtime_config('lock-successor')
+ self.assertNotIn('RUSTFS_PUT_COMMIT_NAMESPACE_LOCK_ACQUIRE_TIMEOUT_MS', timeout)
+ self.assertEqual(queued.pop('RUSTFS_PUT_COMMIT_NAMESPACE_LOCK_ACQUIRE_TIMEOUT_MS'), '60000')
+ self.assertEqual(queued, timeout)
+
+
+class SuccessorFlowTests(unittest.TestCase):
+ def setUp(self):
+ self.scene = fixture.ObserverFlowTests('test_complete_component_keeps_original_unknown_and_all_missing_case_proofs')
+ self.scene.setUp(); self.addCleanup(self.scene.doCleanups)
+ self.probe, self.fleet, self.entry = self.scene.probe, self.scene.fleet, self.scene.entry
+ self.fleet.runtime_profile = 'lock-successor'
+ self.entry['payload']['size'] = successor.MIN_BACKPRESSURE_BYTES
+ self.probe.credential_pair = ('test-access', 'test-secret')
+ self.change = None
+ self.done = threading.Event(); self.calls = 0; self.labels = []
+ self.final_entry = None; self.original = self.inventory(fixture.BUCKET)
+ self.trace = []
+ self.output = self.scene.root / 'successor'
+ owner = self
+ class Writer(successor.Probe):
+ def __init__(self, probe, marker):
+ self.binding = probe.binding; self.endpoint_name = probe.endpoint_name
+ self.marker = marker
+ def s3(self, operation, params, body):
+ owner.calls += 1
+ owner.scene.contender_rows = events(self.marker,
+ ('acquiring', 'acquired') if owner.change == 'early-acquire' else ('acquiring',))
+ if not owner.scene.wire.server.disconnected.wait(3):
+ raise ValueError('caller was never disconnected')
+ if owner.change == 'unknown':
+ return {'acknowledged': False, 'resultUnknown': True, 'response': None}
+ owner.scene.contender_rows = events(self.marker, ('acquiring', 'acquired', 'guard_dropped'))
+ response = observation(self, params, body)
+ owner.final_entry = {'key': params['Key'], 'versionId': VERSION,
+ 'deleteMarker': False, 'isLatest': True, 'payload': response['payload'],
+ 'properties': {'ETag': response['response']['ETag'], 'ContentLength': len(body),
+ 'ContentType': params['ContentType'], 'Metadata': params['Metadata']}, 'tags': []}
+ owner.done.set()
+ return response
+ peer = type('Peer', (), {'inventory': lambda _, bucket: owner.inventory(bucket)})
+ for name, value in [('SuccessorProbe', Writer), ('Probe', lambda *a, **k: peer()),
+ ('physical_append', self.physical)]:
+ p = patch.object(successor, name, value); p.start(); self.addCleanup(p.stop)
+ self.probe.inventory = self.inventory
+ def snapshot(label):
+ self.trace.append(label)
+ changed = self.change == 'historical-read-mutation' and label == 'I05-successor-before-all-peer-reads'
+ return {'vm000': {'files': ['unexpected-read-repair'] if changed else [], 'formats': []}}
+ self.fleet.snapshot = snapshot
+ original_remote = self.fleet.remote
+ def remote(node, action, **params):
+ self.assertNotEqual(action, 'verify-part', 'streaming checksums require a stopped service')
+ value = original_remote(node, action, **params)
+ if action in {'process-metrics', 'caller-socket'}:
+ if action == 'caller-socket':
+ value['process']['settings'] = fleet_node.runtime_config('lock-successor')
+ else:
+ value['identity']['settings'] = fleet_node.runtime_config('lock-successor')
+ return value
+ self.fleet.remote = remote
+
+ def inventory(self, bucket):
+ entries = [{**copy.deepcopy(self.entry), 'isLatest': self.final_entry is None}]
+ if self.final_entry is not None:
+ entries.append(copy.deepcopy(self.final_entry))
+ return {'bucket': bucket, 'entries': entries, 'objectsAndMarkers': len(entries),
+ 'bytesRead': sum(row['payload']['size'] for row in entries),
+ 'pagination': {'versionPageSizes': [len(entries)], 'currentPageSizes': [1]}}
+
+ def physical(self, before, after, bucket, baseline, added):
+ self.assertTrue(self.done.is_set())
+ self.trace.append('physical-append')
+ return {'versionId': added['versionId']}
+
+ def invoke(self):
+ return successor.observe_successor(self.probe, self.fleet, fixture.BUCKET, self.entry, self.output)
+
+ def test_one_queued_put_drains_after_real_caller_socket_closes_and_retains_exact_ack(self):
+ result = self.invoke()
+ self.assertEqual(self.calls, 1)
+ self.assertTrue(result['diagnosticComplete'])
+ self.assertFalse(result['canonicalCasePassClaimed'] or result['heldAtDisconnectProven'])
+ self.assertEqual(result['verifiedEndpoints'], ['node1'])
+ self.assertEqual(len(result['expectedEntries']), 2)
+ self.assertTrue((self.output / 'queued.json').is_file())
+ self.assertTrue((self.output / 'successor-complete.json').is_file())
+ self.assertLess(self.trace.index('physical-append'), self.trace.index('I05-successor-before-all-peer-reads'))
+ transport = json.loads((self.output / 'caller/transport/disconnected.json').read_text())
+ self.assertTrue(transport['resultUnknown'] and transport['clientSocketClosed'])
+
+ def test_unknown_ack_retains_original_call_and_stops_before_public_verification(self):
+ self.change = 'unknown'
+ with self.assertRaisesRegex(ValueError, 'unexpected put_object'):
+ self.invoke()
+ self.assertEqual(self.calls, 1)
+ ack = json.loads((self.output / 'successor/000001-ack.json').read_text())
+ self.assertTrue(ack['observation']['resultUnknown'])
+ self.assertNotIn('physical-append', self.trace)
+ self.assertFalse((self.output / 'diagnostic.json').exists())
+
+ def test_first_historical_verification_read_cannot_hide_physical_mutation(self):
+ self.change = 'historical-read-mutation'
+ with self.assertRaisesRegex(ValueError, 'first historical verification read changed physical content'):
+ self.invoke()
+ self.assertEqual(self.calls, 1)
+ self.assertTrue((self.output / 'successor/000001-ack.json').is_file())
+ self.assertFalse((self.output / 'diagnostic.json').exists())
+
+ def test_early_write_acquisition_is_rejected_and_original_tcp_socket_still_closes(self):
+ self.change = 'early-acquire'
+ with self.assertRaisesRegex(ValueError, 'acquired before caller disconnect'):
+ self.invoke()
+ self.assertTrue(self.scene.wire.server.disconnected.wait(3))
+ self.assertEqual(self.calls, 1)
+ self.assertTrue((self.output / 'successor/000001-ack.json').is_file())
+ self.assertFalse((self.output / 'diagnostic.json').exists())
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/test_validation_heal_lock_timeout.py b/test_validation_heal_lock_timeout.py
new file mode 100644
index 00000000..49534a00
--- /dev/null
+++ b/test_validation_heal_lock_timeout.py
@@ -0,0 +1,279 @@
+"""I05 timeout controller: real caller socket, durable PUT receipts, honest scope."""
+
+import copy
+import http.server
+import json
+import threading
+import time
+import unittest
+from unittest.mock import patch
+
+from scripts.validation_heal import lock_timeout
+import test_validation_heal_lock_observer as fixture
+
+
+CONTENDER = '30000000-0000-4000-8000-000000000001'
+
+
+class TimeoutFlowTests(unittest.TestCase):
+ def setUp(self):
+ self.scene = fixture.ObserverFlowTests(
+ 'test_complete_component_keeps_original_unknown_and_all_missing_case_proofs')
+ self.scene.setUp()
+ self.addCleanup(self.scene.doCleanups)
+ self.probe, self.fleet = self.scene.probe, self.scene.fleet
+ self.entry = self.scene.entry
+ self.entry['payload']['size'] = lock_timeout.MIN_BACKPRESSURE_BYTES
+ self.trace, self.change, self.puts = [], None, 0
+ self.output = self.scene.root / 'timeout'
+ self.fleet.snapshot = self.snapshot
+ self.probe.inventory = self.inventory
+ self.probe.s3 = self.s3
+ original_remote = self.fleet.remote
+ def remote(node, action, **params):
+ value = original_remote(node, action, **params)
+ if self.change == 'no-queue' and action == 'caller-socket' and self.puts:
+ for sample in value['samples']:
+ for match in sample['ownedMatches']: match['txQueueBytes'] = 0
+ if self.change == 'late-fd-leak' and action == 'process-metrics' and (
+ 'I05-timeout-final-physical-after-public-read' in self.trace):
+ value['fileDescriptors'] += 1
+ return value
+ self.fleet.remote = remote
+
+ def inventory(self, bucket):
+ self.trace.append('final-inventory' if self.puts else 'baseline-inventory')
+ entry = {**copy.deepcopy(self.entry), 'isLatest': True}
+ return {'bucket': bucket, 'entries': [entry], 'objectsAndMarkers': 1,
+ 'bytesRead': entry['payload']['size'],
+ 'pagination': {'versionPageSizes': [1], 'currentPageSizes': [1]}}
+
+ def snapshot(self, label):
+ self.trace.append(label)
+ changed = self.change == 'physical-write' and self.puts
+ return {node: {'formats': [], 'files': [{'path': 'original',
+ 'sha256': 'changed' if changed else 'original'}]} for node in self.fleet.nodes}
+
+ def s3(self, operation, params, body=None):
+ if operation == 'get_object':
+ self.trace.append('exact-version-get')
+ return {'acknowledged': True, 'resultUnknown': False,
+ 'response': {'VersionId': fixture.VERSION}, 'payload': self.entry['payload']}
+ self.assertEqual(operation, 'put_object')
+ self.assertEqual(params['Bucket'], fixture.BUCKET)
+ self.assertEqual(params['Key'], self.entry['key'])
+ self.puts += 1
+ self.trace.append('put')
+ self.assertFalse(self.scene.wire.server.disconnected.is_set())
+ self.scene.contender_rows = [{
+ 'target': fixture.observer.TARGET, 'request_id': CONTENDER,
+ 'event': 'object_namespace_lock', 'component': 'ecstore', 'subsystem': 'object_lock',
+ 'op': 'put_object_commit', 'mode': 'write', 'bucket': fixture.BUCKET,
+ 'object': self.entry['key'], 'namespace': 'namespace-one', 'owner': 'owner-one',
+ 'lock_attempt_id': '40000000-0000-4000-8000-000000000001',
+ 'state': state, 'level': 'TRACE' if state == 'acquiring' else 'ERROR',
+ 'timeout_ms': 1000, 'acquire_ms': 0 if state == 'acquiring' else 1001,
+ 'spans': [{'name': 'http-request', 'request_id': CONTENDER, 'method': 'PUT',
+ 'uri': f'/{fixture.BUCKET}/{self.entry["key"]}'}],
+ **({} if state == 'acquiring' else {'failure': 'timeout'}),
+ } for state in ('acquiring', 'failed')]
+ if self.change in ('bucket-read', 'extra-write-lock'):
+ self.scene.contender_rows[:0] = [{
+ 'target': fixture.observer.TARGET, 'request_id': CONTENDER,
+ 'event': 'object_namespace_lock', 'component': 'ecstore', 'subsystem': 'object_lock',
+ 'op': 'get_object', 'mode': 'read', 'bucket': '.rustfs.sys',
+ 'object': f'buckets/{fixture.BUCKET}/.bucket-incarnation',
+ 'namespace': 'namespace-one', 'owner': 'owner-one',
+ 'lock_attempt_id': '40000000-0000-4000-8000-000000000002',
+ 'state': state, 'level': 'TRACE', 'timeout_ms': 1000, 'acquire_ms': 0,
+ } for state in ('acquiring', 'acquired', 'guard_dropped')]
+ if self.change == 'extra-write-lock':
+ self.scene.contender_rows.append({**self.scene.contender_rows[-1],
+ 'op': 'delete_object', 'mode': 'write', 'object': self.entry['key'],
+ 'lock_attempt_id': '40000000-0000-4000-8000-000000000003'})
+ if self.change == 'no-journal': self.scene.contender_rows = []
+ response = {'ResponseMetadata': {'HTTPStatusCode': 503, 'RetryAttempts': 0, 'RequestId': CONTENDER,
+ 'HTTPHeaders': {'retry-after': '1'}}, 'Error': {'Code': 'ServiceUnavailable'}}
+ if self.change == 'unexpected-success':
+ response = {'ResponseMetadata': {'HTTPStatusCode': 200, 'RetryAttempts': 0},
+ 'VersionId': '50000000-0000-4000-8000-000000000001'}
+ return {'endpoint': self.probe.endpoint_name, 'operation': operation, 'params': params,
+ 'payload': {'size': len(body), 'sha256': lock_timeout.hashlib.sha256(body).hexdigest()},
+ 'clockDomain': lock_timeout.CLOCK_DOMAIN,
+ 'clientCallInvoked': True, 'acknowledged': self.change == 'unexpected-success',
+ 'resultUnknown': self.change == 'unknown', 'response': response,
+ 'callStartedMonotonicNs': time.monotonic_ns(), 'callFinishedMonotonicNs': time.monotonic_ns()}
+
+ def invoke(self):
+ return lock_timeout.observe_timeout(self.probe, self.fleet, fixture.BUCKET, self.entry, self.output)
+
+ def test_complete_timeout_keeps_real_caller_unknown_and_physical_check_precedes_public_get(self):
+ proof = self.invoke()
+ self.assertTrue(proof['diagnosticComplete'])
+ self.assertFalse(proof['wholeCasePassClaimed'] or proof['canonicalCasePassClaimed'])
+ timeout = proof['callerObservation']['admittedWork']
+ self.assertTrue(timeout['typedTimeoutBeyondBudgetObserved'])
+ self.assertTrue(timeout['readResponseBackpressureObservedAfterTimeout'])
+ self.assertEqual(self.puts, 1)
+ self.assertLess(self.trace.index('I05-timeout-original-physical'),
+ self.trace.index('baseline-inventory'))
+ self.assertLess(self.trace.index('I05-timeout-final-physical-before-public-read'),
+ self.trace.index('exact-version-get'))
+ transport = json.loads((self.output / 'caller/transport/disconnected.json').read_text())
+ self.assertTrue(transport['clientSocketClosed'] and transport['resultUnknown'])
+ self.assertEqual(transport['receiveBufferBytesRequested'], lock_timeout.CALLER_RECEIVE_BUFFER_BYTES)
+ self.assertTrue((self.output / 'contender/000001-request.json').is_file())
+ self.assertTrue((self.output / 'contender/000001-ack.json').is_file())
+
+ def test_bucket_incarnation_read_lock_does_not_mask_target_write_timeout(self):
+ self.change = 'bucket-read'
+ proof = self.invoke()
+ self.assertTrue(proof['diagnosticComplete'])
+ self.assertTrue(proof['callerObservation']['admittedWork']['typedTimeoutBeyondBudgetObserved'])
+
+ def test_actual_probe_closes_known_503_keepalive_socket_without_retry(self):
+ from scripts.validation_heal.heal_probe import Probe
+ closed, received = threading.Event(), []
+ class Handler(http.server.BaseHTTPRequestHandler):
+ protocol_version = 'HTTP/1.1'
+ def log_message(self, *args): pass
+ def do_PUT(self):
+ received.append(self.rfile.read(int(self.headers['Content-Length'])))
+ body = b'ServiceUnavailableone-refusal'
+ self.send_response(503)
+ self.send_header('Content-Length', str(len(body)))
+ self.send_header('Content-Type', 'application/xml')
+ self.send_header('x-amz-request-id', 'one-refusal')
+ self.send_header('Retry-After', '1')
+ self.end_headers()
+ self.wfile.write(body); self.wfile.flush()
+ self.connection.settimeout(3)
+ try:
+ if self.rfile.read(1) == b'': closed.set()
+ except TimeoutError: pass
+ self.close_connection = True
+ server = http.server.ThreadingHTTPServer(('127.0.0.1', 0), Handler)
+ thread = threading.Thread(target=server.serve_forever, daemon=True)
+ thread.start()
+ try:
+ binding = copy.deepcopy(self.probe.binding)
+ binding['endpoints'] = {'node1': f'http://127.0.0.1:{server.server_port}'}
+ probe = Probe(binding, credential_pair=('test-access', 'test-secret'))
+ result = probe.s3('put_object', {'Bucket': fixture.BUCKET, 'Key': self.entry['key'],
+ 'Metadata': {'heal-fixture': 'I05-timeout-contender'}}, b'one-refused-body')
+ self.assertFalse(result['acknowledged'] or result['resultUnknown'])
+ self.assertEqual(result['response']['ResponseMetadata']['HTTPStatusCode'], 503)
+ self.assertEqual(result['response']['ResponseMetadata']['RetryAttempts'], 0)
+ self.assertNotIn('clientCleanupError', result)
+ self.assertTrue(closed.wait(4), 'actual SDK close must reach the server socket')
+ self.assertEqual(received, [b'one-refused-body'])
+ finally:
+ server.shutdown(); server.server_close(); thread.join()
+
+ def test_an_additional_write_lock_attempt_is_rejected(self):
+ self.change = 'extra-write-lock'
+ with self.assertRaisesRegex(ValueError, 'another namespace write-lock attempt'):
+ self.invoke()
+ self.assertTrue(self.scene.wire.server.disconnected.wait(3))
+ self.assertFalse((self.output / 'diagnostic.json').exists())
+
+ def test_incomplete_bucket_incarnation_read_lock_lifecycle_is_rejected(self):
+ self.change = 'bucket-read'
+ original = self.probe.s3
+ def s3(operation, params, body=None):
+ value = original(operation, params, body)
+ if operation == 'put_object':
+ self.scene.contender_rows = [row for row in self.scene.contender_rows
+ if not (row.get('mode') == 'read' and row.get('state') == 'guard_dropped')]
+ return value
+ self.probe.s3 = s3
+ with self.assertRaisesRegex(ValueError, 'bucket-incarnation read lock has an incomplete lifecycle'):
+ self.invoke()
+ self.assertTrue(self.scene.wire.server.disconnected.wait(3))
+ self.assertFalse((self.output / 'diagnostic.json').exists())
+
+ def test_small_target_is_not_observed_without_sending_contender_or_get(self):
+ self.entry['payload']['size'] = 1
+ with self.assertRaisesRegex(ValueError, 'too small'): self.invoke()
+ self.assertEqual(self.puts, 0)
+ self.assertEqual(self.scene.wire.server.requests, [])
+ self.assertEqual(json.loads((self.output / 'failure.json').read_text())['state'], 'NOT_OBSERVED')
+
+ def test_h2_preserves_all_four_peer_resource_checks(self):
+ self.fleet.nodes = ['vm000', 'vm001', 'vm002', 'vm003']
+ self.fleet.run_id = self.probe.binding['runId'] = 'gh-12345-1-h2'
+ # JSON object order does not define the endpoint-to-host binding.
+ self.probe.binding['endpoints'] = {f'node{index}': f'http://rustfs-node{index}:9000'
+ for index in range(4, 0, -1)}
+ proof = self.invoke()
+ self.assertEqual(set(proof['finalResources']), set(self.fleet.nodes))
+ self.assertEqual(set(proof['callerObservation']['baselineResources']), set(self.fleet.nodes))
+ self.assertFalse(proof['wholeCasePassClaimed'])
+
+ def test_unexpected_put_success_retains_ack_and_closes_caller_without_public_read(self):
+ self.change = 'unexpected-success'
+ with self.assertRaisesRegex(ValueError, 'unexpected put_object result'): self.invoke()
+ ack = json.loads((self.output / 'contender/000001-ack.json').read_text())
+ self.assertEqual(ack['operationCheck'], 'FAIL')
+ self.assertTrue(ack['observation']['acknowledged'])
+ self.assertTrue(self.scene.wire.server.disconnected.wait(3))
+ self.assertEqual(self.puts, 1)
+ self.assertNotIn('exact-version-get', self.trace)
+ self.assertFalse((self.output / 'diagnostic.json').exists())
+
+ def test_unknown_put_retains_receipt_closes_caller_and_does_not_retry(self):
+ self.change = 'unknown'
+ with self.assertRaisesRegex(ValueError, 'unknown transport'): self.invoke()
+ ack = json.loads((self.output / 'contender/000001-ack.json').read_text())
+ self.assertTrue(ack['observation']['resultUnknown'])
+ self.assertTrue(self.scene.wire.server.disconnected.wait(3))
+ self.assertEqual(self.puts, 1)
+ self.assertFalse((self.output / 'diagnostic.json').exists())
+
+ def test_holder_dropped_before_timeout_observation_never_proves_contention(self):
+ self.scene.change = 'timeout-drop'
+ with self.assertRaisesRegex(ValueError, 'owner ended'): self.invoke()
+ self.assertTrue(self.scene.wire.server.disconnected.wait(3))
+ self.assertFalse((self.output / 'timeout.json').exists())
+
+ def test_empty_post_timeout_send_queue_cannot_prove_backpressure(self):
+ self.change = 'no-queue'
+ with self.assertRaisesRegex(ValueError, 'backpressured original GET socket'): self.invoke()
+ self.assertTrue(self.scene.wire.server.disconnected.wait(3))
+ self.assertFalse((self.output / 'timeout.json').exists())
+
+ def test_physical_change_stops_before_any_verification_get(self):
+ self.change = 'physical-write'
+ with self.assertRaisesRegex(ValueError, 'changed an original physical file'): self.invoke()
+ self.assertNotIn('exact-version-get', self.trace)
+ self.assertNotIn('final-inventory', self.trace)
+ self.assertFalse((self.output / 'diagnostic.json').exists())
+
+ def test_missing_timeout_events_exhaust_budget_without_retry(self):
+ self.change = 'no-journal'
+ with patch.object(lock_timeout, 'OBSERVATION_SECONDS', 0), \
+ self.assertRaisesRegex(ValueError, 'observation exceeded its budget'):
+ self.invoke()
+ self.assertEqual(self.puts, 1)
+ self.assertTrue(self.scene.wire.server.disconnected.wait(3))
+
+ def test_inventory_cannot_leave_unclosed_fds_after_caller_drain(self):
+ self.change = 'late-fd-leak'
+ clock = [0]
+ def advance(seconds): clock[0] += fixture.observer.DRAIN_SECONDS + 1
+ with patch.object(lock_timeout.time, 'monotonic', side_effect=lambda: clock[0]), \
+ patch.object(lock_timeout.time, 'sleep', side_effect=advance), \
+ self.assertRaisesRegex(ValueError, 'final timeout FD census exceeded its budget'):
+ self.invoke()
+ self.assertIn('final-inventory', self.trace)
+ self.assertFalse((self.output / 'diagnostic.json').exists())
+
+ def test_unreviewed_source_cannot_even_inventory(self):
+ self.probe.binding['candidateCommit'] = 'a' * 40
+ with self.assertRaisesRegex(ValueError, 'reviewed candidate'): self.invoke()
+ self.assertEqual(self.trace, [])
+ self.assertFalse(self.output.exists())
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/test_validation_heal_lock_timeout_runner.py b/test_validation_heal_lock_timeout_runner.py
new file mode 100644
index 00000000..7a37acde
--- /dev/null
+++ b/test_validation_heal_lock_timeout_runner.py
@@ -0,0 +1,479 @@
+"""I05 deployment boundary and retained failure scenes through real Recorder."""
+
+import copy
+import hashlib
+import json
+import os
+from pathlib import Path
+import signal
+import subprocess
+import tempfile
+from types import SimpleNamespace
+import unittest
+from unittest.mock import patch
+import uuid
+
+from scripts.validation_heal import lock_timeout_runner as runner
+from test_validation_heal_lock_observer import identity
+
+
+class TimeoutRunnerTests(unittest.TestCase):
+ def setUp(self):
+ self.tmp = tempfile.TemporaryDirectory()
+ self.addCleanup(self.tmp.cleanup)
+ self.root = Path(self.tmp.name)
+ self.rc = self.root / 'rc'; self.rc.write_bytes(b'pinned-client')
+ self.candidate = {'source_sha': runner.CANDIDATE, 'package_sha256': 'a' * 64}
+ self.inspector = {'candidateCommit': runner.CANDIDATE, 'binarySha256': 'b' * 64}
+ self.candidate_file = self.root / 'candidate.json'
+ self.candidate_file.write_text(json.dumps(self.candidate))
+ self.args = SimpleNamespace(candidate=self.candidate_file, testing_commit='c' * 40,
+ binary_sha='d' * 64, tool_directory=self.root / 'tools', rc_bin=self.rc,
+ evidence=self.root / 'evidence', topologies=['H1', 'H2'])
+ self.events, self.fleets, self.entries = [], [], []
+ self.change = None
+ owner = self
+ class Probe:
+ def __init__(self, binding, endpoint=None, *, credential_pair=None):
+ self.binding = binding; self.endpoint_name = endpoint or binding['defaultEndpoint']
+ self.credential_pair = credential_pair
+ def s3(self, operation, params, body=None):
+ owner.events.append(operation)
+ response = {'ResponseMetadata': {'HTTPStatusCode': 200}}
+ if operation == 'create_bucket': owner.entries = []
+ if operation == 'put_object':
+ version = str(uuid.uuid4()); response['VersionId'] = version
+ for row in owner.entries:
+ if row['key'] == params['Key']: row['isLatest'] = False
+ owner.entries.append({'key': params['Key'], 'versionId': version,
+ 'isLatest': True, 'deleteMarker': False,
+ 'payload': {'size': len(body), 'sha256': hashlib.sha256(body).hexdigest()}})
+ if operation == 'delete_object':
+ version = str(uuid.uuid4()); response.update(VersionId=version, DeleteMarker=True)
+ for row in owner.entries:
+ if row['key'] == params['Key']: row['isLatest'] = False
+ owner.entries.append({'key': params['Key'], 'versionId': version,
+ 'isLatest': True, 'deleteMarker': True})
+ return {'acknowledged': True, 'resultUnknown': False, 'response': response}
+ def inventory(self, bucket):
+ owner.events.append('inventory-' + self.endpoint_name)
+ if owner.change == 'peer-error' and 'diagnostic' in owner.events:
+ raise ValueError('incomplete peer inventory')
+ return {'bucket': bucket, 'entries': copy.deepcopy(owner.entries)}
+ class Fleet:
+ def __init__(self, nodes, evidence, run_id, *args, **kwargs):
+ self.nodes = nodes; self.evidence = evidence; self.run_id = run_id
+ self.runtime_profile = kwargs.get('runtime_profile')
+ self.secret_values = set(); self.reserved = []; self.sequence = 0
+ self.stopped = False
+ owner.fleets.append(self)
+ def record(self, label, value):
+ self.sequence += 1
+ runner.save(self.evidence / f'{self.sequence:05d}-{label}.json', value)
+ return value
+ def deploy(self, *args):
+ owner.events.append('deploy'); self.reserved.extend(self.nodes)
+ if owner.change == 'reserve-error': raise ValueError('partial reservation')
+ def snapshot(self, label, **kwargs):
+ owner.events.append(label)
+ if kwargs.get('quiescent'):
+ owner.assertTrue(self.stopped)
+ changed = ((owner.change == 'stopped-drift' and label == 'I05-successor-stopped-before-integrity')
+ or (owner.change == 'integrity-drift' and label == 'I05-successor-stopped-after-integrity'))
+ return {node: {'files': ['changed'] if changed else [], 'formats': []} for node in self.nodes}
+ def remote(self, node, action):
+ owner.events.append(action + '-' + node)
+ if action == 'process-metrics':
+ owner.assertFalse(self.stopped)
+ return {'identity': identity(), 'fileDescriptors': 11 if owner.change == 'fd-leak' else 10}
+ return {'journal': 'original log'}
+ def stop(self):
+ owner.events.append('stop'); self.stopped = True
+ def cleanup(self): owner.events.append('cleanup'); return owner.change != 'cleanup-fail'
+ def diagnostic(probe, fleet, bucket, entry, output):
+ owner.events.append('diagnostic')
+ self.assertFalse(entry['isLatest'] or entry['deleteMarker'])
+ self.assertEqual(entry['payload']['size'], 2048)
+ if owner.change == 'diagnostic-error': raise ValueError('original timeout not observed')
+ if owner.change == 'secret-error': raise ValueError(str(probe.credential_pair))
+ if owner.change == 'sigterm': signal.raise_signal(signal.SIGTERM)
+ return {'diagnosticComplete': True, 'wholeCasePassClaimed': False,
+ 'canonicalCasePassClaimed': False, 'callerObservation': {'baselineResources': {
+ node: {'identity': identity(), 'fileDescriptors': 10} for node in fleet.nodes}}}
+ for name, value in [('Fleet', Fleet), ('Probe', Probe), ('observe_timeout', diagnostic),
+ ('MIN_BACKPRESSURE_BYTES', 2048), ('validate_topology', lambda *a: {'topology': a[1]})]:
+ p = patch.object(runner, name, value); p.start(); self.addCleanup(p.stop)
+ p = patch.dict(os.environ, {'GITHUB_RUN_ID': '12345', 'GITHUB_RUN_ATTEMPT': '1',
+ 'RUSTFS_NODES': ' '.join(runner.NODES)})
+ p.start(); self.addCleanup(p.stop)
+
+ def invoke(self, topology='H1'):
+ return runner.run_topology(self.args, self.candidate, self.inspector, topology)
+
+ def test_real_recorder_seeds_history_then_checks_every_peer_before_cleanup(self):
+ result = self.invoke('H2')
+ self.assertTrue(result['diagnosticComplete'])
+ self.assertEqual(result['cleanup'], 'PASS')
+ self.assertFalse(result['canonicalCasePassClaimed'] or result['wholeCasePassClaimed'])
+ self.assertEqual(self.fleets[0].nodes, list(runner.NODES))
+ requests = sorted((self.args.evidence / 'H2/fixture').glob('*-request.json'))
+ acks = sorted((self.args.evidence / 'H2/fixture').glob('*-ack.json'))
+ self.assertEqual(len(requests), 9)
+ self.assertEqual(len(acks), 9)
+ for request, ack in zip(requests, acks):
+ self.assertEqual(json.loads(ack.read_text())['requestSha256'], hashlib.sha256(request.read_bytes()).hexdigest())
+ self.assertEqual(sum(not e['deleteMarker'] for e in self.entries), 6)
+ self.assertEqual(sum(e['deleteMarker'] for e in self.entries), 1)
+ for endpoint in ('node1', 'node2', 'node3', 'node4'):
+ self.assertIn('inventory-' + endpoint, self.events)
+ self.assertLess(self.events.index('all-peer-verification-before-public-reads'),
+ self.events.index('inventory-node2'))
+ self.assertLess(self.events.index('physical-after-all-peer-reads'), self.events.index('cleanup'))
+
+ def test_unreviewed_source_or_mismatched_tool_refuses_before_fleet_creation(self):
+ self.inspector['candidateCommit'] = 'e' * 40
+ with self.assertRaises(ValueError): self.invoke()
+ self.assertFalse(self.fleets or self.events)
+
+ def test_failure_keeps_original_journal_stopped_scene_and_skips_cleanup(self):
+ for change in ('diagnostic-error', 'reserve-error', 'peer-error'):
+ with self.subTest(change=change):
+ self.args.evidence = self.root / change; self.change = change
+ self.events.clear()
+ result = self.invoke()
+ self.assertFalse(result['diagnosticComplete'])
+ self.assertEqual(result['cleanup'], 'FAIL')
+ self.assertTrue(result['namespacesRetained'])
+ self.assertNotIn('cleanup', self.events)
+ self.assertLess(self.events.index('stop'), self.events.index('original-failure-scene'))
+ self.assertIn('logs-vm000', self.events)
+ if change != 'reserve-error':
+ self.assertEqual(len(list((self.args.evidence / 'H1/fixture').glob('*-ack.json'))), 9)
+
+ def test_post_peer_fd_leak_retains_scene_and_never_cleans(self):
+ self.change = 'fd-leak'
+ clock = [0]
+ def advance(_): clock[0] += runner.DRAIN_SECONDS + 1
+ with patch.object(runner.time, 'monotonic', side_effect=lambda: clock[0]), \
+ patch.object(runner.time, 'sleep', side_effect=advance):
+ result = self.invoke()
+ self.assertFalse(result['diagnosticComplete'])
+ self.assertIn('exceeded its budget', result['failure']['message'])
+ self.assertNotIn('cleanup', self.events)
+
+ def test_serial_driver_stops_before_h2_after_dirty_h1_and_retains_summary(self):
+ self.change = 'diagnostic-error'
+ with patch.object(runner, 'preflight', return_value=(self.candidate, self.inspector)):
+ summary = runner.run(self.args)
+ self.assertFalse(summary['diagnosticComplete'] or summary['canonicalCasePassClaimed'])
+ self.assertEqual([row['topology'] for row in summary['diagnostics']], ['H1'])
+ self.assertFalse((self.args.evidence / 'H2').exists())
+ self.assertTrue((self.args.evidence / 'timeout-diagnostic-summary.json').is_file())
+
+ def test_cleanup_failure_blocks_next_topology_even_after_complete_diagnostic(self):
+ self.change = 'cleanup-fail'
+ with patch.object(runner, 'preflight', return_value=(self.candidate, self.inspector)):
+ result = runner.run(self.args)
+ self.assertFalse(result['diagnosticComplete'])
+ self.assertEqual(len(result['diagnostics']), 1)
+ self.assertEqual(result['diagnostics'][0]['cleanup'], 'FAIL')
+
+ def test_preflight_checks_exact_source_helper_tools_and_workflow_before_remote_use(self):
+ with patch.object(runner.subprocess, 'run', side_effect=[SimpleNamespace(stdout='c' * 40 + '\n'),
+ SimpleNamespace(stdout='')]), \
+ patch.object(runner, 'verify_inspector', return_value=self.inspector):
+ self.assertEqual(runner.preflight(self.args), (self.candidate, self.inspector))
+ self.candidate_file.write_text(json.dumps({**self.candidate, 'source_sha': 'e' * 40}))
+ with self.assertRaisesRegex(ValueError, 'exact reviewed'): runner.preflight(self.args)
+ self.assertFalse(self.fleets or self.events)
+
+ def test_source_plan_runs_four_isolated_instances_through_real_recorder(self):
+ self.args.diagnostic_branch = 'source-plan'
+ proof = {'nativeOwnerAndStaleFencesPassed': True}
+ def link(directory, branch, binding, diagnostic):
+ self.assertEqual(directory, self.fleets[-1].evidence)
+ self.assertEqual(binding['runId'], self.fleets[-1].run_id)
+ self.assertTrue(list((directory / 'fixture').glob('*-ack.json')))
+ self.events.append('source-chain')
+ return {'sourceEvidenceComplete': True}
+ with patch.object(runner, 'preflight', return_value=(self.candidate, self.inspector)), \
+ patch.object(runner.lock_source, 'verify_source_native', return_value=proof), \
+ patch.object(runner, 'verify_native_prerequisite', return_value=proof), \
+ patch.object(runner, 'observe_successor', side_effect=self.successor), \
+ patch.object(runner, 'map_target', side_effect=self.new_target), \
+ patch.object(runner, 'verify_cohort', side_effect=self.integrity), \
+ patch.object(runner.lock_source, 'link_fleet_evidence', side_effect=link) as chain:
+ result = runner.run(self.args)
+ self.assertTrue(result['sourceProceduresComplete'])
+ self.assertFalse(result['canonicalCasePassClaimed'] or result['wholeCasePassClaimed'])
+ self.assertEqual(chain.call_count, 4)
+ self.assertEqual([fleet.runtime_profile for fleet in self.fleets],
+ ['lock-timeout'] * 2 + ['lock-successor'] * 2)
+ self.assertEqual(len({fleet.run_id for fleet in self.fleets}), 4)
+ self.assertEqual({fleet.evidence.relative_to(self.args.evidence).as_posix() for fleet in self.fleets},
+ {'source-plan/timeout/H1', 'source-plan/timeout/H2',
+ 'source-plan/queued-successor/H1', 'source-plan/queued-successor/H2'})
+ bindings = [json.loads(next(fleet.evidence.glob('*-binding.json')).read_text()) for fleet in self.fleets]
+ self.assertEqual(len({binding['bucketPrefix'] for binding in bindings}), 4)
+ self.assertEqual([event for event in self.events if event in ('source-chain', 'cleanup')],
+ ['source-chain', 'cleanup'] * 4)
+
+ def test_source_chain_failure_preserves_original_scene_before_cleanup(self):
+ self.args.diagnostic_branch = 'source-plan'
+ with patch.object(runner, 'preflight', return_value=(self.candidate, self.inspector)), \
+ patch.object(runner.lock_source, 'verify_source_native', return_value={}), \
+ patch.object(runner.lock_source, 'link_fleet_evidence', side_effect=ValueError('broken original ACK')):
+ result = runner.run(self.args)
+ self.assertFalse(result['sourceProceduresComplete'])
+ self.assertEqual(len(self.fleets), 1)
+ self.assertIn('broken original ACK', result['diagnostics'][0]['failure']['message'])
+ self.assertTrue(result['diagnostics'][0]['namespacesRetained'])
+ self.assertNotIn('cleanup', self.events)
+ self.assertIn('original-failure-scene', self.events)
+ self.assertTrue(list(self.fleets[0].evidence.glob('*-original-failure-logs.json')))
+
+ def test_source_plan_requires_native_before_deployment_and_evidence_creation(self):
+ self.args.diagnostic_branch = 'source-plan'
+ with patch.object(runner, 'preflight', return_value=(self.candidate, self.inspector)):
+ with self.assertRaisesRegex(ValueError, 'requires exact-source native evidence'):
+ runner.run(self.args)
+ self.assertFalse(self.fleets or self.events or self.args.evidence.exists())
+
+ def test_cli_defaults_to_complete_source_plan(self):
+ argv = self.cli()
+ start = argv.index('--diagnostic-branch')
+ del argv[start:start + 2]
+ with patch.dict(os.environ, {}, clear=True), patch.object(runner.sys, 'argv', argv), \
+ patch.object(runner, 'run', return_value={'diagnosticComplete': True}) as run:
+ self.assertEqual(runner.main(), 0)
+ self.assertEqual(run.call_args.args[0].diagnostic_branch, 'source-plan')
+
+ def test_queued_successor_preflight_requires_exact_candidate_native_group(self):
+ self.args.diagnostic_branch = 'queued-successor'
+ self.args.native_directory = self.root / 'native'
+ proof = {'requiredRegressionLabels': list(runner.NATIVE_I05_LABELS),
+ 'requiredNativeSubsetPassed': True, 'wholeCasePassClaimed': False,
+ 'fleetExecutionClaimed': False, 'canonicalCasePassClaimed': False}
+ with patch.object(runner.subprocess, 'run', side_effect=[SimpleNamespace(stdout='c' * 40 + '\n'),
+ SimpleNamespace(stdout='')]), patch.object(runner, 'verify_inspector', return_value=self.inspector), \
+ patch.object(runner, 'verify_native', return_value=proof) as verify:
+ self.assertEqual(runner.preflight(self.args), (self.candidate, self.inspector))
+ verify.assert_called_once_with(self.args.native_directory, self.candidate,
+ self.args.testing_commit, runner.CASE_ID)
+ self.assertFalse(self.fleets or self.events)
+
+ def test_queued_successor_without_native_artifact_refuses_before_fleet_or_evidence(self):
+ self.args.diagnostic_branch = 'queued-successor'
+ with patch.object(runner, 'preflight', return_value=(self.candidate, self.inspector)):
+ with self.assertRaisesRegex(ValueError, 'immutable exact-source native evidence'):
+ runner.run(self.args)
+ self.assertFalse(self.fleets or self.events)
+ self.assertFalse(self.args.evidence.exists())
+
+ def test_queued_successor_driver_binds_runtime_new_ack_and_every_peer(self):
+ self.args.diagnostic_branch = 'queued-successor'
+ with patch.object(runner, 'preflight', return_value=(self.candidate, self.inspector)), \
+ patch.object(runner, 'verify_native_prerequisite', return_value={'native': True}) as native, \
+ patch.object(runner, 'observe_successor', side_effect=self.successor), \
+ patch.object(runner, 'map_target', side_effect=self.new_target), \
+ patch.object(runner, 'verify_cohort', side_effect=self.integrity):
+ summary = runner.run(self.args)
+ self.assertTrue(summary['diagnosticComplete'])
+ self.assertFalse(summary['canonicalCasePassClaimed'] or summary['wholeCasePassClaimed'])
+ self.assertEqual(native.call_count, 3)
+ self.assertEqual([f.runtime_profile for f in self.fleets], ['lock-successor'] * 2)
+ self.assertEqual(self.events.count('successor'), 2)
+ self.assertNotIn('diagnostic', self.events)
+ proof = json.loads((self.args.evidence / 'native-prerequisite.json').read_text())
+ self.assertEqual(proof, {'native': True})
+ for fleet in self.fleets:
+ peers = list(fleet.evidence.glob('*-all-peer-ACK-proof.json'))
+ self.assertEqual(len(peers), len(fleet.nodes))
+ self.assertTrue(all(json.loads(p.read_text())['seedAckAndFullGetAgree'] for p in peers))
+ integrity = list(fleet.evidence.glob('*-I05-successor-new-shard-integrity.json'))
+ self.assertEqual(len(integrity), 1)
+ self.assertTrue(json.loads(integrity[0].read_text())['allShardBitrotVerified'])
+ for start, end in ((0, self.events.index('cleanup') + 1),
+ (self.events.index('cleanup') + 1, len(self.events))):
+ events = self.events[start:end]
+ self.assertLess(events.index('physical-after-all-peer-reads'), events.index('stop'))
+ self.assertLess(events.index('process-metrics-vm000'), events.index('stop'))
+ self.assertLess(events.index('stop'), events.index('I05-successor-stopped-before-integrity'))
+ self.assertLess(events.index('I05-successor-stopped-before-integrity'), events.index('verify-integrity'))
+ self.assertLess(events.index('verify-integrity'), events.index('I05-successor-stopped-after-integrity'))
+ self.assertLess(events.index('I05-successor-stopped-after-integrity'), events.index('cleanup'))
+
+ def successor(self, probe, fleet, bucket, entry, output):
+ self.events.append('successor')
+ probe.s3('put_object', {'Bucket': bucket, 'Key': entry['key']}, b'new-generation')
+ return {'diagnosticComplete': True, 'canonicalCasePassClaimed': False,
+ 'wholeCasePassClaimed': False, 'expectedEntries': copy.deepcopy(self.entries),
+ 'verifiedEndpoints': list(probe.binding['endpoints']),
+ 'callerObservation': {'baselineResources': {node: {
+ 'identity': identity(), 'fileDescriptors': 10} for node in fleet.nodes}}}
+
+ def new_target(self, snapshot, bucket, key, role, part, version):
+ self.assertEqual(set(snapshot), set(self.fleets[-1].nodes))
+ self.assertTrue(self.fleets[-1].stopped)
+ added = next(item for item in self.entries if item['versionId'] == version)
+ self.assertEqual((added['key'], role, part), (key, 'data', 1))
+ self.assertTrue(added['isLatest'])
+ self.assertFalse(added['deleteMarker'])
+ return {'versionId': version}
+
+ def integrity(self, fleet, target, label):
+ self.assertTrue(fleet.stopped)
+ self.assertEqual(label, 'I05-successor-shard')
+ self.events.append('verify-integrity')
+ if self.change == 'integrity-error':
+ raise ValueError('corrupt acknowledged successor shard')
+ return {'allShardBitrotVerified': True}
+
+ def test_successor_stopped_drift_or_checksum_failure_retains_scene_before_cleanup(self):
+ self.args.diagnostic_branch = 'queued-successor'
+ for change, message in (('stopped-drift', 'stopping the service changed'),
+ ('integrity-error', 'corrupt acknowledged successor shard'),
+ ('integrity-drift', 'streaming verification changed')):
+ with self.subTest(change=change):
+ self.change = change; self.events.clear()
+ self.args.evidence = self.root / change
+ with patch.object(runner, 'verify_native_prerequisite'), \
+ patch.object(runner, 'observe_successor', side_effect=self.successor), \
+ patch.object(runner, 'map_target', side_effect=self.new_target), \
+ patch.object(runner, 'verify_cohort', side_effect=self.integrity), \
+ patch.object(runner, 'seal_successor_scene', return_value={'sealed': True}) as seal:
+ result = self.invoke()
+ self.assertFalse(result['diagnosticComplete'])
+ self.assertEqual(result['cleanup'], 'FAIL')
+ self.assertIn(message, result['failure']['message'])
+ self.assertTrue(result['namespacesRetained'])
+ self.assertNotIn('cleanup', self.events)
+ self.assertIn('original-failure-scene', self.events)
+ seal.assert_called_once()
+
+ def test_successor_unknown_outcome_seals_only_after_stop_scene_logs_and_result(self):
+ self.args.diagnostic_branch = 'queued-successor'
+ def seal(folder, binding, scene):
+ self.assertIn('stop', self.events)
+ self.assertIn('original-failure-scene', self.events)
+ self.assertIn('logs-vm000', self.events)
+ result = list(folder.glob('*-diagnostic-result.json'))
+ self.assertEqual(len(result), 1)
+ self.assertTrue(json.loads(result[0].read_text())['namespacesRetained'])
+ return {'sealed': True}
+ with patch.object(runner, 'verify_native_prerequisite'), \
+ patch.object(runner, 'observe_successor', side_effect=ValueError('unknown PUT result')), \
+ patch.object(runner, 'seal_successor_scene', side_effect=seal) as sealed:
+ result = self.invoke()
+ self.assertFalse(result['diagnosticComplete'])
+ self.assertEqual(result['cleanup'], 'FAIL')
+ self.assertNotIn('cleanup', self.events)
+ sealed.assert_called_once()
+ self.assertEqual(json.loads((self.args.evidence / 'H1/successor-retention.json').read_text()),
+ {'sealed': True})
+
+ def test_successor_failed_stop_never_seals_incomplete_scene(self):
+ self.args.diagnostic_branch = 'queued-successor'
+ with patch.object(runner, 'verify_native_prerequisite'), \
+ patch.object(runner, 'observe_successor', side_effect=ValueError('unknown PUT result')), \
+ patch.object(runner.Fleet, 'stop', side_effect=ValueError('stop failed')), \
+ patch.object(runner, 'seal_successor_scene') as sealed:
+ result = self.invoke()
+ self.assertIn('failureCaptureError', result)
+ sealed.assert_not_called()
+ self.assertNotIn('cleanup', self.events)
+
+ def test_uncommitted_or_untracked_controller_refuses_before_inspector_and_fleet(self):
+ with patch.object(runner.subprocess, 'run', side_effect=[SimpleNamespace(stdout='c' * 40 + '\n'),
+ SimpleNamespace(stdout=' M scripts/validation_heal/lock_timeout.py')]), \
+ patch.object(runner, 'verify_inspector', side_effect=AssertionError('no inspector read')):
+ with self.assertRaisesRegex(ValueError, 'immutable helper'): runner.preflight(self.args)
+ self.assertFalse(self.fleets or self.events)
+
+ def test_in_memory_credentials_cannot_escape_into_failure_or_serial_summary(self):
+ self.change = 'secret-error'
+ with patch.object(runner, 'preflight', return_value=(self.candidate, self.inspector)):
+ result = runner.run(self.args)
+ self.assertIn('[REDACTED]', result['diagnostics'][0]['failure']['message'])
+ for value in self.fleets[0].secret_values:
+ for evidence in self.args.evidence.rglob('*.json'):
+ self.assertNotIn(value, evidence.read_text())
+
+ def cli(self):
+ return ['lock_timeout_runner.py', '--candidate', str(self.candidate_file), '--case-id', runner.CASE_ID,
+ '--testing-commit', self.args.testing_commit, '--binary-sha', self.args.binary_sha,
+ '--tool-directory', str(self.args.tool_directory), '--rc-bin', str(self.rc),
+ '--evidence', str(self.args.evidence), '--diagnostic-branch', 'timeout']
+
+ def test_actual_cli_accepts_wrapper_directory_and_keeps_canonical_summary_blocked(self):
+ self.args.evidence.mkdir()
+ for name in ('timeout-preflight.json', 'setup.log', 'test.log'):
+ (self.args.evidence / name).write_text('wrapper evidence')
+ with patch.object(runner.sys, 'argv', self.cli()), patch.object(runner.signal, 'signal'), \
+ patch.object(runner, 'preflight', return_value=(self.candidate, self.inspector)):
+ self.assertEqual(runner.main(), 0)
+ summary = json.loads((self.args.evidence / 'healthy-summary.json').read_text())
+ self.assertTrue(summary['diagnosticComplete'])
+ self.assertFalse(summary['complete'] or summary['canonicalCasePassClaimed'] or summary['wholeCasePassClaimed'])
+ self.assertEqual(summary['cleanup'], 'PASS')
+ self.assertEqual([row['topology'] for row in summary['diagnostics']], ['H1', 'H2'])
+ self.assertEqual((self.args.evidence / 'setup.log').read_text(), 'wrapper evidence')
+
+ def test_old_topology_or_summary_refuses_before_any_fleet_deployment(self):
+ self.args.evidence.mkdir()
+ for name in ('H1', 'H2', 'healthy-summary.json', 'timeout-diagnostic-summary.json'):
+ with self.subTest(name=name):
+ path = self.args.evidence / name; path.write_text('original evidence')
+ with patch.object(runner, 'preflight', return_value=(self.candidate, self.inspector)):
+ with self.assertRaisesRegex(ValueError, 'fresh topology'): runner.run(self.args)
+ self.assertFalse(self.events or self.fleets)
+ self.assertEqual(path.read_text(), 'original evidence')
+ path.unlink()
+
+ def test_actual_sigterm_retains_h1_and_stops_before_h2(self):
+ self.change = 'sigterm'
+ self.addCleanup(signal.signal, signal.SIGTERM, signal.getsignal(signal.SIGTERM))
+ with patch.object(runner.sys, 'argv', self.cli()), \
+ patch.object(runner, 'preflight', return_value=(self.candidate, self.inspector)):
+ self.assertEqual(runner.main(), 1)
+ summary = json.loads((self.args.evidence / 'healthy-summary.json').read_text())
+ self.assertEqual(len(summary['diagnostics']), 1)
+ self.assertEqual(summary['diagnostics'][0]['failure']['type'], 'KeyboardInterrupt')
+ self.assertTrue(summary['diagnostics'][0]['namespacesRetained'])
+ self.assertNotIn('cleanup', self.events)
+ self.assertIn('original-failure-scene', self.events)
+
+ def test_foreign_node_binding_refuses_before_tools_or_fleet(self):
+ with patch.dict(os.environ, {'RUSTFS_NODES': 'other000 other001 other002 other003'}), \
+ patch.object(runner.subprocess, 'run', side_effect=[SimpleNamespace(stdout='c' * 40),
+ SimpleNamespace(stdout='')]), patch.object(runner, 'verify_inspector') as inspector:
+ with self.assertRaisesRegex(ValueError, 'node binding'): runner.preflight(self.args)
+ inspector.assert_not_called()
+ self.assertFalse(self.events or self.fleets)
+
+ def test_real_shell_diagnostic_verdict_never_certifies_the_canonical_case(self):
+ root = Path(__file__).resolve().parent
+ dry = subprocess.run(['bash', str(root / 'scripts/validation_heal/run_case.sh'),
+ '--validation-case', runner.CASE_ID, '--dry-run'], capture_output=True, text=True)
+ self.assertEqual(dry.returncode, 0, dry.stderr)
+ shell = (root / 'scripts/validation_heal/run_case.sh').read_text()
+ self.assertLess(shell.index(' preflight', shell.index('if [ "${driver}" = lock_timeout_runner.py')),
+ shell.index('(bootstrap)'))
+ verdict = shell[shell.index('verdict=FAIL reason='):shell.index('args=("${ROOT}/scripts/validation_result.py"')]
+ self.args.evidence.mkdir()
+ path = self.args.evidence / 'healthy-summary.json'
+ for code, complete, cleanup, expected in ((0, True, 'PASS', 'BLOCKED'), (1, True, 'PASS', 'FAIL'),
+ (0, False, 'PASS', 'INFRA_ERROR'), (0, True, 'FAIL', 'INFRA_ERROR'),
+ (1, False, 'FAIL', 'FAIL'), (0, None, 'PASS', 'INFRA_ERROR')):
+ if complete is None: path.unlink()
+ else: path.write_text(json.dumps(dict(diagnosticComplete=complete, complete=False,
+ canonicalCasePassClaimed=False)))
+ checked = subprocess.run(['bash', '-euc', verdict + '\nprintf "%s" "$verdict"\n'],
+ capture_output=True, text=True, env={**os.environ, 'case_id': runner.CASE_ID,
+ 'cleanup_status': cleanup, 'case_rc': str(code), 'evidence': str(self.args.evidence)})
+ self.assertEqual(checked.returncode, 0, checked.stderr)
+ self.assertEqual(checked.stdout, expected)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/test_validation_heal_manager_fixture.py b/test_validation_heal_manager_fixture.py
new file mode 100644
index 00000000..d22e529f
--- /dev/null
+++ b/test_validation_heal_manager_fixture.py
@@ -0,0 +1,51 @@
+"""Hosted manager evidence cannot be rebound to another run or case scope."""
+
+import json
+from pathlib import Path
+import tempfile
+import unittest
+
+import test_validation_heal_manager_native as fixtures
+from scripts.validation_heal import manager_fixture as consumer
+
+
+class ManagerFixtureTests(unittest.TestCase):
+ def fixture(self, root):
+ tools, evidence, candidate, _ = fixtures.ManagerNativeEvidenceTests().fixture(root)
+ tools.rename(root / "manager-tools"); evidence.rename(root / "manager-evidence")
+ api = {"id": 12345, "run_attempt": 1, "status": "completed", "conclusion": "success",
+ "path": ".github/workflows/rustfs-functional-chain.yml", "head_sha": "b" * 40}
+ (root / "tool-run.json").write_text(json.dumps(api))
+ return candidate, api
+
+ def test_each_case_consumes_only_its_exact_original_prerequisite_tests(self):
+ with tempfile.TemporaryDirectory() as temp:
+ root = Path(temp); candidate, _ = self.fixture(root)
+ for case, keys in consumer.SCOPES.items():
+ value = consumer.verify(root, candidate, case)
+ self.assertEqual([row["regression"] for row in value["casePrerequisiteTests"]], list(keys))
+ self.assertEqual(value["exactTests"], 15)
+ self.assertFalse(value["canonicalCasePassClaimed"])
+ self.assertFalse(value["canonicalFleetBranchesExecuted"])
+ self.assertFalse(value["individualFsyncHooksClaimed"])
+ self.assertFalse(value["physicalPowerLossClaimed"])
+
+ def test_failed_partial_wrong_attempt_or_foreign_api_cannot_certify_artifact(self):
+ with tempfile.TemporaryDirectory() as temp:
+ root = Path(temp); candidate, api = self.fixture(root)
+ changes = [("id", 12346), ("id", True), ("run_attempt", 2), ("status", "in_progress"),
+ ("conclusion", "failure"), ("path", ".github/workflows/ci.yml"), ("head_sha", "main")]
+ for field, new in changes:
+ (root / "tool-run.json").write_text(json.dumps({**api, field: new}))
+ with self.subTest(field=field), self.assertRaises(ValueError):
+ consumer.verify(root, candidate, next(iter(consumer.SCOPES)))
+
+ def test_unrelated_case_and_changed_original_result_cannot_be_promoted(self):
+ with tempfile.TemporaryDirectory() as temp:
+ root = Path(temp); candidate, _ = self.fixture(root)
+ with self.assertRaises(ValueError): consumer.verify(root, candidate, 'heal.admin-nolock-safety')
+ (root / 'manager-evidence/outcome-truth.log').write_text('running 0 tests\ntest result: ok. 0 passed; 0 failed; 0 ignored;\n')
+ with self.assertRaises(ValueError): consumer.verify(root, candidate, 'heal.outcome-counters-and-error-truth')
+
+
+if __name__ == '__main__': unittest.main()
diff --git a/test_validation_heal_manager_native.py b/test_validation_heal_manager_native.py
new file mode 100644
index 00000000..40145ac1
--- /dev/null
+++ b/test_validation_heal_manager_native.py
@@ -0,0 +1,183 @@
+"""Manager native evidence must bind exact source, executable and real tests."""
+
+import copy
+import hashlib
+import json
+from pathlib import Path
+import tempfile
+import unittest
+
+from scripts.validation_heal import manager_native as suite
+
+
+class ManagerNativeEvidenceTests(unittest.TestCase):
+ def fixture(self, root, candidate=suite.native.CANDIDATE):
+ tools, evidence = root / "tools", root / "evidence"
+ tools.mkdir(); evidence.mkdir()
+ helper = "a" * 40
+ binary = tools / "heal-manager-native-tests"
+ binary.write_bytes(b"original candidate unit executable")
+ digest = hashlib.sha256(binary.read_bytes()).hexdigest()
+ contract = suite.REVIEWED_CANDIDATES[candidate]
+ manifest = {"schemaVersion": 1, "candidateCommit": candidate,
+ "candidateSourceTree": contract["sourceTree"], "candidateCargoLockSha256": contract["cargoLockSha256"],
+ "sourceFiles": contract.get("sourceFiles", suite.SOURCE_FILES), "collectorSources": suite.collectors(),
+ "regressions": suite.REGRESSIONS, "sourceUnmodified": True, "optimized": True,
+ "buildEnvironment": suite.BUILD_ENV, "binaryName": binary.name, "binarySha256": digest,
+ "fleetPassClaimed": False, "runId": "12345", "runAttempt": "1", "testingCommit": helper}
+ (tools / "manager-native.json").write_text(json.dumps(manifest))
+ listing = "\n".join(value["test"] + ": test" for value in suite.REGRESSIONS.values())
+ (tools / "test-inventory.log").write_text(listing)
+ (evidence / "runtime-inventory.log").write_text(listing)
+ rows = []
+ for key, value in suite.REGRESSIONS.items():
+ row = {"regression": key, **value, "candidateCommit": candidate,
+ "binarySha256": digest, "verdict": "PASS", "exitCode": 0, "elapsedNanoseconds": 100,
+ "command": ["/hosted/tools/" + binary.name, value["test"], "--exact", "--nocapture", "--test-threads=1"]}
+ (evidence / (key + ".json")).write_text(json.dumps(row))
+ (evidence / (key + ".log")).write_text("running 1 test\ntest " + value["test"] + " ... ok\n"
+ "test result: ok. 1 passed; 0 failed; 0 ignored; 99 filtered out; finished in 0.01s\n")
+ rows.append(row)
+ summary = {"results": rows, "complete": True, "canonicalCasePassClaimed": False,
+ "fleetPassClaimed": False, "physicalPowerLossClaimed": False}
+ (evidence / "manager-native-summary.json").write_text(json.dumps(summary))
+ return tools, evidence, {"source_sha": candidate}, helper
+
+ def verify(self, fixture, **changes):
+ tools, evidence, candidate, helper = fixture
+ return suite.verify_evidence(tools, evidence, changes.get("candidate", candidate),
+ changes.get("run", 12345), changes.get("attempt", 1), changes.get("helper", helper))
+
+ def test_complete_exact_native_evidence_preserves_separate_scope(self):
+ with tempfile.TemporaryDirectory() as temp:
+ result = self.verify(self.fixture(Path(temp)))
+ self.assertEqual(result["exactTests"], 15)
+ self.assertFalse(result["canonicalCasePassClaimed"])
+ self.assertFalse(result["fleetPassClaimed"])
+
+ def test_each_reviewed_candidate_requires_its_own_tree_and_results(self):
+ for candidate in suite.REVIEWED_CANDIDATES:
+ with self.subTest(candidate=candidate), tempfile.TemporaryDirectory() as temp:
+ result = self.verify(self.fixture(Path(temp), candidate))
+ self.assertEqual(result["candidateCommit"], candidate)
+ self.assertEqual(result["exactTests"], 15)
+ self.assertFalse(result["canonicalCasePassClaimed"])
+
+ def test_old_tree_cannot_be_relabelled_as_new_candidate(self):
+ newer = next(commit for commit in suite.REVIEWED_CANDIDATES if commit != suite.native.CANDIDATE)
+ with tempfile.TemporaryDirectory() as temp:
+ fixture = self.fixture(Path(temp), newer)
+ path = fixture[0] / "manager-native.json"
+ manifest = json.loads(path.read_text())
+ manifest["candidateSourceTree"] = suite.SOURCE_TREE
+ path.write_text(json.dumps(manifest))
+ with self.assertRaisesRegex(ValueError, "reviewed source"):
+ self.verify(fixture)
+
+ def test_b07_candidate_requires_its_actual_storage_source_and_lock(self):
+ candidate = '1c7fc818dd858247c45b39542abd8291b1e6e31b'
+ for field, old in (('sourceFiles', suite.SOURCE_FILES), ('candidateCargoLockSha256', suite.LOCK_SHA)):
+ with self.subTest(field=field), tempfile.TemporaryDirectory() as temporary:
+ fixture = self.fixture(Path(temporary), candidate)
+ path = fixture[0] / 'manager-native.json'; manifest = json.loads(path.read_text())
+ self.assertNotEqual(manifest[field], old)
+ manifest[field] = old; path.write_text(json.dumps(manifest))
+ with self.assertRaisesRegex(ValueError, 'reviewed source'): self.verify(fixture)
+
+ def test_old_result_rows_cannot_certify_new_candidate_manifest(self):
+ newer = next(commit for commit in suite.REVIEWED_CANDIDATES if commit != suite.native.CANDIDATE)
+ with tempfile.TemporaryDirectory() as temp:
+ fixture = self.fixture(Path(temp), newer)
+ summary_path = fixture[1] / "manager-native-summary.json"
+ summary = json.loads(summary_path.read_text())
+ for row in summary["results"]:
+ row["candidateCommit"] = suite.native.CANDIDATE
+ (fixture[1] / (row["regression"] + ".json")).write_text(json.dumps(row))
+ summary_path.write_text(json.dumps(summary))
+ with self.assertRaisesRegex(ValueError, "exact scope or identity"):
+ self.verify(fixture)
+
+ def test_admin_batch_candidate_rejects_previous_manager_source_proof(self):
+ candidate = "7951bb629821246868ee3761b321dd07c06db45c"
+ previous = suite.REVIEWED_CANDIDATES["1c7fc818dd858247c45b39542abd8291b1e6e31b"]
+ for field, old in (("sourceFiles", previous["sourceFiles"]),
+ ("candidateSourceTree", previous["sourceTree"])):
+ with self.subTest(field=field), tempfile.TemporaryDirectory() as temp:
+ fixture = self.fixture(Path(temp), candidate)
+ path = fixture[0] / "manager-native.json"
+ manifest = json.loads(path.read_text())
+ manifest[field] = old
+ path.write_text(json.dumps(manifest))
+ with self.assertRaisesRegex(ValueError, "reviewed source"):
+ self.verify(fixture)
+
+ def test_wrong_candidate_build_attempt_or_helper_is_rejected(self):
+ with tempfile.TemporaryDirectory() as temp:
+ fixture = self.fixture(Path(temp))
+ for changes in [{"candidate": {"source_sha": "b" * 40}}, {"run": 12346}, {"attempt": 2}, {"helper": "b" * 40}]:
+ with self.subTest(changes=changes), self.assertRaises(ValueError): self.verify(fixture, **changes)
+
+ def test_source_lock_collector_and_scope_drift_are_rejected(self):
+ with tempfile.TemporaryDirectory() as temp:
+ fixture = self.fixture(Path(temp)); path = fixture[0] / "manager-native.json"
+ original = json.loads(path.read_text())
+ for field, value in [("candidateSourceTree", "b" * 40), ("candidateCargoLockSha256", "b" * 64),
+ ("sourceFiles", {}), ("collectorSources", {}), ("regressions", {}),
+ ("sourceUnmodified", False), ("optimized", False), ("fleetPassClaimed", True),
+ ("buildEnvironment", {}), ("binaryName", "../foreign")]:
+ path.write_text(json.dumps({**original, field: value}))
+ with self.subTest(field=field), self.assertRaises(ValueError): self.verify(fixture)
+
+ def test_changed_or_symlinked_binary_is_rejected(self):
+ for changed in ["bytes", "symlink"]:
+ with tempfile.TemporaryDirectory() as temp:
+ fixture = self.fixture(Path(temp)); binary = fixture[0] / "heal-manager-native-tests"
+ if changed == "bytes": binary.write_bytes(b"modified")
+ else:
+ original = fixture[0] / "original"; binary.rename(original); binary.symlink_to(original)
+ with self.subTest(changed=changed), self.assertRaises(ValueError): self.verify(fixture)
+
+ def test_zero_ignored_failed_or_missing_original_test_never_passes(self):
+ with tempfile.TemporaryDirectory() as temp:
+ fixture = self.fixture(Path(temp)); key = next(iter(suite.REGRESSIONS)); path = fixture[1] / (key + ".log")
+ original = path.read_text()
+ for output in ["running 0 tests\ntest result: ok. 0 passed; 0 failed; 0 ignored;\n",
+ original.replace(" ... ok", " ... ignored").replace("1 passed; 0 failed; 0 ignored", "0 passed; 0 failed; 1 ignored"),
+ original.replace(" ... ok", " ... FAILED"), original.replace("running 1 test", "running 2 tests")]:
+ path.write_text(output)
+ with self.assertRaises(ValueError): self.verify(fixture)
+
+ def test_missing_duplicate_or_scope_changed_results_are_rejected(self):
+ with tempfile.TemporaryDirectory() as temp:
+ fixture = self.fixture(Path(temp)); path = fixture[1] / "manager-native-summary.json"
+ original = json.loads(path.read_text())
+ for changed in ["missing", "duplicate", "scope", "power", "fleet"]:
+ value = copy.deepcopy(original)
+ if changed == "missing": value["results"].pop()
+ elif changed == "duplicate": value["results"][-1] = value["results"][0]
+ elif changed == "scope": value["results"][0]["emittedBarrierLogsClaimed"] = True
+ elif changed == "power": value["physicalPowerLossClaimed"] = True
+ else: value["fleetPassClaimed"] = True
+ path.write_text(json.dumps(value))
+ with self.subTest(changed=changed), self.assertRaises(ValueError): self.verify(fixture)
+
+ def test_listing_and_invocation_require_the_complete_exact_test_name(self):
+ for changed in ["missing", "duplicate", "filter", "parallel", "wrong-name"]:
+ with tempfile.TemporaryDirectory() as temp:
+ fixture = self.fixture(Path(temp)); key = next(iter(suite.REGRESSIONS))
+ if changed in {"missing", "duplicate"}:
+ path = fixture[1] / "runtime-inventory.log"; listing = path.read_text()
+ path.write_text(listing.replace(suite.REGRESSIONS[key]["test"] + ": test", "") if changed == "missing"
+ else listing + "\n" + suite.REGRESSIONS[key]["test"] + ": test")
+ else:
+ path = fixture[1] / (key + ".json"); row = json.loads(path.read_text())
+ if changed == "filter": row["command"].remove("--exact")
+ elif changed == "parallel": row["command"][-1] = "--test-threads=8"
+ else: row["command"][1] = "checkpoint"
+ path.write_text(json.dumps(row))
+ summary_path = fixture[1] / "manager-native-summary.json"; summary = json.loads(summary_path.read_text())
+ summary["results"][0] = row; summary_path.write_text(json.dumps(summary))
+ with self.subTest(changed=changed), self.assertRaises(ValueError): self.verify(fixture)
+
+
+if __name__ == "__main__": unittest.main()
diff --git a/test_validation_heal_mpu_race_backups.py b/test_validation_heal_mpu_race_backups.py
new file mode 100644
index 00000000..7e7d056a
--- /dev/null
+++ b/test_validation_heal_mpu_race_backups.py
@@ -0,0 +1,151 @@
+"""Actual file/descriptor checks with a mock decoder; no live metadata proof."""
+import copy
+import hashlib
+import json
+import os
+from pathlib import Path
+import tempfile
+from types import SimpleNamespace
+import unittest
+from unittest.mock import Mock,patch
+import uuid
+
+from scripts.validation_heal import fleet_node, heal_fault, mpu_race_backups as backup
+from scripts.validation_heal.healthy import Fleet
+
+
+class MultipartBackupTests(unittest.TestCase):
+ def frames(self,nodes,prefix_count):
+ bucket='rv-heal-0123456789abcdef';old,new=str(uuid.UUID(int=1)),str(uuid.UUID(int=2));binary='a'*64;inspector='b'*64
+ before,after,evidence={},{},{}
+ for host_number,host in enumerate(nodes):
+ common={'hostname':'host-'+host,'binarySha256':binary,'quiescent':True}
+ before[host]={**common,'files':[]};after[host]={**common,'files':[]}
+ evidence[host]={**common,'readOnly':True,'sameHeldFileHashedAndDecoded':True,'rawContentsExported':False,
+ 'liveAuthorityClaimed':False,'inspectorSha256':inspector,'rows':[]}
+ for disk in range(4):
+ v0={'versionId':old,'dataDir':str(uuid.UUID(int=100)),'isLatest':True,'deleted':False,
+ 'codingIndex':host_number*4+disk+1,'parts':[{'number':n,'digest':'old-'+str(n)} for n in (1,2,3)]}
+ v1={**v0,'versionId':new,'dataDir':str(uuid.UUID(int=200)),'parts':[{'number':n,'digest':'new-'+str(n)} for n in (1,2,3)]}
+ m0={'bucket':bucket,'key':backup.KEY,'physicalBitrotVerified':False,'versions':[copy.deepcopy(v0)]}
+ m1={**m0,'versions':[{**v0,'isLatest':False},v1]}
+ before[host]['files'].append({'disk':disk,'path':f'{bucket}/{backup.KEY}/xl.meta','size':100,'sha256':'0'*64,'metadata':m0})
+ after[host]['files'].append({'disk':disk,'path':f'{bucket}/{backup.KEY}/xl.meta','size':200,'sha256':'1'*64,'metadata':m1})
+ row={'disk':disk,'path':f'{bucket}/{backup.KEY}/{v0["dataDir"]}/xl.meta.bkp','size':100+prefix_count,
+ 'sha256':str(prefix_count+2)*64}
+ after[host]['files'].append(copy.deepcopy(row))
+ evidence[host]['rows'].append({**row,'metadata':copy.deepcopy(m0 if prefix_count==0 else m1)})
+ return before,after,evidence,bucket,old,new,binary,inspector
+
+ def test_frozen_backup_oracle_accepts_only_original_or_complete_ack_prefix_in_h1_h2(self):
+ for nodes in (['vm000'],['vm000','vm001','vm002','vm003']):
+ for count in (0,1):
+ with self.subTest(nodes=nodes,prefix=count):
+ before,after,evidence,bucket,old,new,binary,inspector=self.frames(nodes,count)
+ proof=backup.verify(before,after,evidence,nodes,bucket,old,new,binary,inspector)
+ self.assertEqual(len(proof['proofs']),4*len(nodes))
+ self.assertEqual({r['acknowledgedCompletePrefix'] for r in proof['proofs']},{count})
+ self.assertFalse(proof['liveAuthorityClaimed'] or proof['physicalRepairProved'] or proof['canonicalCasePassClaimed'])
+ self.assertTrue(all(not r['unexposedMetadataVerified'] for r in proof['proofs']))
+
+ def test_backup_oracle_rejects_drift_foreign_generation_nonprefix_and_incomplete_scope(self):
+ for fault in ('sha','generation','version','latest','descriptor','omitted','duplicate','hostname','primary','bool-disk','binary'):
+ with self.subTest(fault=fault):
+ nodes=['vm000'];before,after,evidence,bucket,old,new,binary,inspector=self.frames(nodes,1)
+ row=evidence['vm000']['rows'][0]
+ if fault=='sha':row['sha256']='f'*64
+ elif fault=='generation':
+ row['path']=row['path'].replace(str(uuid.UUID(int=100)),str(uuid.UUID(int=200)))
+ after['vm000']['files'][1]['path']=row['path']
+ elif fault=='version':row['metadata']['versions'][1]['versionId']=str(uuid.UUID(int=3))
+ elif fault=='latest':row['metadata']['versions'][0]['isLatest']=True
+ elif fault=='descriptor':row['metadata']['versions'][0]['parts'][0]['digest']='wrong'
+ elif fault=='omitted':evidence['vm000']['rows'].pop()
+ elif fault=='duplicate':evidence['vm000']['rows'].append(copy.deepcopy(row))
+ elif fault=='hostname':evidence['vm000']['hostname']='foreign'
+ elif fault=='primary':after['vm000']['files']=[r for r in after['vm000']['files'] if not (r['disk']==2 and r['path'].endswith('/xl.meta'))]
+ elif fault=='bool-disk':before['vm000']['files'][0]['disk']=False
+ else:binary=None
+ with self.assertRaises(ValueError):backup.verify(before,after,evidence,nodes,bucket,old,new,binary,inspector)
+
+ def fixture(self,folder):
+ folder=Path(folder).resolve();state=folder/'state';state.mkdir()
+ (state/'rustfs').write_bytes(b'owned candidate');(state/'heal-meta-inspect').write_bytes(b'owned inspector')
+ (state/'credentials.env').write_bytes(b'private mock credential; never opened by collector')
+ mounts=tuple(folder/f'mount-{i}' for i in range(4));run='gh-12345-1-h1';bucket='rv-heal-0123456789abcdef'
+ roots=tuple(m/'heal-validation'/run for m in mounts);files=[]
+ for i,root in enumerate(roots):
+ key=root/'data'/bucket/backup.KEY;key.mkdir(parents=True);(key/'xl.meta').write_bytes(b'live current metadata not read here')
+ generation=key/str(uuid.UUID(int=100));generation.mkdir()
+ path=generation/'xl.meta.bkp';path.write_bytes(f'actual original backup on disk{i}'.encode());files.append(path)
+ (generation/'part.1').write_bytes(b'payload part not read by collector')
+ owner={'runtimeProfile':'manual','bucket':bucket,'hostname':'owned-test-host',
+ 'binarySha256':fleet_node.digest(state/'rustfs'),'inspectorSha256':fleet_node.digest(state/'heal-meta-inspect')}
+ stopped=Mock();decoded=[]
+ def digest(p):
+ self.assertIn(p,(state/'rustfs',state/'heal-meta-inspect'))
+ return fleet_node.digest(p)
+ def command(*args,**kwargs):
+ self.assertEqual(args[0],str(state/'heal-meta-inspect'))
+ self.assertTrue(args[1].startswith(f'/proc/{os.getpid()}/fd/'))
+ fd=int(args[1].rsplit('/',1)[1]);raw=os.pread(fd,backup.LIMIT+1,0)
+ self.assertIn(raw,[p.read_bytes() for p in files])
+ decoded.append(hashlib.sha256(raw).hexdigest())
+ return SimpleNamespace(returncode=0,stdout=json.dumps({'bucket':bucket,'key':backup.KEY,
+ 'physicalBitrotVerified':False,'mockInputSha256':decoded[-1]}),stderr='')
+ node={'owned':lambda run:(state,roots,copy.deepcopy(owner)),'MOUNTS':mounts,'checked':fleet_node.checked,
+ 'digest':digest,'stopped':stopped,'fault':heal_fault,'command':command}
+ request={'runId':run,'action':'mpu-race-backups','bucket':bucket,
+ 'expectedBinarySha256':owner['binarySha256'],'expectedInspectorSha256':owner['inspectorSha256']}
+ return request,node,files,decoded
+
+ def test_stopped_census_hash_and_decoder_share_actual_opened_inode_on_every_drive(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ request,node,files,decoded=self.fixture(tmp);original=[p.read_bytes() for p in files]
+ with patch.object(heal_fault,'mount_id',side_effect=lambda fd:os.fstat(fd).st_dev):value=backup.run(request,node)
+ self.assertEqual(len(value['rows']),4);self.assertEqual(len(decoded),4)
+ for i,row in enumerate(value['rows']):
+ self.assertEqual(row['disk'],i);self.assertEqual(row['size'],len(original[i]))
+ self.assertEqual(row['sha256'],hashlib.sha256(original[i]).hexdigest())
+ self.assertEqual(row['metadata']['mockInputSha256'],row['sha256'])
+ self.assertTrue(value['readOnly'] and value['quiescent'] and value['sameHeldFileHashedAndDecoded'])
+ self.assertFalse(value['rawContentsExported'] or value['liveAuthorityClaimed'] or value['canonicalCasePassClaimed'])
+ self.assertEqual([p.read_bytes() for p in files],original)
+ self.assertEqual(node['stopped'].call_count,2)
+
+ def test_hardlinks_symlinks_fifo_oversize_and_unknown_generation_refuse_without_decoder(self):
+ for fault in ('hardlink','symlink','fifo','oversize','generation'):
+ with self.subTest(fault=fault),tempfile.TemporaryDirectory() as tmp:
+ request,node,files,decoded=self.fixture(tmp);p=files[0]
+ if fault=='hardlink':os.link(p,p.with_name('extra-link'))
+ elif fault=='symlink':p.unlink();p.symlink_to(Path(tmp)/'state/credentials.env')
+ elif fault=='fifo':p.unlink();os.mkfifo(p)
+ elif fault=='oversize':p.write_bytes(b'x'*(backup.LIMIT+1))
+ else:p.parent.rename(p.parent.with_name('unregistered-directory'))
+ with patch.object(heal_fault,'mount_id',side_effect=lambda fd:os.fstat(fd).st_dev),self.assertRaises((OSError,ValueError)):
+ backup.run(request,node)
+ self.assertFalse(decoded)
+
+ def test_injected_file_mount_escape_and_decoder_identity_or_inode_drift_refuse(self):
+ for fault in ('mount','decoder','inode'):
+ with self.subTest(fault=fault),tempfile.TemporaryDirectory() as tmp:
+ request,node,files,decoded=self.fixture(tmp);p=files[0];inode=p.stat().st_ino
+ original=node['command']
+ def command(*args,**kwargs):
+ value=original(*args,**kwargs)
+ if fault=='decoder':value.stdout=json.dumps({'bucket':'foreign','key':backup.KEY,'physicalBitrotVerified':False})
+ if fault=='inode':p.unlink();p.write_bytes(b'replaced after held-file decode')
+ return value
+ node['command']=command
+ def mount_id(fd):return os.fstat(fd).st_dev+(1 if fault=='mount' and os.fstat(fd).st_ino==inode else 0)
+ with patch.object(heal_fault,'mount_id',side_effect=mount_id),self.assertRaises(ValueError):backup.run(request,node)
+
+ def test_real_remote_adapter_embeds_backend_and_existing_descriptor_reader(self):
+ def execute(*args,**kwargs):
+ source=kwargs['input'].rsplit('\nmain(',1)[0];scope={'__name__':'__mpu_backups_test__'}
+ exec(compile(source,'g05-backups-node','exec'),scope)
+ self.assertTrue(callable(scope['mpu_race_backups_backend'].run))
+ self.assertTrue(callable(scope['fault'].parent))
+ return SimpleNamespace(returncode=0,stdout='{}',stderr='')
+ with tempfile.TemporaryDirectory() as tmp,patch('scripts.validation_heal.healthy.subprocess.run',side_effect=execute):
+ Fleet(['vm000'],Path(tmp),'gh-12345-1-h1').remote('vm000','mpu-race-backups')
diff --git a/test_validation_heal_mpu_race_committed.py b/test_validation_heal_mpu_race_committed.py
new file mode 100644
index 00000000..020a3331
--- /dev/null
+++ b/test_validation_heal_mpu_race_committed.py
@@ -0,0 +1,127 @@
+"""Decoded G05 business generations across H1/H2; no staging/live PASS claim."""
+import copy
+import hashlib
+import tempfile
+import unittest
+from unittest.mock import patch
+import uuid
+
+from scripts.validation_heal import mpu_race_committed as committed, mpu_race_fixture as model
+import test_validation_heal_mpu_race_fixture as model_fixtures
+
+
+class MultipartCommittedTests(unittest.TestCase):
+ bucket = 'rv-heal-0123456789abcdef'
+
+ def scene(self, directory, topology):
+ recorder = model_fixtures.Recorder(directory)
+ fixture = model.MpuRaceFixture(recorder, self.bucket); fixture.seed()
+ before_model = fixture.manifest()
+ fixture.upload('a', 3); fixture.complete('a'); fixture.abort_b()
+ after_model = fixture.manifest()
+ nodes = ['vm000'] if topology == 'H1' else [f'vm00{i}' for i in range(4)]
+ width = 4 * len(nodes); data = 2 if topology == 'H1' else 12
+ ids = [str(uuid.UUID(int=1000 + i)) for i in range(width)]; distribution = list(range(width, 0, -1))
+ def snapshot(manifest):
+ expected = committed.entries(manifest); result = {}
+ for node_number, node in enumerate(nodes):
+ value = result[node] = {'hostname': 'host-' + node, 'binarySha256': 'a' * 64,
+ 'quiescent': True, 'pid': 0, 'formats': [], 'files': []}
+ for disk in range(4):
+ slot = node_number * 4 + disk; index = distribution[slot]
+ value['formats'].append({'disk': disk, 'value': {'format': 'xl', 'id': 'pool',
+ 'xl': {'sets': [ids], 'this': ids[slot]}}})
+ versions = []
+ for entry in expected:
+ role = next(r for r in ('committed', 'a') if manifest['items'][r].get('versionId') == entry['versionId'])
+ directory_id = str(uuid.UUID(int=100 if role == 'committed' else 200))
+ versions.append({'versionId': entry['versionId'], 'isLatest': entry['isLatest'], 'deleted': False,
+ 'dataDir': directory_id, 'size': entry['payload']['size'], 'inline': None, 'usesLegacyChecksum': True,
+ 'role': 'data' if index <= data else 'parity',
+ 'parts': [{'number': p['number'], 'size': p['size'], 'actualSize': p['size']} for p in entry['multipartParts']],
+ 'erasure': {'index': index, 'data_blocks': data, 'parity_blocks': width - data,
+ 'block_size': 1048576, 'distribution': distribution, 'algorithm': 'rs-vandermonde'}})
+ for part in entry['multipartParts']:
+ path = f'{self.bucket}/{model.KEY}/{directory_id}/part.{part["number"]}'
+ value['files'].append({'disk': disk, 'path': path, 'size': (part['size'] + data - 1) // data + 32,
+ 'sha256': hashlib.sha256(f'{role}/{index}/{part["number"]}'.encode()).hexdigest()})
+ value['files'].append({'disk': disk, 'path': path + '.meta', 'size': 29,
+ 'sha256': hashlib.sha256(f'upload/{role}/{index}/{part["number"]}'.encode()).hexdigest()})
+ value['files'].append({'disk': disk, 'path': f'{self.bucket}/{model.KEY}/xl.meta', 'size': 1000,
+ 'sha256': 'd' * 64, 'metadata': {'bucket': self.bucket, 'key': model.KEY,
+ 'physicalBitrotVerified': False, 'versions': versions}})
+ return result
+ before, after = snapshot(before_model), snapshot(after_model)
+ evidence = {node: {'hostname': 'host-' + node, 'binarySha256': 'a' * 64, 'inspectorSha256': 'b' * 64,
+ 'quiescent': True, 'readOnly': True, 'sameHeldFileHashedAndDecoded': True,
+ 'rawContentsExported': False, 'liveAuthorityClaimed': False, 'rows': []} for node in nodes}
+ return before, after, before_model, after_model, evidence
+
+ def test_three_part_full_cohorts_and_old_version_survive_complete_in_both_topologies(self):
+ for topology in ('H1', 'H2'):
+ with self.subTest(topology=topology), tempfile.TemporaryDirectory() as directory, patch.object(model, 'PART_SIZES', (101, 111, 131)):
+ before, after, original, final, evidence = self.scene(directory, topology)
+ cohorts, proof = committed.transition(before, after, original, final, evidence, 'a' * 64, 'b' * 64)
+ self.assertEqual(len(cohorts), 6)
+ self.assertTrue(all(len(t['cohort']) == 4 * len(before) for t in cohorts.values()))
+ self.assertTrue(proof['oldVersionRetained'] and proof['newVersionExactlyOnce'] and proof['allPartsCodingIndicesVerified'])
+ self.assertTrue(proof['stagingContinuityStillRequired'] and proof['fullPayloadS3StillRequired'])
+ self.assertFalse(proof['canonicalCasePassClaimed'] or proof['physicalRepairProved'])
+
+ def test_reclaimed_upload_sidecar_is_reported_and_never_used_as_committed_authority(self):
+ with tempfile.TemporaryDirectory() as directory, patch.object(model, 'PART_SIZES', (101, 111, 131)):
+ before, after, original, final, evidence = self.scene(directory, 'H1')
+ old_path = f'{self.bucket}/{model.KEY}/{uuid.UUID(int=100)}/part.2.meta'
+ after['vm000']['files'] = [r for r in after['vm000']['files'] if not (r['disk'] == 2 and r['path'] == old_path)]
+ _, proof = committed.transition(before, after, original, final, evidence, 'a' * 64, 'b' * 64)
+ self.assertEqual(proof['reclaimedOldUploadSidecars'], [['vm000', 2, old_path]])
+ self.assertFalse(proof['uploadSidecarsAreCommittedAuthority'])
+
+ def test_backup_prefix_decoder_is_required_by_the_actual_business_transition(self):
+ for count in (0, 1):
+ with self.subTest(prefix=count), tempfile.TemporaryDirectory() as directory, patch.object(model, 'PART_SIZES', (101, 111, 131)):
+ before, after, original, final, evidence = self.scene(directory, 'H1')
+ for disk in range(4):
+ metadata = next(r['metadata'] for r in (before if count == 0 else after)['vm000']['files']
+ if r['disk'] == disk and r['path'].endswith('/xl.meta'))
+ row = {'disk': disk, 'path': f'{self.bucket}/{model.KEY}/{uuid.UUID(int=100)}/xl.meta.bkp',
+ 'size': 101 + count, 'sha256': hashlib.sha256(f'backup/{disk}/{count}'.encode()).hexdigest()}
+ after['vm000']['files'].append(copy.deepcopy(row))
+ evidence['vm000']['rows'].append({**row, 'metadata': copy.deepcopy(metadata)})
+ _, proof = committed.transition(before, after, original, final, evidence, 'a' * 64, 'b' * 64)
+ self.assertEqual({p['acknowledgedCompletePrefix'] for p in proof['backupProof']['proofs']}, {count})
+ evidence['vm000']['rows'][0]['metadata']['versions'][0]['parts'][0]['size'] += 1
+ with self.assertRaises(ValueError): committed.transition(before, after, original, final, evidence, 'a' * 64, 'b' * 64)
+
+ def test_drift_missing_parts_wrong_geometry_and_unknown_leaves_are_rejected(self):
+ for topology in ('H1', 'H2'):
+ for fault in ('old-shard', 'missing-new-part', 'old-sidecar', 'generation', 'extra-version', 'logical-size',
+ 'unknown-leaf', 'format', 'live', 'bool-pid', 'bool-disk', 'sibling-id', 'model-payload', 'multipart-etag',
+ 'a-before-state', 'a-metadata', 'b-parts'):
+ with self.subTest(topology=topology, fault=fault), tempfile.TemporaryDirectory() as directory, patch.object(model, 'PART_SIZES', (101, 111, 131)):
+ before, after, original, final, evidence = self.scene(directory, topology)
+ rows = after['vm000']['files']; prefix = f'{self.bucket}/{model.KEY}/'
+ old_part = next(r for r in rows if r['path'] == prefix + str(uuid.UUID(int=100)) + '/part.2')
+ meta = next(r['metadata'] for r in rows if r['path'] == prefix + 'xl.meta')
+ if fault == 'old-shard': old_part['sha256'] = 'e' * 64
+ elif fault == 'missing-new-part': rows.remove(next(r for r in rows if r['path'] == prefix + str(uuid.UUID(int=200)) + '/part.3'))
+ elif fault == 'old-sidecar': next(r for r in rows if r['path'] == old_part['path'] + '.meta')['sha256'] = 'e' * 64
+ elif fault == 'generation': meta['versions'][-1]['dataDir'] = str(uuid.UUID(int=100))
+ elif fault == 'extra-version': meta['versions'].append({**copy.deepcopy(meta['versions'][0]), 'versionId': str(uuid.UUID(int=999))})
+ elif fault == 'logical-size': meta['versions'][-1]['parts'][1]['size'] += 1
+ elif fault == 'unknown-leaf': rows.append({**old_part, 'path': old_part['path'] + '.unknown'})
+ elif fault == 'format': after['vm000']['formats'][0]['value']['id'] = 'foreign'
+ elif fault == 'live': after['vm000']['quiescent'] = False
+ elif fault == 'bool-pid': after['vm000']['pid'] = False
+ elif fault == 'bool-disk': old_part['disk'] = False
+ elif fault == 'sibling-id': final['items']['b']['uploadId'] += '-different'
+ elif fault == 'model-payload': final['items']['a']['payload']['sha256'] = 'e' * 64
+ elif fault == 'multipart-etag': final['items']['a']['etag'] = '"wrong-3"'
+ elif fault == 'a-before-state': original['items']['a']['state'] = 'unknown'
+ elif fault == 'a-metadata': final['items']['a']['metadata']['nonce'] += '-changed'
+ else: final['items']['b']['parts'].pop()
+ with self.assertRaises(ValueError): committed.transition(before, after, original, final, evidence, 'a' * 64, 'b' * 64)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/test_validation_heal_mpu_race_fixture.py b/test_validation_heal_mpu_race_fixture.py
new file mode 100644
index 00000000..c4abfc9a
--- /dev/null
+++ b/test_validation_heal_mpu_race_fixture.py
@@ -0,0 +1,212 @@
+import copy
+import hashlib
+import json
+from pathlib import Path
+import sys
+import tempfile
+import unittest
+from unittest.mock import Mock,patch
+import uuid
+
+from scripts.validation_heal import mpu_race_fixture as m
+from scripts.validation_heal.heal_workload import Recorder as DurableRecorder
+
+
+class Recorder:
+ def __init__(self,folder):
+ self.output=Path(folder);self.sequence=0;self.uploads={};self.versions=[];self.calls=[];self.fail=None;self.corrupt=None
+
+ def call(self,operation,params,body=None,**kwargs):
+ self.calls.append((operation,copy.deepcopy(params),kwargs));self.sequence+=1
+ if self.fail==operation:raise ValueError('uncertain call; durable production Recorder owns raw receipt')
+ response={'ResponseMetadata':{'HTTPStatusCode':204 if operation=='abort_multipart_upload' else 200}}
+ if operation=='create_multipart_upload':
+ uid=f'upload-{len(self.uploads)+1}';self.uploads[uid]={'params':copy.deepcopy(params),'parts':{},'state':'pending'}
+ response['UploadId']=uid
+ elif operation=='upload_part':
+ self.uploads[params['UploadId']]['parts'][params['PartNumber']]=body;response['ETag']=m.etag(body)
+ elif operation=='complete_multipart_upload':
+ item=self.uploads[params['UploadId']];parts=[{'number':n,'etag':m.etag(b)} for n,b in sorted(item['parts'].items())]
+ item['state']='complete';raw=b''.join(v for _,v in sorted(item['parts'].items()))
+ version={'VersionId':str(uuid.UUID(int=len(self.versions)+1)),'ETag':m.multipart_etag(parts),
+ 'Metadata':item['params']['Metadata'],'ContentType':'application/octet-stream',
+ 'ContentLength':len(raw),'body':raw}
+ self.versions.append(version);response.update({k:version[k] for k in ('VersionId','ETag')})
+ elif operation=='abort_multipart_upload':self.uploads[params['UploadId']]['state']='aborted'
+ elif operation=='list_multipart_uploads':
+ response.update(IsTruncated=False,Uploads=[{'Key':m.KEY,'UploadId':u} for u,v in self.uploads.items() if v['state']=='pending'])
+ elif operation=='list_parts':
+ item=self.uploads[params['UploadId']]
+ if item['state']=='pending':response.update(IsTruncated=False,Parts=[{'PartNumber':n,'ETag':m.etag(b),'Size':len(b)} for n,b in sorted(item['parts'].items())])
+ else:response={'ResponseMetadata':{'HTTPStatusCode':404},'Error':{'Code':'NoSuchUpload'}}
+ elif operation=='list_object_versions':
+ response.update(IsTruncated=False,Versions=[{'Key':m.KEY,'VersionId':v['VersionId'],'ETag':v['ETag'],
+ 'Size':v['ContentLength'],'IsLatest':v is self.versions[-1]} for v in self.versions])
+ elif operation in ('head_object','get_object'):
+ version=next(v for v in self.versions if v['VersionId']==params['VersionId']) if 'VersionId' in params else self.versions[-1]
+ response.update({k:v for k,v in version.items() if k!='body'})
+ else:raise AssertionError(operation)
+ if self.corrupt:response=self.corrupt(operation,response)
+ observation={'params':copy.deepcopy(params),'response':copy.deepcopy(response)}
+ if operation=='get_object':observation['payload']={'size':len(version['body']),'sha256':hashlib.sha256(version['body']).hexdigest()}
+ m.save(self.output/f'{self.sequence:06d}-ack.json',{'observation':observation})
+ return response
+
+
+class MpuRaceFixtureTests(unittest.TestCase):
+ def setUp(self):
+ patcher=patch.object(m,'PART_SIZES',(12,13,14));patcher.start();self.addCleanup(patcher.stop)
+ tmp=tempfile.TemporaryDirectory();self.addCleanup(tmp.cleanup)
+ self.rec=Recorder(tmp.name);self.fixture=m.MpuRaceFixture(self.rec,'rv-g05-owned')
+
+ def test_seed_same_key_original_and_two_distinct_pending_uploads(self):
+ value=self.fixture.seed();items=value['items']
+ self.assertEqual([items[r]['state'] for r in m.ROLES],['complete','pending','pending'])
+ self.assertEqual([len(items[r]['parts']) for r in m.ROLES],[3,2,3])
+ self.assertEqual(len({v['uploadId'] for v in items.values()}),3)
+ self.assertEqual({v[1]['Key'] for v in self.rec.calls},{m.KEY})
+ self.assertEqual(len({items[r]['parts'][0]['etag'] for r in m.ROLES}),3)
+ self.assertFalse(value['casePassClaimed']);self.assertFalse(value['physicalRepairProved'])
+ self.assertEqual(self.fixture.verify_s3(self.rec)['pendingUploads'],2)
+
+ def test_continue_complete_abort_preserve_sibling_and_old_committed_version_both_orders(self):
+ for abort_first in (False,True):
+ with self.subTest(abort_first=abort_first),tempfile.TemporaryDirectory() as tmp:
+ rec=Recorder(tmp);fixture=m.MpuRaceFixture(rec,'rv-g05-owned');fixture.seed()
+ if abort_first:fixture.abort_b();fixture.verify_s3(rec)
+ fixture.upload('a',3);fixture.verify_s3(rec)
+ fixture.complete('a');fixture.verify_s3(rec)
+ if not abort_first:fixture.abort_b()
+ proof=fixture.verify_s3(rec)
+ self.assertEqual(proof['committedVersions'],2);self.assertEqual(proof['pendingUploads'],0)
+ self.assertTrue(proof['sameKeySiblingIsolationVerified']);self.assertFalse(proof['overlapProved'])
+ abort=next(c for c in rec.calls if c[0]=='abort_multipart_upload')
+ self.assertEqual(abort[1]['UploadId'],fixture.items['b']['uploadId']);self.assertEqual(abort[2]['expect_http'],204)
+
+ def test_unknown_mutation_halts_sibling_and_never_retries(self):
+ self.fixture.seed();self.rec.fail='upload_part'
+ with self.assertRaisesRegex(ValueError,'uncertain'):self.fixture.upload('a',3)
+ calls=len(self.rec.calls)
+ with self.assertRaisesRegex(ValueError,'halted'):self.fixture.abort_b()
+ self.assertEqual(len(self.rec.calls),calls);self.assertIsNotNone(self.fixture.poisoned)
+ self.assertEqual(self.fixture.items['a']['state'],'pending')
+ self.assertEqual(self.fixture.events[-1]['state'],'UNRESOLVED')
+ with self.assertRaises(ValueError):self.fixture.verify_s3(self.rec)
+
+ def test_malformed_successful_ack_keeps_durable_failure_and_prevents_retry(self):
+ self.fixture.seed();self.fixture.upload('a',3)
+ self.rec.corrupt=lambda op,r:{**r,'ETag':'"wrong"'} if op=='complete_multipart_upload' else r
+ with self.assertRaisesRegex(ValueError,'multipart ETag'):self.fixture.complete('a')
+ self.assertTrue(self.fixture.events[-1]['responseReceived'])
+ self.assertEqual(self.fixture.items['a']['state'],'pending')
+ with self.assertRaisesRegex(ValueError,'halted'):self.fixture.abort_b()
+
+ def test_out_of_order_duplicate_or_unprepared_calls_never_go_to_network(self):
+ with self.assertRaises(ValueError):self.fixture.create('b')
+ self.assertFalse(self.rec.calls);self.fixture.seed();count=len(self.rec.calls)
+ for method in (lambda:self.fixture.seed(),lambda:self.fixture.create('a'),lambda:self.fixture.upload('a',2),
+ lambda:self.fixture.upload('a',True),lambda:self.fixture.complete('a'),lambda:self.fixture.complete('b')):
+ with self.assertRaises(ValueError):method()
+ self.assertEqual(len(self.rec.calls),count)
+
+ def test_wrong_part_ack_or_reused_upload_identity_is_not_accepted(self):
+ self.fixture.seed();self.rec.corrupt=lambda op,r:{**r,'ETag':'"'+'0'*32+'"'} if op=='upload_part' else r
+ with self.assertRaisesRegex(ValueError,'exact submitted'):self.fixture.upload('a',3)
+ self.assertEqual(len(self.fixture.items['a']['parts']),2)
+ with tempfile.TemporaryDirectory() as tmp:
+ rec=Recorder(tmp);fixture=m.MpuRaceFixture(rec,'rv-g05-owned')
+ rec.corrupt=lambda op,r:{**r,'UploadId':'same'} if op=='create_multipart_upload' else r
+ fixture.create('committed')
+ fixture.items['committed']['state']='complete'
+ with self.assertRaisesRegex(ValueError,'reused UploadId'):fixture.create('a')
+
+ def test_s3_oracle_rejects_sibling_loss_part_corruption_and_version_resurrection(self):
+ self.fixture.seed()
+ for kind in ('upload','part','version','marker','head','get','terminal-error'):
+ def corrupt(op,r):
+ r=copy.deepcopy(r)
+ if kind=='upload' and op=='list_multipart_uploads':r['Uploads'].pop()
+ if kind=='part' and op=='list_parts' and r.get('Parts'):r['Parts'][0]['ETag']='wrong'
+ if kind=='version' and op=='list_object_versions':r['Versions'].append(copy.deepcopy(r['Versions'][0]))
+ if kind=='marker' and op=='list_object_versions':r['DeleteMarkers']=[{'Key':m.KEY}]
+ if kind=='head' and op=='head_object':r['Metadata']={}
+ if kind=='get' and op=='get_object':r['VersionId']=str(uuid.UUID(int=999))
+ if kind=='terminal-error' and op=='list_parts' and 'Error' in r:r['Error']['Code']='NoSuchKey'
+ return r
+ self.rec.corrupt=corrupt
+ with self.subTest(kind=kind),self.assertRaises(ValueError):self.fixture.verify_s3(self.rec)
+
+ def test_manifest_persistence_failure_stops_later_mutation(self):
+ self.fixture.seed()
+ with patch.object(self.fixture,'_persist',side_effect=OSError('full')):
+ with self.assertRaises(OSError):self.fixture.upload('a',3)
+ self.assertIsNotNone(self.fixture.poisoned)
+ count=len(self.rec.calls)
+ with self.assertRaises(ValueError):self.fixture.abort_b()
+ self.assertEqual(count,len(self.rec.calls))
+
+ def test_multipart_digest_requires_all_ordered_plain_parts(self):
+ parts=[{'number':n,'etag':m.etag(m.payload('rv-owned','a',n))} for n in (1,2,3)]
+ self.assertTrue(m.multipart_etag(parts).endswith('-3"'))
+ for bad in (parts[:2],parts[::-1],[{**p,'etag':'wrong'} for p in parts]):
+ with self.assertRaises(ValueError):m.multipart_etag(bad)
+
+ def test_pagination_rejects_missing_typed_flags_missing_cursor_and_cycles(self):
+ recorder=Mock();recorder.call.return_value={}
+ with self.assertRaises(ValueError):m.pages(recorder,'list_parts',{})
+ recorder.call.return_value={'IsTruncated':True,'NextPartNumberMarker':1}
+ with self.assertRaisesRegex(ValueError,'advance'):m.pages(recorder,'list_parts',{})
+ recorder.call.return_value={'IsTruncated':True,'NextKeyMarker':m.KEY,'NextUploadIdMarker':'u'}
+ with self.assertRaisesRegex(ValueError,'repeated'):m.pages(recorder,'list_multipart_uploads',{})
+ recorder.call.side_effect=[{'IsTruncated':True,'NextPartNumberMarker':1},{'IsTruncated':False}]
+ self.assertEqual(len(m.pages(recorder,'list_parts',{})),2)
+
+
+class MigratedRecorderTests(unittest.TestCase):
+ def test_frozen_common_dm_sizes_are_retained(self):
+ self.assertEqual(m.PART_SIZES, (5 * 1024**2 + 17, 5 * 1024**2 + 93, 1024**2 + 47))
+
+ def test_model_uses_actual_auto_test_request_ack_ledger_and_halts_unknown_writes(self):
+ with tempfile.TemporaryDirectory() as folder, patch.object(m, 'PART_SIZES', (12, 13, 14)):
+ root = Path(folder)
+ backend_dir = root / 'backend'
+ backend_dir.mkdir()
+ backend = Recorder(backend_dir)
+ class Probe:
+ binding = {'runId': 'gh-12345-1-h1', 'bucketPrefix': 'rv-g05-owned',
+ 'maxInventoryBytes': 64 * 1024**2}
+ fail_upload = False
+ def s3(self, operation, params, body=None):
+ if self.fail_upload and operation == 'upload_part':
+ return {'operation': operation, 'params': copy.deepcopy(params),
+ 'acknowledged': False, 'resultUnknown': True,
+ 'error': {'type': 'TimeoutError', 'message': 'unknown write'}}
+ backend.call(operation, params, body)
+ observation = json.loads((backend.output / f'{backend.sequence:06d}-ack.json').read_text())['observation']
+ status = observation['response']['ResponseMetadata']['HTTPStatusCode']
+ return {**observation, 'operation': operation, 'acknowledged': 200 <= status < 300,
+ 'resultUnknown': False}
+ probe = Probe()
+ recorder = DurableRecorder(probe, root / 'durable')
+ fixture = m.MpuRaceFixture(recorder, 'rv-g05-owned')
+ fixture.seed()
+ self.assertTrue(fixture.verify_s3(recorder)['sameKeySiblingIsolationVerified'])
+ probe.fail_upload = True
+ with self.assertRaisesRegex(ValueError, 'unexpected upload_part'):
+ fixture.upload('a', 3)
+ sequence = recorder.sequence
+ with self.assertRaisesRegex(ValueError, 'halted'):
+ fixture.abort_b()
+ self.assertEqual(recorder.sequence, sequence)
+ ack = json.loads((recorder.output / f'{sequence:06d}-ack.json').read_text())
+ request = recorder.output / ack['requestFile']
+ self.assertEqual(ack['requestSha256'], hashlib.sha256(request.read_bytes()).hexdigest())
+ self.assertEqual(ack['identity'], {'mpuRole': 'a'})
+ self.assertEqual(ack['operationCheck'], 'FAIL')
+ self.assertTrue(ack['observation']['resultUnknown'])
+ self.assertEqual(fixture.items['b']['state'], 'pending')
+ self.assertEqual(fixture.items['committed']['state'], 'complete')
+ self.assertEqual(fixture.events[-1]['state'], 'UNRESOLVED')
+
+
+if __name__=='__main__':unittest.main()
diff --git a/test_validation_heal_mpu_race_physical.py b/test_validation_heal_mpu_race_physical.py
new file mode 100644
index 00000000..6b0d0859
--- /dev/null
+++ b/test_validation_heal_mpu_race_physical.py
@@ -0,0 +1,161 @@
+"""Real read-only G05 filesystem census and cross-host continuation oracles."""
+import base64
+import copy
+import hashlib
+import json
+import os
+from pathlib import Path
+import tempfile
+import unittest
+from unittest.mock import Mock, patch
+from types import SimpleNamespace
+import uuid
+
+from scripts.validation_heal import fleet_node, mpu_race_physical as scene
+from scripts.validation_heal.healthy import Fleet
+
+
+class MultipartRacePhysicalTests(unittest.TestCase):
+ def test_actual_remote_adapter_embeds_the_local_module(self):
+ def execute(*args, **kwargs):
+ source = kwargs['input'].rsplit('\nmain(', 1)[0]
+ scope = {'__name__': '__g05_node_test__'}
+ exec(compile(source, 'g05-node', 'exec'), scope)
+ self.assertTrue(callable(scope['mpu_race_backend'].run))
+ self.assertEqual(scope['mpu_race_backend'].KEY, scene.KEY)
+ return SimpleNamespace(returncode=0, stdout='{}', stderr='')
+ with tempfile.TemporaryDirectory() as d, patch('scripts.validation_heal.healthy.subprocess.run', side_effect=execute):
+ Fleet(['vm000'], Path(d), 'gh-12345-1-h1').remote('vm000', 'mpu-race-scene')
+
+ def fixture(self, root, *, live=False):
+ root = root.resolve()
+ state = root / 'state'; state.mkdir()
+ binary = b'exact owned G05 candidate'
+ (state / 'rustfs').write_bytes(binary)
+ owner = {'runtimeProfile': 'manual', 'bucket': 'rv-heal-0123456789abcdef',
+ 'binarySha256': hashlib.sha256(binary).hexdigest()}
+ items = []
+ for i, role in enumerate(scene.ROLES):
+ raw = f'{uuid.UUID(int=9)}.{uuid.UUID(int=i+1)}x123456'.encode()
+ items.append({'role': role, 'uploadId': base64.urlsafe_b64encode(raw).decode().rstrip('='),
+ 'bucket': owner['bucket'], 'key': scene.KEY})
+ selected = scene.selected(owner['bucket'], items)
+ roots, raw = tuple(root / f'disk-{i}' for i in range(4)), {}
+ for disk, folder in enumerate(roots):
+ (folder / 'data').mkdir(parents=True)
+ for item in selected:
+ if item['role'] == 'committed': continue
+ upload = folder / 'data' / item['relative']
+ upload.mkdir(parents=True)
+ p = upload / 'xl.meta'; p.write_bytes(b'owned pending metadata'); raw[p] = p.read_bytes()
+ for number in range(1, 3 if item['role'] == 'a' else 4):
+ part = upload / str(uuid.UUID(int=100 if item['role'] == 'a' else 200)) / f'part.{number}'
+ part.parent.mkdir(exist_ok=True)
+ body = (f'{item["role"]}/{number}/{disk}'.encode() * 20)
+ part.write_bytes(body); raw[part] = body
+ sidecar = part.with_name(part.name + '.meta')
+ sidecar.write_bytes(b'original part ACK metadata'); raw[sidecar] = sidecar.read_bytes()
+ (state / 'credentials.env').write_bytes(b'never export this secret')
+ process = {k: k for k in fleet_node.PROCESS_FIELDS}
+ process.update(pid=1234, binarySha256=owner['binarySha256'])
+ def stopped(value):
+ if live: raise ValueError('owned service active')
+ node = {'owned': lambda run: (state, roots, owner), 'checked': fleet_node.checked,
+ 'digest': fleet_node.digest, 'stopped': stopped,
+ 'process': lambda request: copy.deepcopy(process), 'PROCESS_FIELDS': fleet_node.PROCESS_FIELDS}
+ request = {'runId': 'gh-12345-1-h1', 'action': 'mpu-race-scene', 'bucket': owner['bucket'],
+ 'items': items, 'expectedBinarySha256': owner['binarySha256'], 'quiescent': not live}
+ manifest = {'bucket': owner['bucket'], 'key': scene.KEY, 'items': {
+ item['role']: {'uploadId': item['uploadId'], 'state': 'complete' if item['role'] == 'committed' else 'pending',
+ 'parts': [{'number': n} for n in range(1, 3 if item['role'] == 'a' else 4)]}
+ for item in items}}
+ return request, node, roots, raw, manifest
+
+ def test_real_census_hashes_only_three_ack_selected_scopes_and_preserves_all_bytes(self):
+ for live in (False, True):
+ with self.subTest(live=live), tempfile.TemporaryDirectory() as d:
+ request, node, roots, raw, manifest = self.fixture(Path(d), live=live)
+ real_open, opened = os.open, []
+ def checked_open(path, *args, **kwargs):
+ self.assertIn(Path(path), raw); opened.append(Path(path))
+ return real_open(path, *args, **kwargs)
+ with patch.object(scene.os, 'open', side_effect=checked_open):
+ value = scene.run(request, node)
+ self.assertEqual(len(value['rows']), 12)
+ self.assertEqual(len(opened), 48)
+ self.assertEqual(value['bytes'], sum(len(body) for body in raw.values()))
+ self.assertEqual(value['quiescent'], not live)
+ self.assertEqual({p: p.read_bytes() for p in raw}, raw)
+ self.assertNotIn('never export', json.dumps(value))
+ proof = scene.verify(manifest, {'vm000': value}, ['vm000'], request['expectedBinarySha256'])
+ self.assertEqual(proof['pendingAndTerminalWitnesses'], 12)
+ self.assertFalse(proof['codingLayoutProved'] or proof['canonicalCasePassClaimed'])
+
+ def test_cross_host_sibling_and_terminal_continuity_requires_every_original_drive(self):
+ with tempfile.TemporaryDirectory() as d:
+ request, node, roots, raw, manifest = self.fixture(Path(d))
+ value = scene.run(request, node)
+ nodes = [f'vm00{i}' for i in range(4)]
+ baseline = {n: copy.deepcopy(value) for n in nodes}
+ continued = copy.deepcopy(baseline)
+ manifest['items']['a']['parts'].append({'number': 3})
+ for n, observed in continued.items():
+ for row in observed['rows']:
+ if row['role'] != 'a': continue
+ for suffix in ['', '.meta']:
+ body = f'A/3/{n}/{row["disk"]}/{suffix}'.encode()
+ row['files'][f'{uuid.UUID(int=100)}/part.3{suffix}'] = {
+ 'size': 64 if not suffix else len(body), 'sha256': hashlib.sha256(body).hexdigest()}
+ scene.verify(manifest, continued, nodes, request['expectedBinarySha256'], previous=baseline)
+ committed, completed = {n: {'files': []} for n in nodes}, copy.deepcopy(continued)
+ manifest['items']['a']['state'] = 'complete'; manifest['items']['b']['state'] = 'aborted'
+ for n, observed in completed.items():
+ for prefix in observed['keyPrefixes']: prefix['children'] = []
+ for row in observed['rows']:
+ if row['role'] == 'a':
+ for path, entry in row['files'].items():
+ if path.endswith(('/part.1', '/part.2', '/part.3')):
+ committed[n]['files'].append({'disk': row['disk'], 'path': manifest['bucket']+'/'+scene.KEY+'/'+path, **entry})
+ row.update(exists=False, files={}, directories=[])
+ proof = scene.verify(manifest, completed, nodes, request['expectedBinarySha256'], previous=continued, committed=committed)
+ self.assertEqual(proof['completedMembersCompared'], 16)
+ for fault in ('host', 'drive', 'duplicate', 'retired', 'candidate', 'completed-bytes', 'unlisted-upload'):
+ broken, live = copy.deepcopy(completed), copy.deepcopy(committed)
+ if fault == 'host': broken.pop('vm003')
+ elif fault == 'drive': broken['vm000']['rows'].pop()
+ elif fault == 'duplicate': broken['vm000']['rows'][-1] = copy.deepcopy(broken['vm000']['rows'][0])
+ elif fault == 'retired': broken['vm000']['rows'][1]['exists'] = True
+ elif fault == 'candidate': broken['vm000']['binarySha256'] = 'f'*64
+ elif fault == 'unlisted-upload': broken['vm000']['keyPrefixes'][0]['children'].append('unknown-upload')
+ else: live['vm000']['files'][0]['sha256'] = 'f'*64
+ with self.subTest(fault=fault), self.assertRaises(ValueError):
+ scene.verify(manifest, broken, nodes, request['expectedBinarySha256'], previous=continued, committed=live)
+
+ def test_scope_escape_links_budgets_and_changed_process_are_rejected(self):
+ for fault in ('role', 'bucket', 'runtime', 'binary', 'unknown-field', 'upload-path',
+ 'symlink', 'hardlink', 'fifo', 'entries', 'bytes', 'process', 'unlisted-upload'):
+ with self.subTest(fault=fault), tempfile.TemporaryDirectory() as d:
+ request, node, roots, raw, manifest = self.fixture(Path(d), live=fault=='process')
+ part = next(p for p in raw if p.name=='part.1')
+ if fault=='role': request['items'][1]['role']='committed'
+ elif fault=='bucket': request['bucket']='rv-heal-ffffffffffffffff'
+ elif fault=='runtime': node['owned'](request['runId'])[2]['runtimeProfile']='lock-quorum'
+ elif fault=='binary': request['expectedBinarySha256']='f'*64
+ elif fault=='unknown-field': request['path']='/etc'
+ elif fault=='upload-path': request['items'][1]['uploadId']='../outside'
+ elif fault=='symlink': part.unlink(); part.symlink_to(Path(d).resolve()/'state/credentials.env')
+ elif fault=='hardlink': os.link(part,part.with_name('extra-link'))
+ elif fault=='fifo': part.unlink();os.mkfifo(part)
+ elif fault=='unlisted-upload':
+ selected=scene.selected(request['bucket'],request['items'])
+ (roots[0]/'data'/selected[0]['relative']).parent.joinpath('unknown-upload').mkdir()
+ elif fault=='process':
+ first=node['process'](request);second={**first,'startTicks':'another birth'}
+ node['process']=Mock(side_effect=[first,second])
+ with patch.object(scene,'MAX_ENTRIES',1 if fault=='entries' else scene.MAX_ENTRIES), \
+ patch.object(scene,'MAX_BYTES',1 if fault=='bytes' else scene.MAX_BYTES):
+ with self.assertRaises(ValueError): scene.run(request,node)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/test_validation_heal_mpu_race_runner.py b/test_validation_heal_mpu_race_runner.py
new file mode 100644
index 00000000..1fa8a086
--- /dev/null
+++ b/test_validation_heal_mpu_race_runner.py
@@ -0,0 +1,309 @@
+"""Actual controller/Recorder boundaries; synthetic SDK/physical data is not live proof."""
+import copy
+from contextlib import ExitStack, nullcontext
+import hashlib
+import json
+import os
+from pathlib import Path
+import tempfile
+import threading
+import time
+from types import SimpleNamespace
+import unittest
+from unittest.mock import Mock, patch
+import uuid
+
+import test_validation_heal_mpu_race_fixture as model_fixtures
+import test_validation_heal_status_keepalive as transport_fixtures
+from scripts.validation_heal import mpu_race_runner as runner, mpu_race_fixture as model, race
+from scripts.validation_heal.heal_probe import CLOCK_DOMAIN, save
+from scripts.validation_heal.heal_workload import Recorder
+from scripts.validation_heal.healthy import Fleet
+
+
+class SDKProbe:
+ def __init__(self, root):
+ self.binding = transport_fixtures.binding()
+ self.sdk = model_fixtures.Recorder(root);Path(root).mkdir()
+ self.calls = [];self.gate = None;self.fail = None;self.control = 500;self.controls = {}
+
+ def s3(self, operation, params, body=None):
+ started = time.monotonic_ns();self.calls.append((operation,copy.deepcopy(params)))
+ if self.gate and self.gate[0] == operation:
+ _,entered,release = self.gate;self.gate = None;entered.set()
+ if not release.wait(5): raise TimeoutError('component gate not released')
+ if self.fail == operation: raise OSError('lost original response; do not retry')
+ if operation in {'create_bucket','put_bucket_versioning'}:
+ response = {'ResponseMetadata':{'HTTPStatusCode':200}}
+ elif operation == 'put_object':
+ self.control += 1
+ response = {'ResponseMetadata':{'HTTPStatusCode':200},'ETag':model.etag(body),
+ 'VersionId':str(uuid.UUID(int=self.control))}
+ self.controls[params['Key']] = {'key':params['Key'],'versionId':response['VersionId'],
+ 'isLatest':True,'deleteMarker':False,'payload':{'size':len(body),'sha256':hashlib.sha256(body).hexdigest()}}
+ else:
+ response = self.sdk.call(operation,params,body)
+ result = {'operation':operation,'params':copy.deepcopy(params),'response':response,
+ 'acknowledged':200 <= response['ResponseMetadata']['HTTPStatusCode'] < 300,'resultUnknown':False,
+ 'clockDomain':CLOCK_DOMAIN,'callStartedMonotonicNs':started,'callFinishedMonotonicNs':time.monotonic_ns(),
+ 'clientCallInvoked':True}
+ if operation == 'get_object':
+ version = next(v for v in self.sdk.versions if v['VersionId']==params['VersionId']) if 'VersionId' in params else self.sdk.versions[-1]
+ result['payload'] = {'size':len(version['body']),'sha256':hashlib.sha256(version['body']).hexdigest()}
+ return result
+
+ def observation_for(self, sample): return {'phases':{}}
+
+ def inventory(self, bucket):
+ entries = copy.deepcopy(list(self.controls.values()))
+ for version in self.sdk.versions:
+ entries.append({'key':model.KEY,'versionId':version['VersionId'],'isLatest':version is self.sdk.versions[-1],
+ 'deleteMarker':False,'payload':{'size':len(version['body']),'sha256':hashlib.sha256(version['body']).hexdigest()}})
+ return {'bucket':bucket,'entries':entries}
+
+ def status_session(self, token, path): return nullcontext({'componentOnly':True})
+
+
+class MpuRunnerTests(unittest.TestCase):
+ def controller(self, directory, *, fail_stage=None, overlapped=True):
+ """Exercise the actual controller, durable SDK model and final diagnostics.
+
+ Physical/terminal oracles are mocked here; their full-byte tests remain
+ independent. This fixture proves ordering and cleanup authority only.
+ """
+ root = Path(directory);candidate=root/'candidate.json';rc=root/'rc'
+ candidate.write_text(json.dumps({'source_sha':race.COPY_CANDIDATE,'package_sha256':'a'*64}));rc.write_bytes(b'client')
+ probe=SDKProbe(root/'sdk');trace=[];owned=[]
+ args=SimpleNamespace(case_id=runner.CASE,candidate=candidate,evidence=root/'evidence',tool_directory=root,
+ testing_commit='b'*40,binary_sha='c'*64,rc_bin=rc)
+ scene={'vm000':{'files':[],'formats':[]}}
+ def fleet_factory(*positional,**kwargs):
+ fleet=Fleet(*positional,**kwargs);owned.append(fleet)
+ fleet.deploy=Mock(side_effect=lambda *a:trace.append('deploy'))
+ fleet.stop=Mock(side_effect=lambda:trace.append('stop'))
+ fleet.start=Mock(side_effect=lambda:trace.append('start'))
+ fleet.snapshot=Mock(side_effect=lambda label,**kw:(trace.append(label) or scene))
+ def remote(node,action,**kw):
+ if action == 'apply-fault':
+ self.assertEqual(node,'vm000');self.assertEqual(kw['disk'],3)
+ self.assertRegex(kw['faultId'],r'^G05-[0-9a-f]{32}$')
+ trace.append(action)
+ return {}
+ fleet.remote=Mock(side_effect=remote)
+ fleet.cleanup=Mock(return_value=True)
+ return fleet
+ def probe_factory(binding,endpoint=None):
+ probe.binding=binding;return probe
+ probe.admin=Mock(return_value={'httpStatus':200,'response':{'clientToken':transport_fixtures.TOKEN}})
+ real_inventory=probe.inventory
+ def inventory(bucket):
+ trace.append('inventory');return real_inventory(bucket)
+ probe.inventory=inventory
+ real_verify=runner.model.MpuRaceFixture.verify_s3
+ def verify_s3(fixture,reader):
+ trace.append('model-S3');return real_verify(fixture,reader)
+ def mapped_controls(snapshot,bucket,controls):
+ return {(row['key'],row['versionId']):{**row,'node':'vm000','disk':3,'relativePath':row['key']+'/part.1','sha256':'d'*64}
+ for row in controls}
+ def mapped_mpu(snapshot,bucket,entries,**kw):
+ return {(row['versionId'],part):{**row,'dataDir':'old-dir','cohort':[{'node':'vm000','disk':3,
+ 'relativePath':model.KEY+'/part.'+str(part),'sha256':'d'*64}]}
+ for row in entries for part in (1,2,3)},{}
+ def committed(*a):
+ trace.append('committed-physical')
+ if fail_stage=='committed':raise ValueError('committed proof refused')
+ return {},{'physicalComponentOnly':True}
+ def streams(fleet,parts,label,**kw):
+ trace.append(label);self.assertEqual(kw['count'],259 if label=='baseline-streaming' else 262)
+ if fail_stage=='streams-io' and label=='final-streaming':raise OSError('evidence fsync failed')
+ return {'count':kw['count']}
+ def execute(fixture,prepared,*unused):
+ trace.append('foreground')
+ result=runner.schedule.execute(fixture,prepared['branch'],prepared,
+ lambda label,manifest:{'manifest':manifest,'physical':{'componentOnly':True}})
+ return result,overlapped
+ def terminal_proof(*a):trace.append('terminal-proof');return {'componentOnly':True}
+ def roots(*a,**kw):
+ trace.append('root-retirement')
+ return {'.rustfs.sys/root-heal-'+transport_fixtures.TOKEN+'.json':{}} if fail_stage=='root' else {}
+ with ExitStack() as stack:
+ stack.enter_context(patch.dict(os.environ,{'GITHUB_RUN_ID':'12345','GITHUB_RUN_ATTEMPT':'1'}))
+ patches=[(runner,'Fleet',fleet_factory),(runner,'KeepAliveStatusProbe',probe_factory),(runner,'Probe',probe_factory),
+ (runner,'verify_inspector',lambda *a:{'binarySha256':'e'*64}),(runner,'validate_topology',Mock()),
+ (runner,'physical_population',Mock()),(runner.race,'map_fixture',mapped_controls),(runner.business,'mapped',mapped_mpu),
+ (runner,'verify_part_manifest',streams),(runner.staging,'verify',Mock(return_value={})),
+ (runner,'validate_missing_batch',Mock()),(runner,'observe_io',lambda *a:trace.append('io-observe')),
+ (runner,'observe_status',Mock()),(runner,'foreground',execute),(runner,'wait_terminal',lambda *a:{'summary':'finished'}),
+ (runner,'observe_terminal',lambda *a:{'response':{'summary':'finished'}}),
+ (runner,'validate_repaired_physical',Mock()),(runner.business,'transition',committed),
+ (runner.race,'terminal_proof',terminal_proof),(runner,'journal_records',Mock()),(runner,'exact_records',roots),
+ (runner.model.MpuRaceFixture,'verify_s3',verify_s3),(model,'PART_SIZES',(12,13,14)),(race,'SIZE',31)]
+ for target,name,value in patches:stack.enter_context(patch.object(target,name,value))
+ result=runner.run_branch(args,'H1',['vm000'],'upload-continue')
+ return result,owned[0],trace,probe
+
+ def test_complete_controller_proves_physical_before_final_get_and_only_then_cleans(self):
+ with tempfile.TemporaryDirectory() as d:
+ result,fleet,trace,probe=self.controller(d)
+ self.assertEqual((result['verdict'],result['cleanup']),('PASS','PASS'))
+ self.assertFalse(result['canonicalCasePassClaimed']);fleet.cleanup.assert_called_once()
+ self.assertEqual(trace.count('apply-fault'),257)
+ self.assertEqual(trace.count('io-observe'),2)
+ self.assertLess(trace.index('committed-physical'),trace.index('final-streaming'))
+ self.assertLess(trace.index('final-streaming'),trace.index('model-S3',trace.index('foreground')))
+ self.assertLess(trace.index('physical-after-S3'),trace.index('terminal-proof'))
+ self.assertLess(trace.index('terminal-proof'),trace.index('root-retirement'))
+ self.assertFalse(fleet.faults)
+
+ def test_assertion_failure_collects_independent_final_diagnostics_and_retains_all_acks(self):
+ for stage in ('committed','root'):
+ with self.subTest(stage=stage),tempfile.TemporaryDirectory() as d:
+ result,fleet,trace,probe=self.controller(d,fail_stage=stage)
+ self.assertEqual((result['verdict'],result['cleanup']),('FAIL','FAIL'))
+ fleet.cleanup.assert_not_called();self.assertEqual(len(fleet.faults),257)
+ self.assertIn('final-streaming',trace);self.assertIn('terminal-proof',trace);self.assertIn('root-retirement',trace)
+ self.assertEqual(trace[-1],'mpu-scene')
+ acks=sorted((fleet.evidence/'foreground-ACKs').glob('*-ack.json'))
+ self.assertEqual(len(acks),3)
+ self.assertEqual(json.loads(acks[-1].read_text())['observation']['operation'],'abort_multipart_upload')
+
+ def test_evidence_io_failure_stops_final_diagnostics_and_never_authorizes_cleanup(self):
+ with tempfile.TemporaryDirectory() as d:
+ result,fleet,trace,_=self.controller(d,fail_stage='streams-io')
+ self.assertEqual((result['verdict'],result['cleanup']),('FAIL','FAIL'))
+ self.assertEqual(result['failure']['type'],'OSError');fleet.cleanup.assert_not_called()
+ self.assertNotIn('terminal-proof',trace);self.assertIn('mpu-scene',trace)
+
+ def test_missing_overlap_can_clean_only_after_all_physical_s3_and_root_proofs(self):
+ with tempfile.TemporaryDirectory() as d:
+ result,fleet,trace,_=self.controller(d,overlapped=False)
+ self.assertEqual((result['verdict'],result['cleanup']),('NOT_OBSERVED','PASS'))
+ self.assertIn('root-retirement',trace);fleet.cleanup.assert_called_once();self.assertFalse(fleet.faults)
+
+ def test_real_seed_and_durable_pairing_preserve_all_source_controls_and_same_key_uploads(self):
+ for branch in runner.schedule.SELECTED:
+ with self.subTest(branch=branch),tempfile.TemporaryDirectory() as d, \
+ patch.object(model,'PART_SIZES',(12,13,14)),patch.object(race,'SIZE',31):
+ probe = SDKProbe(Path(d)/'sdk');recorder = Recorder(probe,Path(d)/'seed')
+ controls,fixture,prepared = runner.seed(recorder,probe.binding['bucketPrefix'],branch)
+ self.assertEqual([row['key'] for row in controls],[f'objects-{i:04d}.bin' for i in range(256)])
+ self.assertEqual(len(runner.ack_records(recorder)),270+int(branch!='upload-continue'))
+ self.assertEqual([len(prepared['manifest']['items'][r]['parts']) for r in model.ROLES],
+ [3,2 if branch=='upload-continue' else 3,3])
+ self.assertEqual(len({item['uploadId'] for item in fixture.items.values()}),3)
+ self.assertTrue(all(params.get('Key') == model.KEY for op,params in probe.calls if 'multipart' in op or op=='upload_part'))
+
+ def test_original_sdk_pairing_rejects_orphans_changed_requests_or_unknown_results(self):
+ for fault in ('request','ack','sha','sequence','operation','params','unknown'):
+ with self.subTest(fault=fault),tempfile.TemporaryDirectory() as d:
+ probe=SDKProbe(Path(d)/'sdk');recorder=Recorder(probe,Path(d)/'seed')
+ recorder.call('create_bucket',{'Bucket':probe.binding['bucketPrefix']})
+ req=recorder.output/'000001-request.json';ack=recorder.output/'000001-ack.json'
+ if fault=='request':req.unlink()
+ elif fault=='ack':ack.unlink()
+ else:
+ value=json.loads(ack.read_text())
+ if fault=='sha':value['requestSha256']='f'*64
+ elif fault=='sequence':value['sequence']=2
+ else:value['observation'][{'operation':'operation','params':'params','unknown':'resultUnknown'}[fault]]=True if fault=='unknown' else 'foreign'
+ ack.write_text(json.dumps(value))
+ with self.assertRaises(ValueError):runner.ack_records(recorder)
+
+ def test_unknown_selected_foreground_result_halts_all_companions_without_retry(self):
+ for branch,operation in runner.schedule.SELECTED.items():
+ with self.subTest(branch=branch),tempfile.TemporaryDirectory() as d, \
+ patch.object(model,'PART_SIZES',(12,13,14)),patch.object(race,'SIZE',31):
+ root=Path(d);probe=SDKProbe(root/'sdk');seed=Recorder(probe,root/'seed')
+ _,fixture,prepared=runner.seed(seed,probe.binding['bucketPrefix'],branch)
+ fixture.recorder=Recorder(probe,root/'foreground');probe.fail=operation
+ original_calls=len(probe.calls)
+ fleet=SimpleNamespace(nodes=['vm000'],evidence=root,record=Mock())
+ with patch.object(runner,'observe_status',return_value={}),patch.object(runner,'capture_staging') as capture:
+ with self.assertRaises(ValueError):
+ runner.foreground(fixture,prepared,probe,fleet,'path','token',race.COPY_REFERENCE,'a'*64)
+ self.assertEqual([op for op,_ in probe.calls[original_calls:]],[operation]);capture.assert_not_called()
+ self.assertIsNotNone(fixture.poisoned)
+ ack=json.loads((root/'foreground/000001-ack.json').read_text())
+ self.assertTrue(ack['observation']['resultUnknown']);self.assertEqual(ack['operationCheck'],'FAIL')
+
+ def test_stopped_original_root_reaches_final_diagnostics_without_becoming_success(self):
+ probe=Mock();fleet=Mock();fleet.record.side_effect=lambda label,value:value
+ probe.admin.return_value={'httpStatus':200,'response':{'summary':'stopped','settings':race.COPY_REFERENCE['effective']}}
+ with patch.object(runner.time,'sleep',side_effect=AssertionError('stopped root must drain immediately')):
+ terminal=runner.wait_terminal(probe,fleet,'path','token',race.COPY_REFERENCE)
+ self.assertEqual(terminal['summary'],'stopped')
+ with self.assertRaisesRegex(ValueError,'terminal coverage'):
+ race.terminal_proof(terminal,'bucket',{'entries':[]},{'entries':[]})
+
+ def test_selected_first_sdk_overlap_does_not_require_later_companions_to_overlap(self):
+ for branch in runner.schedule.SELECTED:
+ with self.subTest(branch=branch),tempfile.TemporaryDirectory() as d, \
+ patch.object(model,'PART_SIZES',(12,13,14)),patch.object(race,'SIZE',31):
+ root=Path(d);probe=SDKProbe(root/'sdk');seed=Recorder(probe,root/'seed')
+ _,fixture,prepared=runner.seed(seed,probe.binding['bucketPrefix'],branch)
+ fixture.recorder=Recorder(probe,root/'foreground')
+ entered,release=threading.Event(),threading.Event();probe.gate=(prepared['selectedOperation'],entered,release)
+ fleet=SimpleNamespace(nodes=['vm000'],evidence=root,record=Mock(side_effect=lambda label,value:value))
+ path='/rustfs/admin/v3/heal/'+probe.binding['bucketPrefix'];token=str(uuid.UUID(int=1))
+ def observed(*args):
+ self.assertTrue(entered.wait(5));started=time.monotonic_ns();finished=time.monotonic_ns()
+ release.set()
+ return {'path':path,'query':[('clientToken',token)],'httpStatus':200,'clockDomain':CLOCK_DOMAIN,
+ 'callStartedMonotonicNs':started,'callFinishedMonotonicNs':finished,
+ 'statusObservation':{'sessionId':'component','sequence':1},
+ 'response':{'summary':'running','settings':race.COPY_REFERENCE['effective'],
+ 'outcome':{'counters':{'processed':1}}}}
+ with patch.object(runner,'observe_status',side_effect=observed),patch.object(runner,'capture_staging',return_value={'mock':True}):
+ result,overlapped=runner.foreground(fixture,prepared,probe,fleet,path,token,race.COPY_REFERENCE,'a'*64)
+ self.assertTrue(overlapped)
+ acks=runner.ack_records(fixture.recorder)
+ self.assertEqual(result['selectedAckSequence'],1)
+ proof=fleet.record.call_args.args[1]
+ self.assertEqual(proof['selectedOperation'],prepared['selectedOperation'])
+ self.assertEqual(len(acks),3 if branch=='upload-continue' else 2)
+ self.assertTrue(proof['allCompanionACKsVerified']);self.assertFalse(proof['nativeFenceProved'])
+ self.assertEqual((fixture.items['a']['state'],fixture.items['b']['state']),('complete','aborted'))
+
+ def test_seed_unknown_response_preserves_stopped_raw_scene_without_abort_restore_or_cleanup(self):
+ with tempfile.TemporaryDirectory() as d:
+ root=Path(d);candidate=root/'candidate.json';candidate.write_text(json.dumps({'source_sha':race.COPY_CANDIDATE,'package_sha256':'a'*64}))
+ rc=root/'rc';rc.write_bytes(b'owned client');probe=SDKProbe(root/'sdk');probe.fail='create_multipart_upload'
+ fleet=Mock();fleet.nodes=['vm000'];fleet.record.side_effect=lambda label,value:value
+ fleet.remote.return_value={'readOnly':True};fleet.snapshot.return_value={'vm000':{'files':[],'formats':[]}}
+ args=SimpleNamespace(case_id=runner.CASE,candidate=candidate,evidence=root/'evidence',tool_directory=root,
+ testing_commit='b'*40,binary_sha='c'*64,rc_bin=rc)
+ def failed_seed(recorder,bucket,branch):
+ recorder.probe.binding['bucketPrefix']=bucket
+ recorder.call('create_multipart_upload',{'Bucket':bucket,'Key':model.KEY})
+ with patch.dict(os.environ,{'GITHUB_RUN_ID':'12345','GITHUB_RUN_ATTEMPT':'1'}), \
+ patch.object(runner,'Fleet',return_value=fleet),patch.object(runner,'KeepAliveStatusProbe',return_value=probe), \
+ patch.object(runner,'verify_inspector',return_value={}),patch.object(runner,'validate_topology'), \
+ patch.object(runner,'seed',side_effect=failed_seed):
+ result=runner.run_branch(args,'H1',['vm000'],'upload-continue')
+ self.assertEqual((result['verdict'],result['cleanup']),('FAIL','FAIL'))
+ fleet.cleanup.assert_not_called();fleet.start.assert_not_called()
+ self.assertEqual([op for op,_ in probe.calls],['create_multipart_upload'])
+ self.assertTrue(any(call.args[:2] == ('vm000','mpu-scene') for call in fleet.remote.call_args_list))
+ self.assertTrue(any(call.args[0]=='last-ACK-rescue-required' for call in fleet.record.call_args_list))
+
+
+class DurableStatusTests(unittest.TestCase):
+ # Reuse the actual socket fixture; only these new integration checks run here.
+ setUp = transport_fixtures.KeepaliveTests.setUp
+ tearDown = transport_fixtures.KeepaliveTests.tearDown
+
+ def test_g05_status_persists_original_request_before_send_and_response_after_receive(self):
+ with tempfile.TemporaryDirectory() as d,patch.object(race,'settings'), \
+ patch('scripts.validation_heal.heal_probe.os.fsync',wraps=os.fsync) as sync, \
+ patch.dict(os.environ,{'HEAL_ACCESS_KEY':'local-access','HEAL_SECRET_KEY':'local-secret'}):
+ fleet=Fleet(['vm000'],Path(d),'gh-12345-1-h1');path='/rustfs/admin/v3/heal/'+self.probe.binding['bucketPrefix']
+ with self.probe.status_session(transport_fixtures.TOKEN,path) as state:
+ value=runner.observe_status(self.probe,fleet,path,transport_fixtures.TOKEN,race.COPY_REFERENCE)
+ measured=state['observations'][0]
+ self.assertEqual(sync.call_count,4);self.assertTrue(measured['responsePersisted'])
+ self.assertLessEqual(measured['phases']['requestPersistence']['finishedMonotonicNs'],value['callStartedMonotonicNs'])
+ self.assertLessEqual(value['callFinishedMonotonicNs'],measured['phases']['responsePersistence']['startedMonotonicNs'])
+ request=Path(d)/measured['requestFile'];response=Path(d)/measured['responseFile']
+ self.assertEqual(hashlib.sha256(request.read_bytes()).hexdigest(),value['requestSha256'])
+ self.assertEqual(json.loads(response.read_text())['requestSha256'],value['requestSha256'])
+ self.assertNotIn('local-secret',json.dumps(state))
diff --git a/test_validation_heal_mpu_race_schedule.py b/test_validation_heal_mpu_race_schedule.py
new file mode 100644
index 00000000..67df9c3f
--- /dev/null
+++ b/test_validation_heal_mpu_race_schedule.py
@@ -0,0 +1,66 @@
+"""Original G05 operation order and halt boundaries through the migrated model."""
+import copy
+from pathlib import Path
+import tempfile
+import unittest
+from unittest.mock import patch
+
+import test_validation_heal_mpu_race_fixture as model_fixtures
+from scripts.validation_heal import mpu_race_fixture as model, mpu_race_schedule as schedule
+
+
+class MultipartScheduleTests(unittest.TestCase):
+ def test_each_branch_prepares_the_original_parts_before_foreground_and_keeps_both_final_versions(self):
+ for branch in schedule.SELECTED:
+ with self.subTest(branch=branch),tempfile.TemporaryDirectory() as tmp,patch.object(model,'PART_SIZES',(12,13,14)):
+ recorder=model_fixtures.Recorder(Path(tmp)/'seed');recorder.output.mkdir()
+ fixture=model.MpuRaceFixture(recorder,'rv-g05-owned');prepared=schedule.prepare(fixture,branch)
+ self.assertEqual([len(prepared['manifest']['items'][r]['parts']) for r in model.ROLES],
+ [3,2 if branch=='upload-continue' else 3,3])
+ fixture.verify_s3(recorder)
+ # Preparation and foreground use separate actual request sequences.
+ recorder.sequence=0;recorder.output=Path(tmp)/'foreground';recorder.output.mkdir();recorder.calls=[]
+ seen=[]
+ def checkpoint(label,manifest):
+ seen.append(label)
+ self.assertEqual(manifest,fixture.manifest())
+ return {'checkpointLabel':label,'readOnlyMock':True}
+ result=schedule.execute(fixture,branch,prepared,checkpoint)
+ wanted=(['upload_part','complete_multipart_upload','abort_multipart_upload'] if branch=='upload-continue'
+ else ['complete_multipart_upload','abort_multipart_upload'] if branch=='complete'
+ else ['abort_multipart_upload','complete_multipart_upload'])
+ self.assertEqual([c[0] for c in recorder.calls],wanted)
+ self.assertEqual(seen,['continued','completed','aborted'] if branch=='upload-continue'
+ else ['completed','aborted'] if branch=='complete' else ['aborted','completed'])
+ self.assertEqual(result['selectedAckSequence'],1)
+ self.assertEqual(result['selectedOperation'],wanted[0])
+ self.assertFalse(result['ioCapRequested'] or result['physicalRepairProved'] or result['publicOverlapProved']
+ or result['canonicalCasePassClaimed'])
+ proof=fixture.verify_s3(recorder)
+ self.assertEqual((proof['committedVersions'],proof['pendingUploads']),(2,0))
+
+ def test_missing_or_unknown_mutation_checkpoint_stops_every_later_companion(self):
+ for branch in schedule.SELECTED:
+ for failure in ('checkpoint','sdk'):
+ with self.subTest(branch=branch,failure=failure),tempfile.TemporaryDirectory() as tmp,patch.object(model,'PART_SIZES',(12,13,14)):
+ recorder=model_fixtures.Recorder(tmp);fixture=model.MpuRaceFixture(recorder,'rv-g05-owned')
+ prepared=schedule.prepare(fixture,branch);count=len(recorder.calls)
+ if failure=='sdk':recorder.fail=schedule.SELECTED[branch]
+ def failed_checkpoint(*args):raise ValueError('original physical witness unavailable')
+ with self.assertRaises(ValueError):schedule.execute(fixture,branch,prepared,failed_checkpoint)
+ self.assertEqual(len(recorder.calls),count+1)
+ self.assertIsNotNone(fixture.poisoned)
+ with self.assertRaises(ValueError):fixture.abort_b()
+ self.assertEqual(len(recorder.calls),count+1)
+
+ def test_foreign_preparation_branch_or_changed_model_is_refused_before_foreground_sdk(self):
+ for fault in ('branch','manifest','operation','callback'):
+ with self.subTest(fault=fault),tempfile.TemporaryDirectory() as tmp,patch.object(model,'PART_SIZES',(12,13,14)):
+ recorder=model_fixtures.Recorder(tmp);fixture=model.MpuRaceFixture(recorder,'rv-g05-owned')
+ prepared=schedule.prepare(fixture,'complete');count=len(recorder.calls);callback=lambda *args:{}
+ if fault=='branch':prepared['branch']='abort'
+ elif fault=='manifest':prepared=copy.deepcopy(prepared);prepared['manifest']['items']['a']['uploadId']='foreign'
+ elif fault=='operation':prepared['selectedOperation']='abort_multipart_upload'
+ else:callback=None
+ with self.assertRaises(ValueError):schedule.execute(fixture,'complete',prepared,callback)
+ self.assertEqual(len(recorder.calls),count)
diff --git a/test_validation_heal_mpu_scene.py b/test_validation_heal_mpu_scene.py
new file mode 100644
index 00000000..b173cfe9
--- /dev/null
+++ b/test_validation_heal_mpu_scene.py
@@ -0,0 +1,142 @@
+"""Exact stopped MPU census; read-only fixture checks do not certify EC/bitrot."""
+
+import hashlib
+import json
+import os
+from pathlib import Path
+import tempfile
+import unittest
+from unittest.mock import patch
+
+from scripts.validation_heal import mpu_scene as scene, fleet_node
+
+
+class MultipartSceneTests(unittest.TestCase):
+ def fixture(self, root, *, running=False):
+ root = root.resolve()
+ state = root / 'state';state.mkdir()
+ binary = b'exact candidate fixture';(state / 'rustfs').write_bytes(binary)
+ owner = {'runtimeProfile': 'lock-quorum', 'bucket': 'rv-heal-owned',
+ 'binarySha256': hashlib.sha256(binary).hexdigest(), 'runId': 'gh-12345-1-h2'}
+ roots = tuple(root / f'disk-{i}' for i in range(4))
+ keys = scene.keys_for(['node1', 'node3'])
+ raw = {}
+ for disk, folder in enumerate(roots):
+ data = folder / 'data';data.mkdir(parents=True)
+ for key in keys:
+ if key.endswith('/create.bin'): continue
+ digest = hashlib.sha256(f"{owner['bucket']}/{key}".encode()).hexdigest()
+ upload = data / '.rustfs.sys/multipart' / digest / f'opaque-upload-{disk}'
+ upload.mkdir(parents=True)
+ for name, body in [('xl.meta', f'original metadata {key}'.encode()), ('part.1', b'original full part' + bytes([disk]))]:
+ p = upload / name;p.write_bytes(body)
+ raw[p] = body
+ # Another key, state credentials and another system namespace are
+ # deliberately unreadable to the census's digest/open paths.
+ (data / '.rustfs.sys/config').mkdir()
+ (data / '.rustfs.sys/config/credentials').write_bytes(b'out of scope secret')
+ (state / 'credentials.env').write_bytes(b'state secret')
+ def stopped(value):
+ if running: raise ValueError('owned process still active')
+ node = {'owned': lambda run_id: (state, roots, owner), 'stopped': stopped,
+ 'checked': fleet_node.checked, 'digest': fleet_node.digest}
+ request = {'runId': owner['runId'], 'action': 'mpu-scene', 'bucket': owner['bucket'],
+ 'keys': keys, 'expectedBinarySha256': owner['binarySha256']}
+ return request, node, roots, raw
+
+ def test_census_covers_exact_eight_key_prefixes_all_four_drives_without_reading_credentials(self):
+ with tempfile.TemporaryDirectory() as d:
+ request, node, roots, raw = self.fixture(Path(d))
+ opened, real_open = [], os.open
+ def checked_open(path, *args, **kwargs):
+ opened.append(Path(path))
+ self.assertIn(Path(path), raw)
+ return real_open(path, *args, **kwargs)
+ with patch.object(scene.os, 'open', side_effect=checked_open): value = scene.run(request, node)
+ self.assertEqual(len(value['scopes']), 32)
+ self.assertEqual(len(value['files']), 48)
+ self.assertEqual(len(opened), 48)
+ self.assertEqual(len([r for r in value['scopes'] if not r['present']]), 8)
+ self.assertEqual(value['bytesHashed'], sum(len(b) for b in raw.values()))
+ self.assertTrue(value['quiescent'] and value['readOnly'])
+ self.assertFalse(value['rawContentsExported'] or value['payloadIntegrityClaimed'])
+ expected = {(disk, str(p.relative_to(root / 'data'))): hashlib.sha256(body).hexdigest()
+ for disk, root in enumerate(roots) for p, body in raw.items() if p.is_relative_to(root)}
+ self.assertEqual({(r['disk'], r['path']): r['sha256'] for r in value['files']}, expected)
+ self.assertNotIn('out of scope secret', json.dumps(value))
+ self.assertEqual({p: p.read_bytes() for p in raw}, raw)
+
+ def test_capture_distinguishes_partial_refused_abort_and_empty_directories(self):
+ with tempfile.TemporaryDirectory() as d:
+ request, node, roots, raw = self.fixture(Path(d));before = scene.run(request, node)
+ chosen = next(p for p in raw if '/abort.bin' in str(raw[p]) and p.name == 'xl.meta')
+ chosen.unlink()
+ after = scene.run(request, node)
+ self.assertNotEqual(before['sceneSha256'], after['sceneSha256'])
+ self.assertEqual(len(before['files']) - len(after['files']), 1)
+ self.assertEqual(before['directories'], after['directories'])
+ self.assertFalse(after['payloadIntegrityClaimed'])
+
+ def test_live_process_wrong_binary_bucket_runtime_or_key_matrix_refuses_before_file_reads(self):
+ for kind in ('live', 'binary', 'bucket', 'runtime', 'foreign-key', 'missing-key', 'duplicate-key', 'extra-field'):
+ with self.subTest(kind=kind), tempfile.TemporaryDirectory() as d:
+ request, node, roots, raw = self.fixture(Path(d), running=kind == 'live')
+ if kind == 'binary': request['expectedBinarySha256'] = 'f' * 64
+ elif kind == 'bucket': request['bucket'] = 'rv-heal-foreign'
+ elif kind == 'runtime': node['owned'](request['runId'])[2]['runtimeProfile'] = 'manual'
+ elif kind == 'foreign-key': request['keys'][0] = '../credentials'
+ elif kind == 'missing-key': request['keys'].pop()
+ elif kind == 'duplicate-key': request['keys'][0] = request['keys'][1]
+ elif kind == 'extra-field': request['path'] = '/etc'
+ with patch.object(scene.os, 'open', side_effect=AssertionError('census read outside validated scope')):
+ with self.assertRaises(ValueError): scene.run(request, node)
+
+ def test_symlinks_hardlinks_special_files_and_budget_overflow_are_not_hashed(self):
+ for kind in ('symlink', 'hardlink', 'fifo', 'file-cap', 'byte-cap'):
+ with self.subTest(kind=kind), tempfile.TemporaryDirectory() as d:
+ request, node, roots, raw = self.fixture(Path(d));part = next(p for p in raw if p.name == 'part.1')
+ if kind == 'symlink': part.unlink();part.symlink_to(Path(d).resolve() / 'state/credentials.env')
+ elif kind == 'hardlink': os.link(part, part.with_name('extra-link'))
+ elif kind == 'fifo': part.unlink();os.mkfifo(part)
+ with patch.object(scene, 'MAX_FILES', 1 if kind == 'file-cap' else scene.MAX_FILES), \
+ patch.object(scene, 'MAX_BYTES', 1 if kind == 'byte-cap' else scene.MAX_BYTES):
+ with self.assertRaises(ValueError): scene.run(request, node)
+
+ def test_changed_owner_or_process_during_capture_keeps_scene_unaccepted(self):
+ with tempfile.TemporaryDirectory() as d:
+ request, node, roots, raw = self.fixture(Path(d));original = node['owned'];calls = []
+ def changing(run_id):
+ value = original(run_id);calls.append(run_id)
+ if len(calls) > 1: return value[0], value[1], {**value[2], 'binarySha256': 'f' * 64}
+ return value
+ node['owned'] = changing
+ with self.assertRaisesRegex(ValueError, 'owner or original roots changed'): scene.run(request, node)
+ self.assertEqual(len(calls), 2)
+
+ def test_g05_partial_seed_captures_unknown_upload_ids_and_empty_drives(self):
+ with tempfile.TemporaryDirectory() as d:
+ request, node, roots, _ = self.fixture(Path(d))
+ owner = node['owned'](request['runId'])[2]
+ owner.update(runtimeProfile='manual', bucket='rv-heal-0123456789abcdef')
+ request.update(bucket=owner['bucket'], keys=[scene.G05_KEY])
+ key_hash = hashlib.sha256(f"{owner['bucket']}/{scene.G05_KEY}".encode()).hexdigest()
+ expected = {}
+ for disk, root in enumerate(roots[:3]):
+ upload = root/'data/.rustfs.sys/multipart'/key_hash/f'unknown-create-{disk}'
+ upload.mkdir(parents=True)
+ file = upload/'partial-part';file.write_bytes(b'uncertain original bytes'+bytes([disk]))
+ expected[disk, file.relative_to(root/'data').as_posix()] = hashlib.sha256(file.read_bytes()).hexdigest()
+ result = scene.run(request, node)
+ self.assertEqual({(r['disk'],r['path']):r['sha256'] for r in result['files']}, expected)
+ self.assertEqual([r['present'] for r in result['scopes']], [True,True,True,False])
+ self.assertFalse(result['payloadIntegrityClaimed'] or result['rawContentsExported'])
+ for bad in ([], [scene.G05_KEY, scene.G05_KEY], ['objects-0255.bin']):
+ with self.subTest(keys=bad), self.assertRaises(ValueError):
+ scene.run({**request, 'keys':bad}, node)
+ for index in range(3): (roots[0]/'data/.rustfs.sys/multipart'/key_hash/f'extra-{index}').mkdir()
+ with self.assertRaisesRegex(ValueError, 'directory population'):
+ scene.run(request, node)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/test_validation_heal_mrf.py b/test_validation_heal_mrf.py
new file mode 100644
index 00000000..d017413c
--- /dev/null
+++ b/test_validation_heal_mrf.py
@@ -0,0 +1,274 @@
+import base64
+import copy
+import hashlib
+import json
+from pathlib import Path
+import struct
+import tempfile
+import types
+import unittest
+import uuid
+from unittest.mock import patch
+import zlib
+
+from scripts.validation_heal import mrf_records as records, mrf_replay as replay
+from scripts.validation_heal.fleet_node import runtime_config
+from scripts.validation_heal.matrix import select, AUTOMATIC_CASES
+
+
+class DurableMrfTests(unittest.TestCase):
+ owner = "12345678-1234-4234-8234-123456789abc"
+ version = "22345678-1234-4234-8234-123456789abc"
+ incarnation = "32345678-1234-4234-8234-123456789abc"
+ responsibility = "42345678-1234-4234-8234-123456789abc"
+
+ def journal(self, *, layout=2, kind=3, key="offline/new.bin"):
+ bucket = b"rv-heal-0123456789abcdef"
+ raw = struct.pack("NoSuchBucketbucket not found: '
+ + self.buckets[0] + ''}
+ ready = {'httpStatus': 200, 'response': {'quota': 0, 'size': 0, 'rate': 0, 'requests': 0}}
+ probe.admin.side_effect = [not_ready, ready]
+ with patch.object(runner.time, 'sleep') as sleep:
+ self.assertEqual(runner.quota_config(probe, fleet, self.buckets[0], 'quota'), ready['response'])
+ sleep.assert_called_once_with(1)
+ with self.assertRaises(ValueError):
+ runner.quota_config(Mock(admin=Mock(return_value={'httpStatus': 404, 'response':
+ 'NoSuchBucketbucket not found: foreign'}),
+ binding={}), fleet, self.buckets[0], 'quota')
+
+ def test_quota_config_rejects_changed_limits_bool_and_peer_refusal(self):
+ probe, fleet = Mock(), Mock()
+ probe.binding = {'endpoints': {'node1': 'a', 'node2': 'b'}}
+ fleet.record.side_effect = lambda label, value: value
+ good = {'httpStatus': 200, 'response': {'quota': 0, 'size': 0, 'rate': 0, 'requests': 0}}
+ for drift in ('limit', 'bool', 'soft', 'missing', 'http', 'usage'):
+ changed = copy.deepcopy(good)
+ if drift == 'limit': changed['response'].update(quota=1024, size=1024, quotatype='hard')
+ elif drift == 'bool': changed['response']['quota'] = False
+ elif drift == 'soft': changed['response']['quotatype'] = 'soft'
+ elif drift == 'missing': changed['response'].pop('requests')
+ elif drift == 'http': changed['httpStatus'] = 503
+ else: changed['response'] = {'bucket': self.buckets[0], 'quota': None, 'quota_type': 'HARD'}
+ with self.subTest(drift=drift), patch.object(runner, 'Probe') as peers:
+ peers.return_value.admin.side_effect = [good, changed]
+ with self.assertRaises(ValueError): runner.quota_status(probe, fleet, self.buckets, 'quota')
+
+ def test_h5_pool_observations_accept_only_exact_signed_get_queries(self):
+ binding = {'schemaVersion': 1, 'runId': 'gh-123-1-h5', 'jumpHostOnly': True, 'exclusiveHealService': True,
+ 'leaseExpiresAt': (datetime.now(timezone.utc) + timedelta(minutes=30)).isoformat(),
+ 'bucketPrefix': self.buckets[0], 'endpoints': {f'node{i}': f'http://rustfs-node{i}:9000' for i in range(1, 5)},
+ 'defaultEndpoint': 'node1', 'candidateCommit': 'a' * 40, 'harnessCommit': 'b' * 40,
+ 'candidatePackageSha256': 'c' * 64, 'candidateBinarySha256': 'd' * 64, 'rcBinarySha256': 'e' * 64,
+ 'maxInventoryObjects': 256, 'maxInventoryBytes': 512 * 1024**2}
+ for path, query, method, auth, allowed in (
+ ('list', [], 'GET', 'signed', True), ('status', [('pool', '1'), ('by-id', 'true')], 'GET', 'signed', True),
+ ('list', [], 'POST', 'signed', False), ('list', [], 'GET', 'anonymous', False),
+ ('list', [('pool', '0')], 'GET', 'signed', False), ('status', [('pool', '2'), ('by-id', 'true')], 'GET', 'signed', False),
+ ('status', [('pool', '0'), ('by-id', 'true'), ('pool', '1')], 'GET', 'signed', False)):
+ probe = runner.Probe(binding)
+ with self.subTest(path=path, query=query, method=method, auth=auth), patch.object(probe, 'credentials', side_effect=RuntimeError('allowed route reached signing')):
+ if allowed:
+ with self.assertRaisesRegex(RuntimeError, 'reached signing'):
+ probe.admin('/rustfs/admin/v3/pools/' + path, query, b'', method=method, auth=auth)
+ else:
+ with self.assertRaises(ValueError): probe.admin('/rustfs/admin/v3/pools/' + path, query, b'', method=method, auth=auth)
+
+ def test_original_h2_mapper_rejects_multi_pool_without_explicit_authority(self):
+ snapshot, expected = self.fixture()
+ with self.assertRaises(ValueError): validate_topology(snapshot, 'H2')
+ with self.assertRaises(ValueError): map_target(snapshot, self.buckets[0], 'p1/s1.bin', 'data', 1)
+ before = copy.deepcopy(snapshot)
+ layout, mapped = runner.map_versions(snapshot, expected)
+ self.assertEqual(snapshot, before)
+ self.assertEqual(layout['independentDrives'], 16)
+ self.assertEqual(len(mapped), 16)
+ self.assertEqual({(row['poolIndex'], row['setIndex']) for row in mapped.values()}, {(0, 0), (0, 1), (1, 0), (1, 1)})
+ self.assertTrue(all(len(row['cohort']) == len({member['node'] for member in row['cohort']}) == 4 for row in mapped.values()))
+
+ def test_rejects_missing_original_drives_wrong_pools_aliases_and_host_local_sets(self):
+ for kind in ('drive', 'pool', 'alias', 'host-local', 'duplicate'):
+ snapshot, _ = self.fixture()
+ if kind == 'drive': snapshot['node1']['formats'].pop()
+ elif kind == 'pool': snapshot['node1']['formats'][2]['value']['xl']['sets'] = copy.deepcopy(snapshot['node1']['formats'][0]['value']['xl']['sets'])
+ elif kind == 'alias': snapshot['node1']['formats'][2]['value']['xl']['this'] = snapshot['node1']['formats'][0]['value']['xl']['this']
+ elif kind == 'duplicate': snapshot['node1']['formats'][0]['disk'] = 1
+ else:
+ sets = [[f'p0-s{group}-n{node}' for group in range(2) for node in range(start, start + 2)] for start in (0, 2)]
+ for value in snapshot.values():
+ for fmt in value['formats'][:2]: fmt['value']['xl']['sets'] = sets
+ with self.subTest(kind=kind), self.assertRaises(ValueError): validate(snapshot)
+
+ def test_missing_ack_version_wrong_role_geometry_or_extra_pool_authority_fail(self):
+ for kind in ('missing', 'role', 'geometry', 'authority', 'unpopulated'):
+ snapshot, expected = self.fixture()
+ entry = next(row for row in snapshot['node1']['files'] if row['path'] == self.buckets[0] + '/p1/s1.bin/xl.meta')
+ if kind == 'missing': entry['metadata']['versions'].clear()
+ elif kind == 'role': entry['metadata']['versions'][0]['role'] = 'parity'
+ elif kind == 'geometry': entry['metadata']['versions'][0]['erasure']['parity_blocks'] = 3
+ elif kind == 'authority': snapshot['node1']['files'].append({**copy.deepcopy(entry), 'disk': 0})
+ else: expected[self.buckets[0]] = [row for row in expected[self.buckets[0]] if row['key'] != 'p0/s0.bin']
+ with self.subTest(kind=kind), self.assertRaises(ValueError): runner.map_versions(snapshot, expected)
+
+ def test_raw_pool_only_receipts_and_exact_version_scope_are_not_rewritten(self):
+ current, mapped, selected, repaired, options = self.receipt('selected-pool')
+ original = copy.deepcopy(current)
+ proof = runner.responsibility(current, mapped, selected, repaired, options)
+ self.assertEqual(current, original)
+ self.assertTrue(proof['rawUnrestrictedSetNotRewritten'])
+ self.assertEqual({row['setIndex'] for row in proof['physicalScopeBindings']}, {0, 1})
+ self.assertEqual(proof['repaired'], 4)
+ self.assertEqual(proof['versions'], 8)
+
+ def test_wrong_selectors_versions_responsibility_and_false_counts_fail(self):
+ for kind in ('pool', 'set', 'bool', 'version', 'duplicate', 'missing', 'foreign', 'unknown', 'counter', 'settings', 'truncated', 'incarnation'):
+ current, mapped, selected, repaired, options = self.receipt('selected-set')
+ item = current['outcome']['objects'][0]
+ if kind in ('pool', 'set', 'bool', 'version', 'incarnation'):
+ name, value = {'pool': ('poolIndex', None), 'set': ('setIndex', 0), 'bool': ('poolIndex', True),
+ 'version': ('versionId', 'foreign'), 'incarnation': ('bucketIncarnationId', '')}[kind]
+ item['identity'][name] = value
+ elif kind == 'duplicate': current['outcome']['objects'][-1] = copy.deepcopy(item)
+ elif kind == 'missing': current['outcome']['objects'].pop()
+ elif kind == 'foreign': item['identity']['bucket'] = 'foreign'
+ elif kind == 'unknown': item['disposition']['state'] = 'unknown'
+ elif kind == 'counter': current['outcome']['counters']['healed'] += 1
+ elif kind == 'settings': current['settings'] = {**options, 'set': 0}
+ else: current['outcome']['objectsTruncated'] = True
+ with self.subTest(kind=kind), self.assertRaises(ValueError): runner.responsibility(current, mapped, selected, repaired, options)
+
+ def test_control_missing_part_is_preserved_until_selected_physical_proof(self):
+ original, expected = self.fixture()
+ _, mapped = runner.map_versions(original, expected)
+ _, selected, targets = runner.selection(mapped, 'selected-set')
+ after = copy.deepcopy(original)
+ missing = {(target['node'], target['disk'], target['relativePath']) for target in targets
+ if (target['bucket'], target['key'], target['versionId']) not in selected}
+ for node, observed in after.items():
+ observed['files'] = [row for row in observed['files'] if (node, row['disk'], row['path']) not in missing]
+ proof = physical_selection(original, after, targets, {identity[:2] for identity in selected})
+ self.assertEqual(proof['selectedReconstructedParts'], 2)
+ self.assertEqual(proof['unselectedStillMissingParts'], 4)
+ identity = next(iter(missing))
+ after[identity[0]]['files'].append(files(original)[identity])
+ with self.assertRaises(ValueError): physical_selection(original, after, targets, {identity[:2] for identity in selected})
+
+ def seed_row(self, version='v1', resolved=False):
+ params = {'Bucket': self.buckets[1], 'Key': 'object', 'ContentType': 'application/octet-stream', 'Metadata': {'nonce': version}}
+ row = {'operation': 'put_object', 'params': params, 'payload': {'size': 1, 'sha256': version}, 'resolved': resolved,
+ 'observation': {'acknowledged': resolved, 'resultUnknown': not resolved, 'response': {'VersionId': version}}}
+ entry = {'key': 'object', 'versionId': version, 'deleteMarker': False, 'isLatest': True, 'payload': row['payload'],
+ 'properties': {'Metadata': params['Metadata'], 'ContentType': params['ContentType']},
+ 'tags': [{'Key': 'fixture', 'Value': 'H06'}, {'Key': 'suite', 'Value': 'heal'}]}
+ return row, entry
+
+ def test_unknown_seed_is_resolved_as_committed_or_absent_without_retry(self):
+ first, previous = self.seed_row('v1', True)
+ pending, newest = self.seed_row('v2')
+ rows = [first, pending]
+ absent = runner.resolve_fixture_inventory(rows, {'entries': [previous]})
+ self.assertEqual(absent[-1]['state'], 'ABSENT')
+ previous['isLatest'] = False
+ committed = runner.resolve_fixture_inventory(rows, {'entries': [previous, newest]})
+ self.assertEqual(committed[-1]['versionId'], 'v2')
+
+ def test_unknown_seed_drift_ack_replacement_tags_extra_version_and_latest_fail(self):
+ for kind in ('payload', 'version', 'tags', 'extra', 'latest'):
+ row, entry = self.seed_row('v1', True)
+ if kind == 'payload': entry['payload'] = {'size': 2, 'sha256': 'drift'}
+ elif kind == 'version': entry['versionId'] = 'replacement'
+ elif kind == 'tags': entry['tags'] = []
+ elif kind == 'latest': entry['isLatest'] = False
+ entries = [entry, {**entry, 'versionId': 'extra'}] if kind == 'extra' else [entry]
+ with self.subTest(kind=kind), self.assertRaises(ValueError): runner.resolve_fixture_inventory([row], {'entries': entries})
+
+ def test_pending_registration_precedes_recorder_or_ack_validation_failure(self):
+ ledger = []
+ recorder = Mock()
+ def fail(*args):
+ self.assertEqual(len(ledger), 1)
+ raise OSError('collector failed before durable receipt')
+ recorder.call.side_effect = fail
+ with self.assertRaises(OSError): runner.fixture_call(recorder, ledger, 'create_bucket', {'Bucket': self.buckets[0]})
+ self.assertFalse(ledger[0]['resolved'])
+ ledger = []
+ recorder.call.side_effect = None
+ recorder.call.return_value = {'VersionId': 'null'}
+ with self.assertRaises(ValueError): runner.fixture_call(recorder, ledger, 'put_object', {'Bucket': self.buckets[1]}, b'x')
+ self.assertFalse(ledger[0]['resolved'])
+
+ def test_uncertain_bucket_mutations_and_cross_peer_divergence_retain_dirty_boundary(self):
+ fleet = Mock()
+ with self.assertRaises(ValueError): runner.resolve_pending_fixture(fleet, {}, [{'operation': 'create_bucket', 'resolved': False}])
+ fleet.record.assert_not_called()
+ row, entry = self.seed_row()
+ binding = {'endpoints': {'node1': 'a', 'node2': 'b'}}
+ fleet.record.side_effect = lambda label, value: value
+ with patch.object(runner, 'Probe') as probe:
+ probe.return_value.inventory.side_effect = [{'entries': [entry]}, {'entries': []}]
+ with self.assertRaises(ValueError): runner.resolve_pending_fixture(fleet, binding, [row])
+ self.assertFalse(row['resolved'])
+
+ def test_node_redundancy_requires_actual_owned_stop_and_exact_reads_from_every_survivor(self):
+ snapshot, expected = self.fixture()
+ _, mapped = runner.map_versions(snapshot, expected)
+ for entries in expected.values():
+ for entry in entries: entry['payload'] = {'size': runner.SIZE, 'sha256': entry['key'] + entry['versionId']}
+ binding = {'candidateBinarySha256': 'candidate', 'endpoints': {f'node{i}': f'http://node{i}:9000' for i in range(1, 5)}}
+ for drift in (False, True):
+ fleet = Mock(nodes=list(snapshot))
+ fleet.record.side_effect = lambda label, value: value
+ fleet.remote.side_effect = [
+ {'pid': 5, 'startTicks': 1, 'unit': 'owned', 'bootId': 'boot', 'binarySha256': 'candidate'},
+ {'quiescent': True, 'service': {'MainPID': '0'}}, {},
+ {'pid': 6, 'startTicks': 2, 'unit': 'owned', 'bootId': 'boot', 'binarySha256': 'candidate'}]
+ with patch.object(runner, 'endpoint_refused', return_value={'connectionRefused': True}), patch.object(runner, 'Probe') as probe:
+ def read(operation, params):
+ entry = next(item for item in expected[params['Bucket']] if item['key'] == params['Key'] and item['versionId'] == params['VersionId'])
+ return {'acknowledged': True, 'resultUnknown': False, 'payload': {} if drift else entry['payload'],
+ 'response': {'VersionId': entry['versionId']}}
+ probe.return_value.s3.side_effect = read
+ if drift:
+ with self.assertRaises(ValueError): runner.witness_peer_loss(fleet, binding, expected, mapped)
+ else:
+ proof = runner.witness_peer_loss(fleet, binding, expected, mapped)
+ self.assertEqual(proof['exactVersions'], 16)
+ self.assertEqual(probe.return_value.s3.call_count, 48)
+ self.assertEqual({call.args[1] for call in probe.call_args_list}, {'node1', 'node2', 'node3'})
+
+ def test_unresolved_seed_prevents_namespace_cleanup_and_next_branch_admission(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ directory = Path(tmp)
+ candidate = directory / 'candidate.json'
+ candidate.write_text(__import__('json').dumps({'source_sha': runner.REFERENCE['candidateCommit'], 'package_sha256': 'package'}))
+ rc = directory / 'rc'
+ rc.write_bytes(b'collector')
+ args = types.SimpleNamespace(evidence=directory / 'evidence', tool_directory=directory,
+ candidate=candidate, binary_sha='binary', testing_commit='harness', rc_bin=rc)
+ fleet = Mock(nodes=['node1', 'node2', 'node3', 'node4'], quorum_gates=set())
+ fleet.record.side_effect = lambda label, value: value
+ def failed_seed(probe, path, buckets, ledger):
+ ledger.append({'operation': 'create_bucket', 'params': {'Bucket': buckets[0]}, 'resolved': False})
+ raise OSError('unknown initial bucket mutation')
+ with patch.dict(runner.os.environ, {'GITHUB_RUN_ID': '123', 'GITHUB_RUN_ATTEMPT': '1'}), \
+ patch.object(runner, 'Fleet', return_value=fleet), patch.object(runner, 'verify_inspector', return_value={}), \
+ patch.object(runner, 'Probe'), patch.object(runner, 'seed_fixtures', side_effect=failed_seed):
+ result = runner.run_branch(args, fleet.nodes, 'selected-pool')
+ self.assertEqual(result['verdict'], 'FAIL')
+ self.assertEqual(result['cleanup'], 'FAIL')
+ self.assertEqual(result['seedResolution']['state'], 'UNRESOLVED')
+ fleet.cleanup.assert_not_called()
+ self.assertTrue(any(call.args[0] == 'retained-unresolved-seed' for call in fleet.record.call_args_list))
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/test_validation_heal_native.py b/test_validation_heal_native.py
new file mode 100644
index 00000000..6dbe91b2
--- /dev/null
+++ b/test_validation_heal_native.py
@@ -0,0 +1,337 @@
+import copy
+import json
+from pathlib import Path
+import tempfile
+import unittest
+from unittest.mock import patch
+import subprocess
+
+from scripts.validation_heal import native_regressions as native
+from scripts.validation_heal import persistence_native as persistence
+
+
+def evidence_fixture(root, commit=native.INCARNATION_CANDIDATE):
+ tools, evidence = root / 'native-tools', root / 'native-evidence'
+ tools.mkdir(); evidence.mkdir()
+ binary = tools / 'ecstore-native-tests'
+ binary.write_bytes(b'original exact candidate test executable')
+ contract = native.REVIEWED_CANDIDATES[commit]
+ manifest = {'schemaVersion': 1, 'candidateCommit': commit, 'candidateSourceTree': contract['sourceTree'],
+ 'candidateCargoLockSha256': native.CARGO_LOCK_SHA, 'sourceFiles': contract['sourceFiles'],
+ 'sourceUnmodified': True, 'regressions': contract['regressions'], 'optimized': True,
+ 'collectorSha256': native.sha(Path(native.__file__)), 'binaryName': binary.name,
+ 'binarySha256': native.sha(binary), 'binaryKind': 'native-regression-not-released-service',
+ 'buildCommand': native.BUILD_COMMAND, 'buildEnvironment': native.BUILD_ENV,
+ 'runtimeEnvironment': native.runtime_environment(commit), 'fleetPassClaimed': False,
+ 'runId': '12345', 'runAttempt': '1', 'testingCommit': 'a' * 40}
+ (tools / 'native-regressions.json').write_text(json.dumps(manifest))
+ listing = '\n'.join(spec['test'] + ': test' for spec in contract['regressions'].values())
+ (tools / 'test-inventory.log').write_text(listing)
+ (evidence / 'runtime-inventory.log').write_text(listing)
+ rows = []
+ for key, spec in contract['regressions'].items():
+ events = spec.get('barrierEvents', [])
+ row = {'regression': key, 'test': spec['test'], 'sourceAssertions': spec['sourceAssertions'],
+ 'fenceKind': spec['fenceKind'], 'limitations': spec['limits'], 'candidateCommit': commit,
+ 'binarySha256': manifest['binarySha256'], 'command': native.exact_command(binary, spec['test'], commit),
+ 'runtimeEnvironment': manifest['runtimeEnvironment'], 'verdict': 'PASS', 'exitCode': 0,
+ 'fleetPassClaimed': False, 'emittedBarrierLogsClaimed': bool(events), 'elapsedNanoseconds': 100}
+ if events: row['observedBarrierEvents'] = events
+ output = f'running 1 test\ntest {spec["test"]} ... ok\n'
+ output += '\n'.join('incarnation_commit_barrier: ' + event for event in events) + '\n'
+ output += 'test result: ok. 1 passed; 0 failed; 0 ignored; 99 filtered out;\n'
+ (evidence / (key + '.log')).write_text(output)
+ (evidence / (key + '.json')).write_text(json.dumps(row))
+ rows.append(row)
+ (evidence / 'native-summary.json').write_text(json.dumps({'results': rows, 'complete': True,
+ 'sourceUnmodified': True, 'fleetPassClaimed': False, 'canonicalCasePassClaimed': False}))
+ return tools, evidence, {'source_sha': commit}, 'a' * 40
+
+
+class NativeRegressionTests(unittest.TestCase):
+ name = native.PREFIX + 'read_repair_commit_aborts_when_object_changes_before_final_rename'
+
+ def test_full_test_name_must_appear_once_in_actual_inventory(self):
+ native.exact_listing(self.name + ': test\n', self.name)
+ for listing in ('0 tests, 0 benchmarks\n', self.name.split('::')[-1] + ': test\n',
+ (self.name + ': test\n') * 2, self.name + '-other: test\n'):
+ with self.subTest(listing=listing), self.assertRaises(ValueError):
+ native.exact_listing(listing, self.name)
+
+ def test_zero_filtered_ignored_failed_and_other_test_success_are_not_pass(self):
+ passed = f'running 1 test\ntest {self.name} ... ok\ntest result: ok. 1 passed; 0 failed; 0 ignored; 200 filtered out;\n'
+ proof = native.test_result(passed, self.name, 0)
+ self.assertTrue(proof['exactTestExecuted'])
+ self.assertFalse(proof['fleetPassClaimed'])
+ self.assertFalse(proof['emittedBarrierLogsClaimed'])
+ for output, code in ((passed, 1), (passed, False), (passed + passed, 0),
+ (passed + 'test unrelated ... ok\n', 0),
+ (passed.replace('running 1 test', 'running 0 tests'), 0),
+ (passed.replace('1 passed; 0 failed; 0 ignored', '0 passed; 0 failed; 1 ignored'), 0),
+ (passed.replace(self.name, self.name + '-other'), 0),
+ (passed.replace('... ok', '... FAILED'), 0)):
+ with self.subTest(output=output, code=code), self.assertRaises(ValueError):
+ native.test_result(output, self.name, code)
+
+ def test_uncontrolled_concurrency_is_explicitly_not_a_fence(self):
+ for key in ('overwrite-concurrency', 'version-delete-concurrency'):
+ self.assertIn('not deterministic fence', native.REGRESSIONS[key]['fenceKind'])
+ self.assertIn('cannot invent emitted barrier logs', native.REGRESSIONS['read-repair-lock-owner']['limits'])
+
+ def test_authority_guards_bind_exact_candidate_tests_without_claiming_runtime_pause(self):
+ for key, suffix in (
+ ('conflicting-authority-guard', 'dangling_delete_guard_preserves_conflicting_identities_without_writing_metadata'),
+ ('reconstructable-authority-preservation', 'heal_meta_quorum_failure_preserves_reconstructable_uncommitted_candidate')):
+ spec = native.REGRESSIONS[key]
+ self.assertEqual(spec['test'], native.PREFIX + suffix)
+ self.assertEqual(spec['relatedCases'], ['heal.conflicting-subquorum-authority'])
+ self.assertIn('deterministic', spec['fenceKind'])
+ self.assertIn('pause', spec['limits'])
+
+ def test_wrong_source_modified_contract_or_collector_fail_before_execution(self):
+ base = {'schemaVersion': 1, 'candidateCommit': native.CANDIDATE, 'sourceFiles': {native.SOURCE: native.SOURCE_SHA},
+ 'sourceUnmodified': True, 'regressions': native.REGRESSIONS, 'optimized': True,
+ 'collectorSha256': native.sha(Path(native.__file__)), 'binaryName': 'ecstore-native-tests', 'binarySha256': '0' * 64}
+ for field, value in (('candidateCommit', '0' * 40), ('sourceUnmodified', False),
+ ('regressions', {}), ('collectorSha256', '0' * 64), ('binaryName', '../foreign')):
+ with self.subTest(field=field), tempfile.TemporaryDirectory() as temp:
+ directory = Path(temp)
+ (directory / 'native-regressions.json').write_text(json.dumps({**base, field: value}))
+ with self.assertRaises(ValueError):
+ native.run(directory, ['read-repair-final-rename'], directory / 'evidence')
+ self.assertFalse((directory / 'evidence').exists())
+
+
+class PersistenceNativeTests(unittest.TestCase):
+ def fixture(self, root):
+ tools, original, _, _ = evidence_fixture(root)
+ inventory = tools / 'test-inventory.log'
+ inventory.write_text(inventory.read_text() + '\n' + '\n'.join(
+ persistence.PREFIX + suffix + ': test' for suffix, _ in persistence.REGRESSIONS.values()))
+ return tools, original, root / 'persistence'
+
+ def execute(self, tools, fail=None):
+ def child(command, **kwargs):
+ if command[-1] == '--list':
+ return subprocess.CompletedProcess(command, 0, (tools / 'test-inventory.log').read_text(), '')
+ self.assertEqual(command[2:], ['--exact', '--show-output', '--test-threads=1'])
+ self.assertEqual(kwargs['env']['RUST_MIN_STACK'], '33554432')
+ if fail == 'timeout': raise subprocess.TimeoutExpired(command, 120, output=b'original timeout output\n')
+ name = command[1]
+ output = f'running 1 test\ntest {name} ... ok\ntest result: ok. 1 passed; 0 failed; 0 ignored;\n'
+ if fail == 'zero-test': output = 'running 0 tests\ntest result: ok. 0 passed; 0 failed; 0 ignored;\n'
+ return subprocess.CompletedProcess(command, 0, output, '')
+ return child
+
+ def run_fixture(self, root):
+ tools, original, out = self.fixture(root)
+ with patch.object(persistence.subprocess, 'run', side_effect=self.execute(tools)):
+ persistence.run(tools, original, out, 'b' * 40)
+ return tools, original, out
+
+ def test_executes_existing_exact_tests_without_claiming_complete_cuts(self):
+ with tempfile.TemporaryDirectory() as temp:
+ tools, original, out = self.run_fixture(Path(temp))
+ proof = persistence.verify(tools, original, out, 'b' * 40)
+ self.assertEqual(proof['exactTests'], 6)
+ self.assertEqual(proof['nativeTestingCommit'], 'a' * 40)
+ self.assertEqual(proof['collectorCommit'], 'b' * 40)
+ for flag in ('requiredNativeCoverageComplete', 'individualFileFsyncCovered', 'physicalPowerLossClaimed',
+ 'canonicalCasePassClaimed', 'fleetPassClaimed', 'emittedBarrierLogsClaimed'):
+ self.assertIs(proof[flag], False)
+
+ def test_unverified_original_suite_cannot_launch_supplement(self):
+ for change in ('source', 'binary', 'original-log', 'missing-test'):
+ with tempfile.TemporaryDirectory() as temp, self.subTest(change=change):
+ tools, original, out = self.fixture(Path(temp))
+ if change == 'source':
+ path = tools / 'native-regressions.json'
+ value = json.loads(path.read_text()); value['candidateCommit'] = native.CANDIDATE
+ path.write_text(json.dumps(value))
+ elif change == 'binary': (tools / 'ecstore-native-tests').write_bytes(b'changed executable')
+ elif change == 'original-log': (original / 'incarnation-final-rename.log').write_text('running 0 tests')
+ else:
+ path = tools / 'test-inventory.log'
+ path.write_text(path.read_text().replace(persistence.PREFIX + next(iter(persistence.REGRESSIONS.values()))[0] + ': test', ''))
+ with patch.object(persistence.subprocess, 'run') as execute, self.assertRaises(ValueError):
+ persistence.run(tools, original, out, 'b' * 40)
+ execute.assert_not_called()
+ self.assertFalse(out.exists())
+
+ def test_zero_test_and_timeout_keep_original_failed_diagnostics(self):
+ for failure in ('zero-test', 'timeout'):
+ with tempfile.TemporaryDirectory() as temp, self.subTest(failure=failure):
+ tools, original, out = self.fixture(Path(temp))
+ with patch.object(persistence.subprocess, 'run', side_effect=self.execute(tools, failure)), self.assertRaises(ValueError):
+ persistence.run(tools, original, out, 'b' * 40)
+ summary = json.loads((out / 'persistence-native-summary.json').read_text())
+ self.assertIs(summary['complete'], False)
+ self.assertEqual(len(summary['results']), 6)
+ self.assertTrue(all(row['verdict'] == 'FAIL' for row in summary['results']))
+ self.assertIn('original timeout output' if failure == 'timeout' else 'running 0 tests',
+ (out / 'mrf-manifest-directory-sync.log').read_text())
+ with self.assertRaises(ValueError): persistence.verify(tools, original, out, 'b' * 40)
+
+ def test_tampered_source_commands_logs_or_scope_cannot_certify_fsync(self):
+ for change in ('candidate', 'attempt', 'collector', 'powerloss', 'complete-cuts', 'duplicate', 'command', 'assertion', 'log', 'inventory'):
+ with tempfile.TemporaryDirectory() as temp, self.subTest(change=change):
+ tools, original, out = self.run_fixture(Path(temp))
+ path = out / 'persistence-native-summary.json'
+ summary = json.loads(path.read_text())
+ if change == 'candidate': summary['candidateCommit'] = 'f' * 40
+ elif change == 'attempt': summary['runAttempt'] = '2'
+ elif change == 'collector': summary['collectorSha256'] = '0' * 64
+ elif change == 'powerloss': summary['physicalPowerLossClaimed'] = True
+ elif change == 'complete-cuts': summary['requiredNativeCoverageComplete'] = True
+ elif change == 'duplicate': summary['results'][0] = copy.deepcopy(summary['results'][1])
+ elif change in ('command', 'assertion'):
+ row = summary['results'][0]
+ if change == 'command': row['command'][2] = '--ignored'
+ else: row['sourceAssertion'] = 'all fsync and powerloss cuts'
+ (out / (row['regression'] + '.json')).write_text(json.dumps(row))
+ elif change == 'log': (out / 'cas-update-directory-sync.log').write_text('running 0 tests')
+ else: (out / 'runtime-inventory.log').write_text('0 tests')
+ path.write_text(json.dumps(summary))
+ with self.assertRaises(ValueError): persistence.verify(tools, original, out, 'b' * 40)
+
+
+class NativeEvidenceTests(unittest.TestCase):
+ def verify(self, fixture, **changes):
+ tools, evidence, candidate, helper = fixture
+ return native.verify_evidence(tools, evidence, changes.get('candidate', candidate),
+ changes.get('run', 12345), changes.get('attempt', 1), changes.get('helper', helper))
+
+ def test_complete_suites_preserve_original_six_and_exact_nine_scopes(self):
+ for commit, count in ((native.CANDIDATE, 6), (native.INCARNATION_CANDIDATE, 9)):
+ with self.subTest(commit=commit), tempfile.TemporaryDirectory() as temp:
+ proof = self.verify(evidence_fixture(Path(temp), commit))
+ self.assertEqual(proof['exactTests'], count)
+ self.assertFalse(proof['canonicalCasePassClaimed'])
+ self.assertFalse(proof['fleetPassClaimed'])
+ self.assertEqual(bool(proof['observedBarrierEvents']), count == 9)
+
+ def test_runner_executes_full_candidate_suite_and_retains_original_milestones(self):
+ with tempfile.TemporaryDirectory() as temp:
+ fixture = evidence_fixture(Path(temp)); tools, evidence, _, _ = fixture
+ calls = []
+ def execute(command, **kwargs):
+ calls.append(command)
+ if command[-1] == '--list':
+ return subprocess.CompletedProcess(command, 0, (tools / 'test-inventory.log').read_text(), '')
+ self.assertEqual(kwargs['env']['RUST_MIN_STACK'], '33554432')
+ self.assertEqual(command[2:], ['--exact', '--show-output', '--test-threads=1'])
+ key = next(key for key, spec in native.REVIEWED_CANDIDATES[native.INCARNATION_CANDIDATE]['regressions'].items()
+ if spec['test'] == command[1])
+ return subprocess.CompletedProcess(command, 0, (evidence / (key + '.log')).read_text(), '')
+ out = Path(temp) / 'executed'
+ with patch.object(native.subprocess, 'run', side_effect=execute): native.run(tools, None, out)
+ self.assertEqual(len(calls), 10)
+ proof = native.verify_evidence(tools, out, fixture[2], 12345, 1, fixture[3])
+ self.assertEqual(proof['observedBarrierEvents']['incarnation-final-rename'], native.INCARNATION_EVENTS)
+
+ def test_runner_retains_missing_barrier_failure_and_refuses_complete_success(self):
+ with tempfile.TemporaryDirectory() as temp:
+ tools, evidence, _, _ = evidence_fixture(Path(temp))
+ def execute(command, **kwargs):
+ if command[-1] == '--list':
+ output = (tools / 'test-inventory.log').read_text()
+ else:
+ output = (evidence / 'incarnation-final-rename.log').read_text().replace(
+ 'incarnation_commit_barrier: final_rename_paused', '')
+ return subprocess.CompletedProcess(command, 0, output, '')
+ out = Path(temp) / 'failed'
+ with patch.object(native.subprocess, 'run', side_effect=execute), self.assertRaises(ValueError):
+ native.run(tools, ['incarnation-final-rename'], out)
+ self.assertIn('caller_aborted_writer_pending', (out / 'incarnation-final-rename.log').read_text())
+ summary = json.loads((out / 'native-summary.json').read_text())
+ self.assertFalse(summary['complete'])
+ self.assertEqual(summary['results'][0]['verdict'], 'FAIL')
+
+ def test_source_tree_lock_build_runtime_collector_and_scope_drift_fail(self):
+ with tempfile.TemporaryDirectory() as temp:
+ fixture = evidence_fixture(Path(temp)); path = fixture[0] / 'native-regressions.json'
+ original = json.loads(path.read_text())
+ for field, value in [('candidateCommit', native.CANDIDATE), ('candidateSourceTree', 'b' * 40),
+ ('candidateCargoLockSha256', 'b' * 64), ('sourceFiles', {}), ('collectorSha256', 'b' * 64),
+ ('regressions', {}), ('sourceUnmodified', False), ('optimized', False), ('buildCommand', []),
+ ('buildEnvironment', {}), ('runtimeEnvironment', {}), ('fleetPassClaimed', True),
+ ('binaryKind', 'released-service'), ('binaryName', '../foreign')]:
+ path.write_text(json.dumps({**original, field: value}))
+ with self.subTest(field=field), self.assertRaises(ValueError): self.verify(fixture)
+
+ def test_candidate_run_attempt_helper_and_binary_are_independently_bound(self):
+ with tempfile.TemporaryDirectory() as temp:
+ fixture = evidence_fixture(Path(temp))
+ for changes in [{'candidate': {'source_sha': 'b' * 40}}, {'run': 12346}, {'attempt': 2}, {'helper': 'b' * 40}]:
+ with self.subTest(changes=changes), self.assertRaises(ValueError): self.verify(fixture, **changes)
+ (fixture[0] / 'ecstore-native-tests').write_bytes(b'wrong executable')
+ with self.assertRaises(ValueError): self.verify(fixture)
+
+ def test_symlinked_binary_is_rejected(self):
+ with tempfile.TemporaryDirectory() as temp:
+ fixture = evidence_fixture(Path(temp)); binary = fixture[0] / 'ecstore-native-tests'
+ original = fixture[0] / 'original'; binary.rename(original); binary.symlink_to(original)
+ with self.assertRaises(ValueError): self.verify(fixture)
+
+ def test_summary_cannot_omit_duplicate_or_promote_tests(self):
+ with tempfile.TemporaryDirectory() as temp:
+ fixture = evidence_fixture(Path(temp)); path = fixture[1] / 'native-summary.json'
+ original = json.loads(path.read_text())
+ for change in ('missing', 'duplicate', 'incomplete', 'fleet', 'canonical'):
+ value = copy.deepcopy(original)
+ if change == 'missing': value['results'].pop()
+ elif change == 'duplicate': value['results'][-1] = value['results'][0]
+ elif change == 'incomplete': value['complete'] = False
+ elif change == 'fleet': value['fleetPassClaimed'] = True
+ else: value['canonicalCasePassClaimed'] = True
+ path.write_text(json.dumps(value))
+ with self.subTest(change=change), self.assertRaises(ValueError): self.verify(fixture)
+
+ def test_original_milestones_must_be_complete_unique_and_ordered(self):
+ with tempfile.TemporaryDirectory() as temp:
+ fixture = evidence_fixture(Path(temp)); path = fixture[1] / 'incarnation-final-rename.log'
+ original = path.read_text()
+ first, second = ['incarnation_commit_barrier: ' + e for e in native.INCARNATION_EVENTS[:2]]
+ for output in (original.replace(first, ''), original + first + '\n',
+ original.replace(first, 'TEMP').replace(second, first).replace('TEMP', second)):
+ path.write_text(output)
+ with self.subTest(output=output), self.assertRaises(ValueError): self.verify(fixture)
+
+ def test_forged_rows_and_commands_cannot_replace_original_proof(self):
+ for change in ('row-only', 'events', 'claim', 'command', 'runtime', 'scope', 'exit', 'binary'):
+ with self.subTest(change=change), tempfile.TemporaryDirectory() as temp:
+ fixture = evidence_fixture(Path(temp)); key = 'incarnation-final-rename'
+ path = fixture[1] / (key + '.json'); row = json.loads(path.read_text())
+ if change in ('row-only', 'events'): row['observedBarrierEvents'] = []
+ elif change == 'claim': row['emittedBarrierLogsClaimed'] = False
+ elif change == 'command': row['command'][3] = '--nocapture'
+ elif change == 'runtime': row['runtimeEnvironment'] = {}
+ elif change == 'scope': row['fleetPassClaimed'] = True
+ elif change == 'exit': row['exitCode'] = 1
+ else: row['binarySha256'] = 'b' * 64
+ path.write_text(json.dumps(row))
+ if change != 'row-only':
+ summary_path = fixture[1] / 'native-summary.json'
+ summary = json.loads(summary_path.read_text())
+ summary['results'][-1] = row; summary_path.write_text(json.dumps(summary))
+ with self.assertRaises(ValueError): self.verify(fixture)
+
+ def test_original_inventory_and_test_execution_cannot_be_missing_or_ignored(self):
+ for change in ('missing', 'duplicate', 'zero', 'ignored', 'wrong-test'):
+ with self.subTest(change=change), tempfile.TemporaryDirectory() as temp:
+ fixture = evidence_fixture(Path(temp)); key = 'incarnation-final-rename'
+ name = native.INCARNATION_REGRESSIONS[key]['test']
+ if change in ('missing', 'duplicate'):
+ path = fixture[1] / 'runtime-inventory.log'; original = path.read_text()
+ path.write_text(original.replace(name + ': test', '') if change == 'missing' else original + '\n' + name + ': test')
+ else:
+ path = fixture[1] / (key + '.log'); original = path.read_text()
+ if change == 'zero': original = original.replace('running 1 test', 'running 0 tests')
+ elif change == 'ignored': original = original.replace('... ok', '... ignored')
+ else: original = original.replace(name, name + '-other')
+ path.write_text(original)
+ with self.assertRaises(ValueError): self.verify(fixture)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/test_validation_heal_network.py b/test_validation_heal_network.py
new file mode 100644
index 00000000..2ee9a136
--- /dev/null
+++ b/test_validation_heal_network.py
@@ -0,0 +1,629 @@
+"""Network fault ownership, real kernel rules and negative quorum truth."""
+
+import copy
+import errno
+import hashlib
+import json
+import os
+from pathlib import Path
+import shutil
+import subprocess
+import sys
+import tempfile
+import unittest
+from types import SimpleNamespace
+from unittest.mock import patch
+
+from scripts.validation_heal import network, network_gate as gate, fleet_node
+from scripts.validation_heal.matrix import AUTOMATIC_CASES, select
+from scripts.validation_heal import shards
+import test_validation_heal as oracle
+
+
+class NetworkTests(unittest.TestCase):
+ def scene(self, branch='read-boundary'):
+ base = oracle.HealReadQuorumBoundaryTest()
+ snapshot, expected = base.scene(total=16, data=8)
+ target = shards.map_target(snapshot, base.bucket, expected[0]['key'], 'data', 1)
+ target = {**target, **next(row for row in target['cohort'] if row['node'] == 'node4')}
+ return snapshot, target, network.plan_for(snapshot, target, branch)
+
+ def test_both_actual_r_w_and_lock_boundaries_are_independent(self):
+ for branch in network.BRANCHES:
+ snapshot, target, plan = self.scene(branch)
+ self.assertEqual(plan['readQuorum'], 8)
+ self.assertEqual(plan['writeQuorum'], 9)
+ self.assertEqual(plan['exclusiveLockQuorum'], 3)
+ self.assertEqual(plan['sides'][0]['writableDrives'], 8)
+ self.assertGreaterEqual(plan['sides'][0]['readableSources'], 8)
+ self.assertTrue(all(side['writableDrives'] < plan['writeQuorum'] for side in plan['sides']))
+ if branch in {'read-boundary', 'lock-boundary'}:
+ self.assertEqual(plan['sides'][0]['readableSources'], 8)
+ self.assertEqual(plan['sides'][1]['readableSources'], 7)
+ self.assertFalse(plan['gates'])
+ self.assertTrue(all(side['lockClients'] < 3 for side in plan['sides']))
+ else:
+ self.assertEqual(plan['sides'][0]['lockClients'], 3)
+ self.assertTrue(plan['sides'][0]['exclusiveLockQuorumPresent'])
+ self.assertEqual(plan['gates'], {'node1': [0, 1, 2, 3]})
+ self.assertEqual(plan['sides'][1]['readableSources'], 3)
+ for index, node in enumerate(snapshot):
+ for other in plan['blocked'][node]:
+ self.assertIn(index, plan['blocked'][list(snapshot)[other]])
+
+ def test_foreign_geometry_duplicate_slots_and_wrong_defect_member_are_rejected(self):
+ for kind in ('geometry', 'duplicate', 'foreign-node', 'target-node', 'branch'):
+ snapshot, target, _ = self.scene()
+ branch = 'read-boundary'
+ if kind == 'geometry': target['logical']['erasure']['data_blocks'] = 12
+ elif kind == 'duplicate': target['cohort'][-1] = copy.deepcopy(target['cohort'][0])
+ elif kind == 'foreign-node': target['cohort'][-1]['node'] = 'foreign'
+ elif kind == 'target-node': target['node'] = 'node1'
+ else: branch = 'invented'
+ with self.subTest(kind=kind), self.assertRaises(ValueError):
+ network.plan_for(snapshot, target, branch)
+
+ def test_partial_coverage_cannot_be_converted_into_global_idle(self):
+ value = {'clusterStatusComplete': False, 'state': 'degraded',
+ 'coverage': {'expected': 4, 'responded': 2, 'unknown': 2, 'reasons': ['partition']}}
+ network.validate_partial(value, 2)
+ for kind in ('complete', 'idle', 'hidden-peer', 'no-reason', 'untyped'):
+ broken = copy.deepcopy(value)
+ if kind == 'complete': broken['clusterStatusComplete'] = True
+ elif kind == 'idle': broken['state'] = 'idle'
+ elif kind == 'hidden-peer': broken['coverage']['unknown'] = 0
+ elif kind == 'no-reason': broken['coverage']['reasons'] = []
+ else: broken['coverage']['responded'] = True
+ with self.subTest(kind=kind), self.assertRaises(ValueError):
+ network.validate_partial(broken, 2)
+
+ def negative(self, state='deferred'):
+ expected = [{'key': 'objects/a', 'versionId': 'v0'}, {'key': 'objects/a', 'versionId': 'v1'}]
+ target = {'key': 'objects/a', 'versionId': 'v1'}
+ objects = [{'identity': {'bucket': 'owned', 'object': 'objects/a', 'versionId': version,
+ 'bucketIncarnationId': 'incarnation', 'poolIndex': 0, 'setIndex': 0},
+ 'disposition': {'state': 'verified_healthy' if version == 'v0' else state}} for version in ('v0', 'v1')]
+ failed = state == 'failed'
+ current = {'summary': 'stopped' if failed else 'finished', 'truncated': False,
+ 'outcome': {'execution': {'state': 'completed_with_errors' if failed else 'completed'},
+ 'coverage': 'complete', 'objectsTruncated': False, 'objects': objects,
+ 'counters': {'processed': 2, 'healed': 0, 'unchanged': 1, 'failed': int(failed),
+ 'skipped': int(not failed), 'unknown': int(state == 'unknown'),
+ 'attemptFailures': int(failed), 'overflowed': False}}}
+ return current, expected, target
+
+ def test_failed_deferred_and_unknown_remain_unresolved_without_unsafe_repair(self):
+ for state in ('failed', 'deferred', 'unknown'):
+ current, expected, target = self.negative(state)
+ self.assertFalse(network.validate_negative_task(current, 'owned', expected, target)['unsafeRepairClaimed'])
+ current, expected, target = self.negative()
+ for kind in ('healed', 'false-healthy', 'hidden-failure', 'foreign', 'duplicate', 'lost-target', 'uncounted-unknown'):
+ broken = copy.deepcopy(current)
+ objects, counts = broken['outcome']['objects'], broken['outcome']['counters']
+ if kind == 'healed': counts['healed'] = 1
+ elif kind == 'false-healthy': objects[-1]['disposition']['state'] = 'verified_healthy'
+ elif kind == 'hidden-failure': objects[-1]['disposition']['state'] = 'failed'
+ elif kind == 'foreign': objects[-1]['identity']['bucket'] = 'foreign'
+ elif kind == 'duplicate': objects[-1] = copy.deepcopy(objects[0])
+ elif kind == 'lost-target': objects.pop(); counts.update(processed=1, skipped=0)
+ else: objects[-1]['disposition']['state'] = 'unknown'
+ with self.subTest(kind=kind), self.assertRaises(ValueError):
+ network.validate_negative_task(broken, 'owned', expected, target)
+
+ def test_structured_abort_preserves_untraversable_responsibility_without_success(self):
+ current, expected, target = self.negative()
+ current['outcome'].update(execution={'state': 'aborted', 'reason': 'untraversable'}, coverage='partial', objects=[])
+ current['outcome']['counters'].update(processed=0, unchanged=0, skipped=0)
+ network.validate_negative_task(current, 'owned', expected, target)
+ current['outcome']['coverage'] = 'complete'
+ with self.assertRaises(ValueError): network.validate_negative_task(current, 'owned', expected, target)
+
+ def test_write_identity_mismatch_stays_pending_and_no_retry_is_issued(self):
+ row = {'key': 'unique', 'payload': {'size': 1, 'sha256': 'a'},
+ 'observed': {'acknowledged': True, 'resultUnknown': False, 'response': {'VersionId': 'ack-version'}}}
+ class Fleet:
+ def record(self, label, value): return value
+ observed = {'acknowledged': True, 'resultUnknown': False, 'payload': row['payload'], 'response': {'VersionId': 'foreign'}}
+ with patch.object(network, 'Probe') as probe, self.assertRaises(ValueError):
+ probe.return_value.s3.return_value = observed
+ network.resolve_writes(Fleet(), {'endpoints': {'node1': 'url', 'node2': 'url2'}}, 'bucket', [row])
+ self.assertNotIn('resolution', row)
+
+ def test_owned_address_allowlist_rejects_nonfleet_destinations(self):
+ gate.addresses(['10.0.0.1', '10.0.0.2', '10.0.0.3', '10.0.0.4'])
+ for address in ('127.0.0.1', '0.0.0.0', '169.254.1.1', '8.8.8.8', '224.0.0.1', '::1', '10.0.0.2'):
+ with self.subTest(address=address), self.assertRaises(ValueError):
+ gate.addresses([address, '10.0.0.2', '10.0.0.3', '10.0.0.4'])
+
+ def test_ipv6_alias_cannot_bypass_the_registered_ipv4_partition(self):
+ rows = [(gate.socket.AF_INET, gate.socket.SOCK_STREAM, 6, '', ('10.0.0.1', 9000)),
+ (gate.socket.AF_INET6, gate.socket.SOCK_STREAM, 6, '', ('fd00::1', 9000, 0, 0))]
+ with patch.object(gate.socket, 'getaddrinfo', return_value=rows), self.assertRaisesRegex(ValueError, 'unguarded IPv6'):
+ gate.resolve()
+
+ def table(self):
+ table, blocked = 'rv_i03_test', ['10.0.0.2', '10.0.0.3']
+ rows = []
+ for entry in gate.spec(table, blocked)['nftables']:
+ kind, value = next(iter(entry['add'].items()))
+ value = copy.deepcopy(value)
+ value['handle'] = len(rows) + 1
+ if kind == 'rule':
+ value['expr'][-2]['counter'] = {'packets': 3, 'bytes': 180}
+ rows.append({kind: value})
+ return {'nftables': [{'metainfo': {'json_schema_version': 1}}] + rows}, table, blocked
+
+ def test_exact_rules_exclude_ssh_and_reject_foreign_expressions_before_deletion(self):
+ raw, table, blocked = self.table()
+ counters = gate.validate_table(raw, table, blocked)
+ self.assertEqual(len(counters), 8)
+ for kind in ('ssh', 'address', 'accept', 'priority', 'foreign-rule', 'missing', 'chain'):
+ broken = copy.deepcopy(raw)
+ rule = next(row['rule'] for row in broken['nftables'] if 'rule' in row)
+ chain = next(row['chain'] for row in broken['nftables'] if 'chain' in row)
+ if kind == 'ssh': rule['expr'][1]['match']['right'] = 22
+ elif kind == 'address': rule['expr'][0]['match']['right'] = '10.0.0.9'
+ elif kind == 'accept': rule['expr'][-1] = {'accept': None}
+ elif kind == 'priority': chain['prio'] = -100
+ elif kind == 'foreign-rule': rule['comment'] = 'foreign'
+ elif kind == 'missing': broken['nftables'].pop()
+ else: chain['hook'] = 'forward'
+ with self.subTest(kind=kind), self.assertRaises(ValueError):
+ gate.validate_table(broken, table, blocked)
+
+ def test_all_peer_directions_and_packet_counter_hits_are_required(self):
+ snapshot, _, plan = self.scene()
+ nodes = list(snapshot)
+ observed = {node: {'state': 'ACTIVE', 'localIndex': index, 'expiryArmed': True, 'process': {'pid': 123},
+ 'probes': [{'peerIndex': other, 'service': {'connected': other not in plan['blocked'][node]},
+ 'management': {'connected': True}} for other in range(4) if other != index],
+ 'countersBefore': [{'packets': 0}], 'countersAfter': [{'packets': 1}]} for index, node in enumerate(nodes)}
+ network.validate_witness(observed, plan, nodes)
+ for kind in ('stopped-service', 'management-blocked', 'still-connected', 'missing-peer', 'no-hit', 'expired'):
+ broken = copy.deepcopy(observed)
+ first = broken[nodes[0]]
+ if kind == 'stopped-service': first['process']['pid'] = 0
+ elif kind == 'management-blocked': first['probes'][0]['management']['connected'] = False
+ elif kind == 'still-connected': first['probes'][-1]['service']['connected'] = True
+ elif kind == 'missing-peer': first['probes'].pop()
+ elif kind == 'no-hit': first['countersAfter'][0]['packets'] = 0
+ else: first['expiryArmed'] = False
+ with self.subTest(kind=kind), self.assertRaises(ValueError): network.validate_witness(broken, plan, nodes)
+
+ def test_source_branches_and_existing_dispatch_contract_remain_complete(self):
+ self.assertEqual(select(network.CASE_ID)[0]['variants'], list(network.BRANCHES))
+ self.assertIn(network.CASE_ID, AUTOMATIC_CASES)
+ source = Path('scripts/validation_heal/run_case.sh').read_text()
+ self.assertIn(network.CASE_ID + ') driver=network.py', source)
+
+
+class NetworkGetDiagnosticsTests(unittest.TestCase):
+ params = {'Bucket': 'owned', 'Key': 'objects/a', 'VersionId': 'exact-version'}
+
+ def fixture(self, *, changed_node=None, journal_failure=False):
+ class Fleet:
+ nodes = ('vm000', 'vm001', 'vm002', 'vm003')
+ def __init__(self):
+ self.sequence, self.evidence, self.calls, self.process_reads = 0, [], [], {}
+ def record(self, label, value):
+ self.sequence += 1
+ self.evidence.append((self.sequence, label, copy.deepcopy(value)))
+ return value
+ def remote(self, node, action, **params):
+ self.calls.append((node, action, params))
+ if action == 'process':
+ self.process_reads[node] = self.process_reads.get(node, 0) + 1
+ return {'pid': 123, 'startTicks': 7 + int(node == changed_node and self.process_reads[node] > 1),
+ 'bootId': 'boot', 'unit': 'owned.service', 'executable': '/owned/rustfs',
+ 'binarySha256': 'a' * 64, 'mountNamespace': 'mnt:1', 'commandSha256': 'command',
+ 'environmentFileSha256': 'environment', 'settings': {'RUSTFS_SCANNER_ENABLED': 'false'}}
+ if action != 'logs': raise AssertionError('diagnostics attempted a mutation: ' + action)
+ if params.get('checkpoint'):
+ return {'complete': True, 'afterCursor': node + '-before'}
+ if journal_failure: raise ValueError('original journal read failed')
+ return {'complete': True, 'beforeCursor': params['afterCursor'], 'afterCursor': node + '-after',
+ 'hasMore': False, 'journal': json.dumps({'message': 'original error', 'request_id': 'request-500'})}
+ observed = {'acknowledged': False, 'resultUnknown': False,
+ 'response': {'Error': {'Code': 'InternalError'}, 'ResponseMetadata': {'HTTPStatusCode': 500, 'RequestId': 'request-500'}}}
+ probe = SimpleNamespace(binding={'candidateBinarySha256': 'a' * 64}, endpoint_name='node4',
+ s3=unittest.mock.Mock(return_value=observed))
+ return Fleet(), probe, observed
+
+ def test_get500_keeps_four_cursor_windows_and_identity_before_oracle_failure(self):
+ fleet, probe, original = self.fixture()
+ observed, diagnostics = network.partition_get(probe, fleet, self.params)
+ self.assertIs(observed, original)
+ self.assertTrue(diagnostics['complete'])
+ self.assertEqual(diagnostics['requestId'], 'request-500')
+ self.assertFalse(diagnostics['causalAttributionClaimed'])
+ request = next(row for row in fleet.evidence if row[1] == 'partition-explicit-version-GET')
+ self.assertEqual(diagnostics['requestEvidenceSequence'], request[0])
+ windows = [row for row in fleet.evidence if row[1] == 'partition-GET-journal-window']
+ self.assertEqual(len(windows), 4)
+ self.assertTrue(all(row[0] > request[0] and 'original error' in row[2]['journal'] for row in windows))
+ self.assertEqual(fleet.process_reads, dict.fromkeys(fleet.nodes, 2))
+ probe.s3.assert_called_once_with('get_object', self.params)
+ with self.assertRaisesRegex(ValueError, 'below-quorum GET'):
+ network.validate_unrecoverable_get(observed, {'unrecoverableGet': {'httpStatus': 503, 'code': 'SlowDownRead'}})
+
+ def test_failed_journal_keeps_response_and_checks_every_after_process(self):
+ fleet, probe, original = self.fixture(journal_failure=True)
+ observed, diagnostics = network.partition_get(probe, fleet, self.params)
+ self.assertIs(observed, original)
+ self.assertFalse(diagnostics['complete'])
+ self.assertEqual(diagnostics['errors'][0]['stage'], 'journal')
+ self.assertEqual(fleet.process_reads, dict.fromkeys(fleet.nodes, 2))
+ probe.s3.assert_called_once()
+
+ def test_process_restart_cannot_certify_the_original_request(self):
+ fleet, probe, _ = self.fixture(changed_node='vm002')
+ _, diagnostics = network.partition_get(probe, fleet, self.params)
+ self.assertFalse(diagnostics['complete'])
+ self.assertEqual(diagnostics['errors'][0]['node'], 'vm002')
+ self.assertEqual(fleet.process_reads, dict.fromkeys(fleet.nodes, 2))
+ probe.s3.assert_called_once()
+
+ def test_request_exception_preserves_original_error_and_still_collects_diagnostics(self):
+ fleet, probe, _ = self.fixture()
+ probe.s3.side_effect = ValueError('original request exception')
+ with self.assertRaisesRegex(ValueError, '^original request exception$'):
+ network.partition_get(probe, fleet, self.params)
+ diagnostics = fleet.evidence[-1][2]
+ self.assertFalse(diagnostics['complete'])
+ self.assertIsNone(diagnostics['requestEvidenceSequence'])
+ self.assertEqual(fleet.process_reads, dict.fromkeys(fleet.nodes, 2))
+ self.assertEqual(len([row for row in fleet.evidence if row[1] == 'partition-GET-journal-window']), 4)
+
+ def test_wrong_candidate_cannot_send_a_diagnostic_get(self):
+ fleet, probe, _ = self.fixture()
+ probe.binding['candidateBinarySha256'] = 'b' * 64
+ with self.assertRaisesRegex(ValueError, 'live candidate process'):
+ network.partition_get(probe, fleet, self.params)
+ probe.s3.assert_not_called()
+
+
+class NetworkScopedContractTests(unittest.TestCase):
+ def contract(self):
+ source = '1c7fc818dd858247c45b39542abd8291b1e6e31b'
+ return {'candidateCommit': source, 'recognized': True,
+ 'sourceFiles': {'rustfs/src/error.rs': network.GET_SOURCE_FILES[source]['rustfs/src/error.rs']},
+ 'unrecoverableGet': {'httpStatus': 503, 'code': 'SlowDownRead'}}
+
+ def test_lock_and_data_losses_require_different_exact503_codes(self):
+ for clients, expected in [(1, 'ServiceUnavailable'), (2, 'SlowDownRead'), (3, 'SlowDownRead')]:
+ contract = network.negative_get_contract(self.contract(), {'lockClients': clients}, {'sharedLockQuorum': 2})
+ for status, code in [(503, 'ServiceUnavailable'), (503, 'SlowDownRead'), (500, 'InternalError')]:
+ observed = {'acknowledged': False, 'resultUnknown': False,
+ 'response': {'Error': {'Code': code}, 'ResponseMetadata': {'HTTPStatusCode': status}}}
+ with self.subTest(clients=clients, status=status, code=code):
+ if status == 503 and code == expected:
+ network.validate_unrecoverable_get(observed, contract)
+ else:
+ with self.assertRaises(ValueError): network.validate_unrecoverable_get(observed, contract)
+
+ def test_foreign_source_or_unknown_lock_domain_cannot_select_an_error(self):
+ for kind in ('source', 'digest', 'unrecognized', 'boolean', 'quorum'):
+ contract, side, plan = self.contract(), {'lockClients': 1}, {'sharedLockQuorum': 2}
+ if kind == 'source': contract['candidateCommit'] = 'f' * 40
+ elif kind == 'digest': contract['sourceFiles']['rustfs/src/error.rs'] = 'f' * 64
+ elif kind == 'unrecognized': contract['recognized'] = False
+ elif kind == 'boolean': side['lockClients'] = True
+ else: plan['sharedLockQuorum'] = 1
+ with self.subTest(kind=kind), self.assertRaises(ValueError): network.negative_get_contract(contract, side, plan)
+
+ def test_negative_heal_requests_both_versions_without_changing_safe_refusal(self):
+ fleet = SimpleNamespace(record=lambda label, value: value)
+ probe = SimpleNamespace(admin=unittest.mock.Mock(return_value={'httpStatus': 503,
+ 'response': {'Code': 'ServiceUnavailable'}}))
+ result = network.negative_heal(probe, fleet, 'owned', [], {'key': 'objects/a'}, [])
+ self.assertTrue(json.loads(probe.admin.call_args.args[2])['recursive'])
+ self.assertTrue(result['admissionRefused'])
+ self.assertFalse(result['repairClaimed'])
+
+ def test_recovery_requests_every_historical_and_null_version(self):
+ status, expected, target = NetworkTests().negative('repaired')
+ expected = [{**entry, 'deleteMarker': False} for entry in expected]
+ for version in ('v2', 'null'):
+ expected.append({'key': 'objects/a', 'versionId': version, 'deleteMarker': False})
+ status['outcome']['objects'].append({'identity': {'bucket': 'owned', 'object': 'objects/a',
+ 'versionId': None if version == 'null' else version, 'bucketIncarnationId': 'incarnation',
+ 'poolIndex': 0, 'setIndex': 0}, 'disposition': {'state': 'verified_healthy'}})
+ status['outcome']['counters'].update(processed=4, healed=1, unchanged=3, skipped=0)
+ binding = {'endpoints': {'node1': 'one', 'node2': 'two', 'node3': 'three', 'node4': 'four'}}
+ for target_version in ('v1', 'null'):
+ for missing in (False, True):
+ current = copy.deepcopy(status)
+ for row in current['outcome']['objects']:
+ row['disposition']['state'] = ('repaired' if network.public_version_id(row['identity']['versionId'])
+ == target_version else 'verified_healthy')
+ if missing:
+ current['outcome']['objects'] = current['outcome']['objects'][1:]
+ current['outcome']['counters'].update(processed=3, unchanged=2)
+ selected = {**target, 'versionId': target_version}
+ with self.subTest(target=target_version, missing=missing), patch.object(network, 'run_task', return_value=current) as task:
+ if missing:
+ with self.assertRaisesRegex(ValueError, 'omitted or duplicated'):
+ network.recover_versions(None, None, binding, 'owned', expected, selected, [])
+ else:
+ proof = network.recover_versions(None, None, binding, 'owned', expected, selected, [])
+ self.assertEqual((proof['receipts'], proof['repaired']), (4, 1))
+ self.assertEqual(task.call_args.kwargs['option_overrides'], {'recursive': True})
+ self.assertEqual(task.call_args.kwargs['status_endpoints'], ('node2', 'node3', 'node4'))
+
+
+
+class NetworkVersionFixtureTests(unittest.TestCase):
+ def fixture(self, *, wrong_ack=False):
+ class Recorder:
+ def __init__(self):
+ self.mode, self.rows, self.calls, self.nonce = None, [], [], 0
+ self.probe = SimpleNamespace(binding={'bucketPrefix': 'owned'}, inventory=self.inventory)
+ def inventory(self, bucket):
+ return {'entries': copy.deepcopy(self.rows)}
+ def call(self, operation, params, body=None):
+ self.calls.append((operation, copy.deepcopy(params)))
+ self.nonce += 1
+ if operation == 'put_bucket_versioning':
+ self.mode = params['VersioningConfiguration']['Status']
+ if operation == 'get_bucket_versioning': return {'Status': self.mode}
+ if operation not in {'put_object', 'delete_object'}: return {}
+ key = params['Key']
+ if operation == 'delete_object' and 'VersionId' in params:
+ self.rows = [entry for entry in self.rows if (entry['key'], entry['versionId']) != (key, params['VersionId'])]
+ same = [entry for entry in self.rows if entry['key'] == key]
+ if same: same[-1]['isLatest'] = True
+ return {'VersionId': params['VersionId'], 'DeleteMarker': True}
+ version = f'v{self.nonce}' if self.mode == 'Enabled' else 'null'
+ if version == 'null':
+ self.rows = [entry for entry in self.rows if (entry['key'], entry['versionId']) != (key, 'null')]
+ for entry in self.rows:
+ if entry['key'] == key: entry['isLatest'] = False
+ marker = operation == 'delete_object'
+ entry = {'key': key, 'versionId': version, 'isLatest': True, 'deleteMarker': marker}
+ if body is not None: entry['payload'] = {'size': len(body), 'sha256': hashlib.sha256(body).hexdigest()}
+ self.rows.append(entry)
+ return {'VersionId': 'null' if wrong_ack and not marker else version,
+ 'DeleteMarker': marker, 'ResponseMetadata': {'HTTPStatusCode': 204 if marker else 200}}
+ recorder = Recorder()
+ evidence = []
+ fleet = SimpleNamespace(record=lambda label, value: evidence.append((label, copy.deepcopy(value))) or value)
+ return recorder, fleet, evidence
+
+ def seed(self):
+ recorder, fleet, evidence = self.fixture()
+ with patch.object(network, 'SIZE', 97), patch('scripts.validation_heal.versions.VERSION_SIZE', 123):
+ expected = network.seed_versions(recorder, fleet, 'owned')
+ return expected, recorder, evidence
+
+ def test_real_seed_uses_three_versions_and_suspended_null_delete_ack_sequence(self):
+ expected, recorder, evidence = self.seed()
+ coverage = network.version_coverage(expected)
+ self.assertEqual(len(expected), 19)
+ self.assertEqual(recorder.mode, 'Enabled')
+ self.assertEqual(len([entry for entry in expected if not entry['deleteMarker']]), 15)
+ self.assertFalse(coverage['suspendedPartitionClaimed'])
+ self.assertFalse(coverage['canonicalCasePassClaimed'])
+ self.assertEqual(len([params for op, params in recorder.calls if op == 'delete_object' and params.get('VersionId') == 'null']), 4)
+ suspended = next(value for label, value in evidence if label == 'suspended-null-ACK-manifest')
+ self.assertTrue(suspended['seedAckAndFullGetAgree'])
+ self.assertEqual(len(suspended['expected']), 18)
+ self.assertEqual([params['VersioningConfiguration']['Status'] for op, params in recorder.calls
+ if op == 'put_bucket_versioning'], ['Enabled', 'Suspended', 'Enabled'])
+ target_versions = [entry for entry in expected if entry['key'] == 'objects/0001.bin']
+ self.assertEqual(len(target_versions), 4)
+ self.assertTrue(any(entry['versionId'] == 'null' and entry['isLatest'] for entry in target_versions))
+
+ def test_missing_fixture_dimensions_or_duplicate_identities_cannot_claim_coverage(self):
+ expected, _, _ = self.seed()
+ for kind in ('third-version', 'duplicate', 'same-content', 'current-marker', 'historical-marker', 'null', 'latest'):
+ broken = copy.deepcopy(expected)
+ if kind == 'third-version': broken.pop(0)
+ elif kind == 'duplicate': broken.append(copy.deepcopy(broken[0]))
+ elif kind == 'same-content': broken[1]['payload'] = broken[0]['payload']
+ elif kind == 'current-marker':
+ broken = [entry for entry in broken if not (entry['deleteMarker'] and entry['isLatest'])]
+ elif kind == 'historical-marker':
+ broken = [entry for entry in broken if not (entry['deleteMarker'] and not entry['isLatest'])]
+ elif kind == 'null': broken = [entry for entry in broken if entry['versionId'] != 'null']
+ else: broken[0]['isLatest'] = True
+ with self.subTest(kind=kind), self.assertRaises(ValueError): network.version_coverage(broken)
+
+ def test_seed_refuses_missing_enabled_version_ack_before_faults(self):
+ recorder, fleet, _ = self.fixture(wrong_ack=True)
+ with patch.object(network, 'SIZE', 97), self.assertRaisesRegex(ValueError, 'real version ID'):
+ network.seed_versions(recorder, fleet, 'owned')
+ self.assertEqual(len([op for op, params in recorder.calls if op == 'put_object']), 1)
+
+ def test_two_version_or_foreign_target_recovery_fails_before_task_admission(self):
+ expected, _, _ = self.seed()
+ versions = [entry for entry in expected if entry['key'] == 'objects/0001.bin']
+ for selected, target in [(versions[:2], versions[0]), (versions, {'key': versions[0]['key'], 'versionId': 'foreign'})]:
+ with patch.object(network, 'run_task') as task, self.assertRaises(ValueError):
+ network.recover_versions(None, None, {'endpoints': {}}, 'owned', selected, target, [])
+ task.assert_not_called()
+
+ def test_head_identity_length_and_last_modified_are_required(self):
+ entry = {'key': 'objects/a', 'versionId': 'v1', 'deleteMarker': False, 'payload': {'size': 123}}
+ original = {'acknowledged': True, 'resultUnknown': False,
+ 'response': {'VersionId': 'v1', 'ContentLength': 123, 'ETag': 'etag', 'LastModified': 'original-time',
+ 'ResponseMetadata': {'HTTPStatusCode': 200}}}
+ fleet = SimpleNamespace(record=lambda label, value: value)
+ for kind in ('valid', 'unknown', 'wrong-version', 'short', 'partial', 'no-modification-time', 'no-etag'):
+ observed = copy.deepcopy(original)
+ if kind == 'unknown': observed['resultUnknown'] = True
+ elif kind == 'wrong-version': observed['response']['VersionId'] = 'v2'
+ elif kind == 'short': observed['response']['ContentLength'] = 122
+ elif kind == 'partial': observed['response']['ResponseMetadata']['HTTPStatusCode'] = 206
+ elif kind == 'no-modification-time': observed['response'].pop('LastModified')
+ elif kind == 'no-etag': observed['response'].pop('ETag')
+ probe = SimpleNamespace(s3=unittest.mock.Mock(return_value=observed))
+ with self.subTest(kind=kind):
+ if kind == 'valid': self.assertEqual(len(network.version_heads(probe, fleet, 'owned', [entry], 'head')), 1)
+ else:
+ with self.assertRaises(ValueError): network.version_heads(probe, fleet, 'owned', [entry], 'head')
+ probe.s3.assert_called_once_with('head_object', {'Bucket': 'owned', 'Key': 'objects/a', 'VersionId': 'v1'})
+
+
+class NetworkExpiryTests(unittest.TestCase):
+ owner = {'runId': 'gh-123-1-h2', 'hostname': 'node1', 'unit': 'owned.service',
+ 'runtimeProfile': 'lock-quorum', 'binarySha256': 'a' * 64}
+ peers = ['10.17.0.1', '10.17.0.2', '10.17.0.3', '10.17.0.4']
+
+ def prepare(self, folder, *, timer_active=True):
+ calls = []
+ def command(*args, input=None):
+ calls.append((args, input))
+ if args[:4] == ('nft', '-j', 'list', 'tables'): return '{"nftables":[]}'
+ if 'LoadState' in args: return 'not-found'
+ if 'ActiveState' in args: return 'active' if timer_active else 'inactive'
+ return ''
+ source = Path(gate.__file__).read_text()
+ with patch.object(gate, 'command', side_effect=command), patch.object(gate, 'resolve', return_value=self.peers), patch.object(gate.socket, 'socket'):
+ value = gate.prepare(folder, self.owner, {'addresses': self.peers, 'localIndex': 0, 'blockedIndices': [2, 3]}, source)
+ return value, calls
+
+ def test_independent_expiry_is_armed_and_intent_is_durable_before_any_rule_mutation(self):
+ with tempfile.TemporaryDirectory() as folder:
+ state = Path(folder)
+ value, calls = self.prepare(state)
+ self.assertEqual(value['state'], 'ARMED')
+ self.assertEqual(json.loads((state / gate.MARKER).read_text())['phase'], 'armed')
+ self.assertTrue((state / gate.SCRIPT).is_file())
+ self.assertEqual((state / gate.MARKER).stat().st_mode & 0o777, 0o600)
+ self.assertTrue(any(args[0] == 'systemd-run' for args, _ in calls))
+ self.assertFalse(any(args[:4] == ('nft', '-j', '-f', '-') for args, _ in calls))
+
+ def test_failed_timer_keeps_undo_intent_and_never_installs_rules(self):
+ with tempfile.TemporaryDirectory() as folder:
+ state = Path(folder)
+ with self.assertRaisesRegex(ValueError, 'timer was not armed'):
+ self.prepare(state, timer_active=False)
+ self.assertTrue((state / gate.MARKER).is_file())
+ self.assertEqual(json.loads((state / gate.MARKER).read_text())['phase'], 'armed')
+
+ def test_lost_timer_refuses_activation_before_touching_nft(self):
+ with tempfile.TemporaryDirectory() as folder:
+ value, _ = self.prepare(Path(folder))
+ with patch.object(gate, 'read_marker', return_value=value), patch.object(gate, 'table_present', return_value=False), \
+ patch.object(gate, 'command', return_value='inactive') as command, self.assertRaises(ValueError):
+ gate.activate(Path(folder), self.owner)
+ self.assertTrue(all(call.args[0] != 'nft' for call in command.call_args_list))
+
+ def release_fixture(self, state, *, service_loaded=False, timer_loaded=True, stop_fails=False):
+ value, _ = self.prepare(state)
+ states = {'.timer': 'active' if timer_loaded else 'inactive', '.service': 'active' if service_loaded else 'inactive'}
+ calls = []
+ def command(*args, input=None):
+ calls.append(args)
+ if args[:2] == ('systemctl', 'show'):
+ suffix = '.timer' if args[2].endswith('.timer') else '.service'
+ loaded = timer_loaded if suffix == '.timer' else service_loaded
+ if 'LoadState' in args: return 'loaded' if loaded else 'not-found'
+ return states[suffix]
+ if args[:2] == ('systemctl', 'stop'):
+ for unit in args[2:]:
+ suffix = '.timer' if unit.endswith('.timer') else '.service'
+ if suffix == '.service' and not service_loaded:
+ raise ValueError('systemctl network operation failed: Unit expiry.service not loaded')
+ if stop_fails: raise ValueError('systemctl network operation failed: stop refused')
+ states[suffix] = 'inactive'
+ return ''
+ return value, command, calls
+
+ def test_release_stops_timer_when_its_service_has_never_been_loaded(self):
+ with tempfile.TemporaryDirectory() as folder:
+ state = Path(folder)
+ value, command, calls = self.release_fixture(state)
+ with patch.object(gate, 'read_marker', return_value=value), patch.object(gate, 'command', side_effect=command), patch.object(gate, 'table_present', return_value=False):
+ result = gate.release(state, self.owner)
+ self.assertEqual(result['rulesRemaining'], 0)
+ self.assertFalse(result['expiryTriggered'])
+ self.assertIn(('systemctl', 'stop', value['timer'] + '.timer'), calls)
+ self.assertFalse(any(value['timer'] + '.service' in args for args in calls if args[:2] == ('systemctl', 'stop')))
+ self.assertFalse((state / gate.MARKER).exists())
+
+ def test_release_stops_an_active_worker_after_stopping_its_timer(self):
+ with tempfile.TemporaryDirectory() as folder:
+ state = Path(folder)
+ value, command, calls = self.release_fixture(state, service_loaded=True)
+ with patch.object(gate, 'read_marker', return_value=value), patch.object(gate, 'command', side_effect=command), patch.object(gate, 'table_present', return_value=False):
+ gate.release(state, self.owner)
+ stops = [args[2:] for args in calls if args[:2] == ('systemctl', 'stop')]
+ self.assertEqual(stops, [(value['timer'] + '.timer',), (value['timer'] + '.service',)])
+
+ def test_missing_normal_timer_or_failed_stop_preserves_intent_and_rules(self):
+ for config in ({'timer_loaded': False}, {'stop_fails': True}):
+ with self.subTest(config=config), tempfile.TemporaryDirectory() as folder:
+ state = Path(folder)
+ value, command, calls = self.release_fixture(state, **config)
+ with patch.object(gate, 'read_marker', return_value=value), patch.object(gate, 'command', side_effect=command), patch.object(gate, 'table_present', return_value=True), self.assertRaises(ValueError):
+ gate.release(state, self.owner)
+ self.assertTrue((state / gate.MARKER).exists())
+ self.assertFalse(any(args[:4] == ('nft', '-j', '-f', '-') for args in calls))
+
+ def test_explicit_recovery_accepts_unloaded_timer_and_stops_any_remaining_worker(self):
+ for service_loaded in (False, True):
+ with self.subTest(service_loaded=service_loaded), tempfile.TemporaryDirectory() as folder:
+ state = Path(folder)
+ value, command, calls = self.release_fixture(state, timer_loaded=False, service_loaded=service_loaded)
+ with patch.object(gate, 'read_marker', return_value=value), patch.object(gate, 'command', side_effect=command), patch.object(gate, 'table_present', return_value=False):
+ result = gate.release(state, self.owner, expired=True)
+ self.assertTrue(result['expiryTriggered'])
+ self.assertEqual(result['rulesRemaining'], 0)
+ self.assertTrue((state / gate.EXPIRY).is_file())
+ self.assertFalse((state / gate.MARKER).exists())
+ self.assertFalse(any(value['timer'] + '.timer' in args for args in calls if args[:2] == ('systemctl', 'stop')))
+
+ def test_foreign_rule_blocks_cleanup_instead_of_deleting_the_table(self):
+ with tempfile.TemporaryDirectory() as folder:
+ state = Path(folder)
+ value, _ = self.prepare(state)
+ broken = {'nftables': [{'table': {'family': 'inet', 'name': value['table']}},
+ {'rule': {'family': 'inet', 'table': value['table'], 'chain': 'input',
+ 'comment': 'foreign', 'expr': [{'accept': None}]}}]}
+ def command(*args, input=None):
+ if args[:4] == ('nft', '-j', 'list', 'table'): return json.dumps(broken)
+ if 'LoadState' in args: return 'loaded'
+ if 'ActiveState' in args: return 'inactive'
+ return ''
+ with patch.object(gate, 'read_marker', return_value=value), patch.object(gate, 'table_present', return_value=True), \
+ patch.object(gate, 'command', side_effect=command) as observed, self.assertRaises(ValueError):
+ gate.release(state, self.owner)
+ self.assertTrue((state / gate.MARKER).exists())
+ self.assertFalse(any(call.args[:4] == ('nft', '-j', '-f', '-') for call in observed.call_args_list))
+
+
+class NetworkKernelTests(unittest.TestCase):
+ @unittest.skipUnless(sys.platform == 'linux', 'requires an isolated Linux network namespace')
+ def test_real_nft_json_round_trip_and_foreign_rule_refusal(self):
+ self.assertIsNotNone(shutil.which('nft'), 'Linux CI must install nftables; this gate may not skip silently')
+ program = '''import copy,json
+from scripts.validation_heal import network_gate as gate
+table='rv_i03_kernel_test'
+peers=['10.17.0.2','10.17.0.3']
+spec=gate.spec(table,peers)
+gate.command('nft','-j','-c','-f','-',input=json.dumps(spec))
+gate.command('nft','-j','-f','-',input=json.dumps(spec))
+raw=json.loads(gate.command('nft','-j','list','table','inet',table))
+assert len(gate.validate_table(raw,table,peers))==8
+foreign={'nftables':[{'add':{'rule':{'family':'inet','table':table,'chain':'input','expr':[{'accept':None}]}}}]}
+gate.command('nft','-j','-f','-',input=json.dumps(foreign))
+try:
+ gate.validate_table(json.loads(gate.command('nft','-j','list','table','inet',table)),table,peers)
+except ValueError:
+ pass
+else:
+ raise AssertionError('foreign rule was accepted')
+gate.command('nft','-j','-f','-',input=json.dumps({'nftables':[{'delete':{'table':{'family':'inet','name':table}}}]}))
+assert not gate.table_present(table)
+print('PASS: actual private-network kernel rules and deletion guards')
+'''
+ command = ['unshare', '--net', sys.executable, '-B', '-c', program]
+ if os.geteuid() != 0: command = ['sudo', '-n', *command]
+ result = subprocess.run(command, capture_output=True, text=True, timeout=30)
+ self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
+ self.assertIn('PASS: actual private-network kernel rules', result.stdout)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/test_validation_heal_network_adapter.py b/test_validation_heal_network_adapter.py
new file mode 100644
index 00000000..2f720e9b
--- /dev/null
+++ b/test_validation_heal_network_adapter.py
@@ -0,0 +1,202 @@
+"""Execute live-adapter control flow with modeled services; no live acceptance."""
+
+import copy
+from contextlib import ExitStack
+import json
+import os
+from pathlib import Path
+import tempfile
+import types
+import unittest
+from unittest.mock import patch
+
+from scripts.validation_heal import network
+from scripts.validation_heal.mpu_scene import keys_for
+from scripts.validation_heal.network_load_repair import CANDIDATE
+
+
+class NetworkAdapterTests(unittest.TestCase):
+ def run_mode(self, directory, mode):
+ events, state = [], {'running': False, 'recovered': None, 'session': None}
+ root = Path(directory);candidate = root / 'candidate.json';binary = root / 'candidate'
+ candidate.write_text(json.dumps({'source_sha': CANDIDATE, 'package_sha256': 'b' * 64}));binary.write_bytes(b'candidate')
+ args = types.SimpleNamespace(evidence=root / 'evidence', candidate=candidate, rc_bin=binary,
+ tool_directory=root, testing_commit='c' * 40, binary_sha='d' * 64)
+ nodes = ['vm000', 'vm001', 'vm002', 'vm003']
+ payload = {'size': 100, 'sha256': 'original'}
+ def entry(key, version, latest):
+ return {'key': key, 'versionId': version, 'deleteMarker': False, 'isLatest': latest, 'payload': payload.copy()}
+ dv = [entry('objects/0001.bin', version, version == 'null') for version in ('v0', 'v1', 'v2', 'null')]
+ dl_expected = [entry(f'dl/{endpoint}/control.bin', f'{endpoint}-v{i}', i == 2)
+ for endpoint in ('node1', 'node3') for i in range(3)]
+ fixture = {'expected': dl_expected, 'sides': {endpoint: {'controlKey': f'dl/{endpoint}/control.bin',
+ 'versions': [row for row in dl_expected if row['key'] == f'dl/{endpoint}/control.bin']}
+ for endpoint in ('node1', 'node3')}}
+ class Fleet:
+ def __init__(self, nodes, evidence, run_id, *args, **kwargs):
+ self.nodes, self.evidence, self.run_id = nodes, evidence, run_id
+ self.network_gates, self.quorum_gates, self.faults = set(), set(), []
+ self.reserved = nodes[:]
+ def record(self, label, value): events.append(('record', label));return value
+ def deploy(self, *args): state['running'] = True;events.append(('deploy',))
+ def start(self): state['running'] = True;events.append(('start',))
+ def stop(self): state['running'] = False;events.append(('stop',))
+ def snapshot(self, label, **kwargs):
+ events.append(('snapshot', label, copy.deepcopy(kwargs)))
+ value = {node: {'files': [], 'formats': []} for node in nodes}
+ if mode == 'business-gate-drift' and label == 'DL-original-unresolved-business-scene':
+ value[nodes[0]]['files'].append({'disk': 0, 'path': state['session'].bucket + '/unexpected-part',
+ 'size': 1, 'sha256': 'changed-during-gate-release'})
+ return value
+ def remote(self, node, action, **params):
+ events.append(('remote', node, action))
+ if action == 'network-preflight':
+ return {'addresses': ['10.0.0.' + str(i) for i in range(1, 5)], 'localIndex': nodes.index(node), 'exclusiveServicePort': 9000}
+ if action == 'quorum-gate-probe':
+ return {'disks': [{'writable': True, 'readableMetadata': True, 'sameServiceUidAndMountNamespace': True}] * 4}
+ return {'state': 'CLEAR', 'rulesRemaining': 0, 'expiryTriggered': False}
+ def cleanup(self):
+ assert state['session'].safe_to_cleanup
+ events.append(('cleanup',));return True
+ class Probe:
+ def __init__(self, binding, endpoint='node1'): self.endpoint_name, self.binding = endpoint, binding
+ def inventory(self, bucket, **kwargs):
+ events.append(('inventory', bucket))
+ rows = [] if bucket.endswith('-db') else copy.deepcopy(dv + dl_expected)
+ if state['recovered'] and not bucket.endswith('-db'):
+ row = state['recovered']
+ rows.append({**entry(row['key'], row['versionId'], True), 'payload': row['payload'],
+ 'properties': {'Metadata': {'heal-fixture': 'I03', 'nonce': row['key']}},
+ 'tags': [{'Key': 'fixture', 'Value': 'I03'}, {'Key': 'suite', 'Value': 'heal'}]})
+ return {'bucket': bucket, 'entries': rows}
+ def admin(self, *args): return {'httpStatus': 200, 'response': {}}
+ test = self
+ class Session:
+ def __init__(self, binding, bucket, endpoints, directory):
+ self.binding, self.bucket, self.endpoints = binding, bucket, tuple(endpoints)
+ self.keys, self.safe_to_cleanup = keys_for(endpoints), False
+ self.fixture, self.stages = fixture, []
+ state['session'] = self;events.append(('register-session',))
+ def seed(self):
+ events.append(('seed-DL',))
+ if mode == 'seed-unknown': raise ValueError('original seed Create result UNKNOWN')
+ return fixture
+ def register_baseline(self, value):
+ test.assertEqual({(r['key'], r['versionId']) for r in value['entries']},
+ {(r['key'], r['versionId']) for r in dv + dl_expected})
+ events.append(('freeze-public',))
+ def register_business_baseline(self, value):
+ test.assertFalse(state['running']);events.append(('freeze-business',))
+ def capture_mpu(self, fleet, stage):
+ test.assertFalse(state['running']);self.stages.append(stage)
+ events.append(('capture-MPU', stage))
+ value = {'nodes': {n: {'sceneSha256': 'f' * 64} for n in nodes}}
+ if mode == 'mpu-gate-drift' and self.stages.count('failure') == 2:
+ value['nodes'][nodes[0]]['sceneSha256'] = 'e' * 64
+ return value
+ def run_load(self, readable):
+ test.assertTrue(state['running']);test.assertEqual(readable, {'node1': True, 'node3': True})
+ events.append(('load-inside-partition',))
+ return {'complete': mode == 'pass'}
+ def registered_receipts(self): return {'original': 'preserved'}
+ def reconcile(self):
+ test.assertTrue(state['running']);test.assertEqual(self.stages[-1], 'pre-reconcile')
+ events.append(('reconcile-original-effects',));return {'namespaceCleanupAllowed': False}
+ def finish_reconciliation(self, scene, *, repair_targets):
+ test.assertFalse(state['running']);test.assertEqual(self.stages[-1], 'post-reconcile')
+ events.append(('close-original-effects',));self.safe_to_cleanup = True
+ return {'namespaceCleanupAllowed': True}
+ def map_target(snapshot, bucket, key, role, number, version, **kwargs):
+ return {'bucket': bucket, 'key': key, 'versionId': version,
+ 'node': 'vm000', 'disk': 0, 'relativePath': bucket + '/' + key + '/part.1', 'sha256': 'original',
+ 'cohort': [{'node': n, 'disk': 0} for n in nodes]}
+ def unique(probe, directory, bucket, key, rows):
+ events.append(('unique-PUT', key));test.assertTrue(state['running'])
+ if key.startswith('recovered/'):
+ test.assertTrue(state['session'].safe_to_cleanup)
+ row = {'key': key, 'versionId': 'new-original-ACK', 'endpoint': probe.endpoint_name, 'payload': payload.copy(),
+ 'observed': {'acknowledged': key.startswith('recovered/'), 'response': {'VersionId': 'new-original-ACK'}}}
+ rows.append(row)
+ if key.startswith('recovered/'): state['recovered'] = row
+ return row
+ def resolve(fleet, binding, bucket, rows):
+ for row in rows: row['resolution'] = {'state': 'REDEEMED' if row['observed']['acknowledged'] else 'ABSENT'}
+ return rows
+ def retain(fleet, binding, keys, business, mpu):
+ test.assertFalse(state['running']);test.assertFalse(fleet.network_gates or fleet.quorum_gates)
+ events.append(('freeze-retention-intent',));return {'original': 'intent'}
+ def isolate(fleet, plan):
+ events.append(('isolate-original-inodes',))
+ if mode == 'isolation-error': raise ValueError('retained bytes drifted')
+ return {'cleanup': 'ISOLATED', 'originalSceneRestored': False}
+ plan = {'readQuorum': 8, 'sharedLockQuorum': 2, 'exclusiveLockQuorum': 3, 'gates': {},
+ 'blocked': {node: [] for node in nodes}, 'sides': [
+ {'endpoint': endpoint, 'members': members, 'metadataAuthorities': 8,
+ 'readableSources': 8 if index == 0 else 7, 'writableDrives': 8, 'lockClients': 2}
+ for index, (endpoint, members) in enumerate((('node1', [0, 1]), ('node3', [2, 3])))]}
+ replacements = {'Fleet': Fleet, 'Probe': Probe, 'LoadSession': Session, 'Recorder': lambda *args: None,
+ 'verify_inspector': lambda *args: {}, 'verify_quorum_contract': lambda *args: {
+ 'candidateCommit': CANDIDATE, 'recognized': True, 'sourceFiles': network.GET_SOURCE_FILES[CANDIDATE],
+ 'unrecoverableGet': {'httpStatus': 503, 'code': 'SlowDownRead'}},
+ 'seed_versions': lambda *args: copy.deepcopy(dv), 'db_parameters': lambda *args: {'count': 128},
+ 'seed_db': lambda *args: [], 'verify_seed': lambda *args: {}, 'version_heads': lambda *args: [],
+ 'validate_topology': lambda *args: {}, 'map_target': map_target, 'map_db': lambda *args: ([], []),
+ 'plan_for': lambda *args: plan, 'validate_fault': lambda *args: {},
+ 'validate_witness': lambda *args: {}, 'expected_reads': lambda *args: [],
+ 'partition_get': lambda *args: ({'acknowledged': True, 'resultUnknown': False, 'payload': payload.copy()}, {'complete': True}),
+ 'validate_unrecoverable_get': lambda *args: None, 'unique_put': unique, 'negative_put': lambda *args: {},
+ 'partition_db': lambda *args: {}, 'negative_heal': lambda *args: {}, 'validate_partial': lambda *args: {},
+ 'classify_deletes': lambda *args: {'policies': {}, 'repairTargets': []},
+ 'recover_versions': lambda *args: {}, 'recover_controls': lambda *args: {}, 'idle_fleet': lambda *args: None,
+ 'validate_repaired_physical': lambda *args, **kwargs: {},
+ 'verify_cohort': lambda fleet, cohort, label: events.append(('streaming-cohort', cohort['key'], cohort['versionId'])),
+ 'prove_db': lambda *args: {}, 'resolve_writes': resolve, 'compare_inventories': lambda *args: {},
+ 'verify_db': lambda *args: {}, 'prepare_retention': retain, 'isolate_retention': isolate}
+ with ExitStack() as stack:
+ stack.enter_context(patch.dict(os.environ, {'GITHUB_RUN_ID': '12345', 'GITHUB_RUN_ATTEMPT': '1'}))
+ for name, value in replacements.items(): stack.enter_context(patch.object(network, name, value))
+ result = network.run_branch(args, nodes, 'read-boundary')
+ return result, events
+
+ def test_complete_adapter_keeps_all_dl_versions_physical_then_closes_before_new_safe_put(self):
+ with tempfile.TemporaryDirectory() as d: result, events = self.run_mode(d, 'pass')
+ self.assertEqual((result['verdict'], result['cleanup']), ('PASS', 'PASS'))
+ cohorts = [event for event in events if event[0] == 'streaming-cohort' and event[1].startswith('dl/')]
+ self.assertEqual(len(cohorts), 6)
+ self.assertLess(events.index(('freeze-business',)), events.index(('load-inside-partition',)))
+ self.assertLess(events.index(('load-inside-partition',)), events.index(('record', 'all-directed-peer-probes-after')))
+ self.assertLess(events.index(('streaming-cohort', 'dl/node3/control.bin', 'node3-v2')),
+ events.index(('reconcile-original-effects',)))
+ self.assertLess(events.index(('close-original-effects',)), events.index(('unique-PUT', 'recovered/unique.bin')))
+ self.assertEqual([e for e in events if e[0] == 'cleanup'], [('cleanup',)])
+ self.assertFalse(any(e[0] == 'isolate-original-inodes' for e in events))
+ for event in events:
+ if event[0] == 'snapshot' and event[1] != 'DB-size-calibration-live':
+ self.assertEqual(len(event[2]['buckets']), 2)
+
+ def test_unknown_seed_or_load_never_runs_ordinary_cleanup_or_retries_original_mutation(self):
+ for mode in ('seed-unknown', 'unknown-load', 'isolation-error', 'business-gate-drift', 'mpu-gate-drift'):
+ with self.subTest(mode=mode), tempfile.TemporaryDirectory() as d:
+ result, events = self.run_mode(d, mode)
+ self.assertEqual((result['verdict'], result['cleanup']), ('FAIL', 'FAIL'))
+ self.assertNotIn(('cleanup',), events)
+ self.assertNotIn(('unique-PUT', 'recovered/unique.bin'), events)
+ self.assertNotIn(('reconcile-original-effects',), events)
+ if mode.endswith('gate-drift'):
+ self.assertNotIn(('isolate-original-inodes',), events)
+ self.assertIn('gate recovery changed', result['retentionError']['message'])
+ else:
+ self.assertIn(('isolate-original-inodes',), events)
+ if mode == 'seed-unknown':
+ self.assertFalse(any(event[0] == 'inventory' for event in events))
+ if mode == 'isolation-error': self.assertIn('retentionError', result)
+ elif not mode.endswith('gate-drift'): self.assertEqual(result['retention']['cleanup'], 'ISOLATED')
+ if mode != 'seed-unknown':
+ before = next(i for i, event in enumerate(events) if event[:2] ==
+ ('snapshot', 'DL-original-unresolved-business-before-gate-release'))
+ releases = [i for i, event in enumerate(events) if event[0] == 'remote' and event[2] == 'network-release']
+ self.assertEqual(len(releases), 4)
+ self.assertLess(before, min(releases))
+
+
+if __name__ == '__main__': unittest.main()
diff --git a/test_validation_heal_network_delete_records.py b/test_validation_heal_network_delete_records.py
new file mode 100644
index 00000000..940e1564
--- /dev/null
+++ b/test_validation_heal_network_delete_records.py
@@ -0,0 +1,113 @@
+"""Real stopped fixture journal bytes bind safe selectors without exporting metadata."""
+
+import copy
+from contextlib import redirect_stdout
+import hashlib
+import io
+import json
+import os
+from pathlib import Path
+import tempfile
+import types
+import unittest
+from unittest.mock import patch
+
+from scripts.validation_heal import network_delete_records as records
+from scripts.validation_heal.healthy import Fleet
+import test_validation_heal_mpu_scene as mpu_tests
+
+
+class DeleteRecordTests(unittest.TestCase):
+ def scene(self, directory):
+ request, node, roots, ignored = mpu_tests.MultipartSceneTests().fixture(Path(directory))
+ keys = ['dl/node1/control.bin', 'dl/node3/control.bin']
+ request.update(action='network-delete-records', keys=keys)
+ transaction = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
+ versions = [f'12345678-1234-1234-1234-{i + 1:012d}' for i in range(3)]
+ dirs = [f'22345678-1234-1234-1234-{i + 1:012d}' for i in range(3)]
+ paths = []
+ for root in roots:
+ for key in keys:
+ folder = root / 'data' / request['bucket'] / key / transaction;folder.mkdir(parents=True)
+ value = {'protocol_version': 1, 'volume': request['bucket'], 'object': key, 'transaction': transaction,
+ 'bucket_incarnation': '33333333-3333-3333-3333-333333333333',
+ 'original_digest': [1] * 32, 'target_digest': [2] * 32,
+ 'data_dirs': dirs[:1], 'deleted_versions': versions[:1],
+ 'requests': [{'name': key, 'version_id': versions[0], 'deleted': False, 'mark_deleted': False,
+ 'metadata': {'fixture-private-value': 'must-not-be-exported'}}],
+ 'original_versions': [{'version_id': v, 'data_dir': d, 'deleted': False, 'metadata_digest': [3] * 32}
+ for v, d in zip(versions, dirs)], 'predecessors': []}
+ path = folder / 'delete-prepared';path.write_text(json.dumps(value));paths.append(path)
+ (folder / 'do-not-read').write_bytes(b'private payload')
+ return request, node, roots, paths
+
+ def test_all_four_original_disks_two_keys_raw_shas_are_retained_without_exporting_nested_metadata(self):
+ with tempfile.TemporaryDirectory() as directory:
+ request, node, roots, paths = self.scene(directory)
+ before = {p: p.read_bytes() for p in paths};opened, real_open = [], os.open
+ def checked(path, *args, **kwargs):
+ self.assertIn(Path(path), paths);opened.append(Path(path));return real_open(path, *args, **kwargs)
+ with patch.object(records.os, 'open', side_effect=checked): value = records.run(request, node)
+ self.assertEqual(len(value['records']), 8)
+ self.assertEqual(set(opened), set(paths))
+ for row in value['records']:
+ path = roots[row['disk']] / 'data' / row['path']
+ self.assertEqual(row['sha256'], hashlib.sha256(before[path]).hexdigest())
+ self.assertEqual(row['size'], len(before[path]))
+ self.assertEqual(row['originalMetadataSha256'], '01' * 32)
+ self.assertEqual(row['targetMetadataSha256'], '02' * 32)
+ self.assertNotIn('must-not-be-exported', json.dumps(value))
+ self.assertTrue(value['readOnly'] and value['quiescent'])
+ self.assertFalse(value['commitOrRollbackClaimed'] or value['rawContentsExported'] or value['credentialContentsRead'])
+ self.assertEqual({p: p.read_bytes() for p in paths}, before)
+
+ def test_wrong_scope_schema_digest_selector_type_duplicate_fields_links_and_live_service_refuse(self):
+ for kind in ('bucket', 'keys', 'runtime', 'live', 'schema', 'duplicate-json', 'digest', 'extra-field', 'symlink', 'hardlink', 'oversize'):
+ with self.subTest(kind=kind), tempfile.TemporaryDirectory() as directory:
+ request, node, roots, paths = self.scene(directory);path = paths[0]
+ if kind == 'bucket': request['bucket'] = 'foreign'
+ elif kind == 'keys': request['keys'][1] = 'dl/node2/control.bin'
+ elif kind == 'runtime': node['owned'](request['runId'])[2]['runtimeProfile'] = 'manual'
+ elif kind == 'live': node['stopped'] = lambda owner: (_ for _ in ()).throw(ValueError('running'))
+ elif kind == 'symlink': path.unlink();path.symlink_to(Path(directory).resolve() / 'state/credentials.env')
+ elif kind == 'hardlink': os.link(path, path.with_name('extra-link'))
+ elif kind == 'oversize': path.write_bytes(b'x' * (1024**2 + 1))
+ else:
+ value = json.loads(path.read_text())
+ if kind == 'schema': value['protocol_version'] = True
+ elif kind == 'digest': value['original_digest'][0] = True
+ elif kind == 'extra-field': value['unknown'] = 'unbound'
+ if kind == 'duplicate-json': path.write_text(json.dumps(value)[:-1] + ',"protocol_version":1}')
+ else: path.write_text(json.dumps(value))
+ with self.assertRaises(ValueError): records.run(request, node)
+
+ def test_owner_change_after_read_refuses_original_scene_claim(self):
+ with tempfile.TemporaryDirectory() as directory:
+ request, node, roots, paths = self.scene(directory);original = node['owned'];calls = []
+ def changed(run_id):
+ state, roots, owner = original(run_id);calls.append(run_id)
+ return state, roots, owner if len(calls) == 1 else {**owner, 'bucket': 'foreign'}
+ node['owned'] = changed
+ with self.assertRaisesRegex(ValueError, 'owner/roots changed'): records.run(request, node)
+
+ def test_actual_remote_source_injection_and_action_dispatch_use_the_read_only_backend(self):
+ with tempfile.TemporaryDirectory() as directory:
+ request, node, roots, paths = self.scene(directory)
+ def execute(*args, **kwargs):
+ source = kwargs['input'].rsplit('\nmain(', 1)[0]
+ scope = {'__name__': '__owned_node_test__'}
+ exec(compile(source, 'network-delete-node', 'exec'), scope)
+ scope.update(node)
+ out = io.StringIO()
+ with redirect_stdout(out), patch.object(scope['os'], 'geteuid', return_value=0):
+ scope['main'](request)
+ return types.SimpleNamespace(returncode=0, stdout=out.getvalue(), stderr='')
+ with patch('scripts.validation_heal.healthy.subprocess.run', side_effect=execute):
+ result = Fleet(['vm000'], Path(directory), request['runId'], runtime_profile='lock-quorum').remote(
+ 'vm000', 'network-delete-records', bucket=request['bucket'], keys=request['keys'],
+ expectedBinarySha256=request['expectedBinarySha256'])
+ self.assertEqual(len(result['records']), 8)
+ self.assertFalse(result['credentialContentsRead'] or result['commitOrRollbackClaimed'])
+
+
+if __name__ == '__main__': unittest.main()
diff --git a/test_validation_heal_network_fixtures.py b/test_validation_heal_network_fixtures.py
new file mode 100644
index 00000000..e4cdd87b
--- /dev/null
+++ b/test_validation_heal_network_fixtures.py
@@ -0,0 +1,149 @@
+"""Network source fixture completeness, real boundaries and fixed resource caps."""
+
+import copy
+import hashlib
+from pathlib import Path
+from types import SimpleNamespace
+import unittest
+from unittest.mock import patch
+
+from scripts.validation_heal import network, network_fixtures as fixtures
+from scripts.validation_heal.boundaries import boundary_sizes
+from scripts.validation_heal.healthy import Fleet
+from scripts.validation_heal.fleet_node import snapshot_buckets
+
+
+class NetworkBoundaryFixturesTest(unittest.TestCase):
+ def parameters(self):
+ source = '1c7fc818dd858247c45b39542abd8291b1e6e31b'
+ files = {name: 'a' * 64 for name in ('crates/ecstore/src/config/storageclass.rs',
+ 'crates/ecstore/src/set_disk/mod.rs', 'crates/ecstore/src/set_disk/ops/object.rs',
+ 'crates/ecstore/src/erasure/coding/erasure.rs')}
+ files['crates/ecstore/src/set_disk/mod.rs'] = fixtures.MPU_SOURCE[source]
+ manifest = {'sizeContract': {'recognized': True, 'candidateCommit': source,
+ 'extractorSourceSha256': hashlib.sha256(Path('scripts/validation_heal/prepare_heal_meta_inspector.py').read_bytes()).hexdigest(),
+ 'inlineRule': 'ceil(size/dataBlocks)<=effectiveInlineBlock AND size<=actualECBlock',
+ 'effectiveDefaultRule': 'min(ceil(2*defaultInlineBlock/dataBlocks),defaultInlineBlock)',
+ 'defaultInlineBlock': 128 * 1024, 'defaultObjectBudgetFactor': 2, 'sourceFiles': files}}
+ target = {'logical': {'usesLegacyChecksum': False, 'erasure': {
+ 'data_blocks': 8, 'parity_blocks': 8, 'block_size': 1024**2, 'distribution': list(range(1, 17))}}}
+ processes = {f'node{i}': {'storageClassEnvironment': {
+ 'RUSTFS_STORAGE_CLASS_INLINE_BLOCK': None, 'RUSTFS_STORAGE_CLASS_STANDARD': 'EC:8'}} for i in range(4)}
+ candidate = {'source_sha': source}
+ return manifest, candidate, target, processes
+
+ def test_actual_ec8_size_contract_keeps_all_boundaries_and_default128(self):
+ manifest, candidate, target, processes = self.parameters()
+ parameters = fixtures.db_parameters(manifest, candidate, target, processes)
+ self.assertEqual(parameters['objectCount'], 128)
+ self.assertEqual(parameters['actualLogicalInlineLimit'], 262144)
+ self.assertEqual(parameters['groups']['inline-minus-exact-plus'], [262143, 262144, 262145])
+ self.assertEqual(parameters['groups']['stripe-minus-exact-plus'], [1048575, 1048576, 1048577])
+ self.assertEqual(parameters['groups']['multipart-minimum-minus-exact-plus'], [5242879, 5242880, 5242881])
+ self.assertTrue({0, 1, 4095, 4096, 4097} <= set(parameters['sizeCycle']))
+ self.assertLess(parameters['payloadBytes'], 512 * 1024**2)
+ self.assertFalse(parameters['completedMultipartProtocolClaimed'])
+
+ def test_missing_environment_wrong_geometry_or_foreign_source_refuses_calibration(self):
+ for kind in ('default', 'foreign-class', 'inline-override', 'geometry', 'distribution', 'source-hash', 'candidate'):
+ manifest, candidate, target, processes = self.parameters()
+ if kind == 'default': processes['node0']['storageClassEnvironment']['RUSTFS_STORAGE_CLASS_STANDARD'] = None
+ elif kind == 'foreign-class': processes['node0']['storageClassEnvironment']['RUSTFS_STORAGE_CLASS_STANDARD'] = 'EC:4'
+ elif kind == 'inline-override': processes['node0']['storageClassEnvironment']['RUSTFS_STORAGE_CLASS_INLINE_BLOCK'] = '1MiB'
+ elif kind == 'geometry': target['logical']['erasure']['parity_blocks'] = 4
+ elif kind == 'distribution': target['logical']['erasure']['distribution'].pop()
+ elif kind == 'source-hash': manifest['sizeContract']['sourceFiles']['crates/ecstore/src/set_disk/mod.rs'] = 'f' * 64
+ else:
+ candidate['source_sha'] = 'f' * 40
+ manifest['sizeContract']['candidateCommit'] = 'f' * 40
+ with self.subTest(kind=kind), self.assertRaises(ValueError):
+ fixtures.db_parameters(manifest, candidate, target, processes)
+
+ def test_default_boundary_callers_cannot_silently_use_ec8_environment(self):
+ manifest, candidate, target, processes = self.parameters()
+ with self.assertRaises(ValueError): boundary_sizes(manifest['sizeContract'], target, processes)
+ for value in ('EC:4', True, 'EC:16'):
+ with self.subTest(value=value), self.assertRaises(ValueError):
+ boundary_sizes(manifest['sizeContract'], target, processes, standard_class=value)
+
+ def test_seed_uses_real_db128_cycle_and_refuses_budget_before_first_call(self):
+ params = fixtures.db_parameters(*self.parameters())
+ calls = []
+ def call(operation, values, body=None):
+ calls.append((operation, values, len(body) if body is not None else None))
+ return {'ETag': 'ack'}
+ recorder = SimpleNamespace(probe=SimpleNamespace(binding={'bucketPrefix': 'rv-heal-owned',
+ 'maxInventoryBytes': 512 * 1024**2, 'maxInventoryObjects': 128}), call=call)
+ expected = fixtures.seed_db(recorder, 'rv-heal-owned-db', params)
+ self.assertEqual(len(expected), 128)
+ self.assertEqual(calls[0][0], 'create_bucket')
+ self.assertEqual(sum(c[2] for c in calls if c[0] == 'put_object'), params['payloadBytes'])
+ self.assertEqual({x['payload']['size'] for x in expected}, set(params['sizeCycle']))
+ calls.clear()
+ recorder.probe.binding['maxInventoryBytes'] = params['payloadBytes'] - 1
+ with self.assertRaises(ValueError): fixtures.seed_db(recorder, 'rv-heal-owned-db', params)
+ self.assertFalse(calls)
+
+ def test_each128_control_is_shape_mapped_and_all_unique_sizes_have_representatives(self):
+ params = fixtures.db_parameters(*self.parameters())
+ expected = [{'key': f'objects/{i:04d}.bin', 'versionId': 'null', 'isLatest': True,
+ 'deleteMarker': False, 'payload': {'size': params['sizeCycle'][i % len(params['sizeCycle'])]}} for i in range(128)]
+ mapped = []
+ def target(entry, shape):
+ mapped.append((entry['key'], shape))
+ return {'cohort': [{}] * 16, 'logical': {'size': entry['payload']['size'], 'erasure': {'data_blocks': 8, 'parity_blocks': 8}}}
+ def external(snapshot, bucket, key, role, part):
+ entry = next(x for x in expected if x['key'] == key)
+ self.assertGreater(entry['payload']['size'], params['actualLogicalInlineLimit'])
+ return target(entry, 'external')
+ def inline(snapshot, bucket, entry, *, allow_empty):
+ self.assertEqual(allow_empty, entry['payload']['size'] == 0)
+ self.assertLessEqual(entry['payload']['size'], params['actualLogicalInlineLimit'])
+ return target(entry, 'zero' if allow_empty else 'inline')
+ with patch.object(fixtures, 'map_target', side_effect=external), patch.object(fixtures, 'map_inline_target', side_effect=inline):
+ representatives, witnessed = fixtures.map_db({}, 'owned-db', expected, params)
+ self.assertEqual((len(mapped), len(witnessed)), (128, 128))
+ self.assertEqual({r['expected']['payload']['size'] for r in representatives}, set(params['sizeCycle']))
+ self.assertEqual(set(shape for key, shape in mapped), {'zero', 'inline', 'external'})
+ for wrong in (expected[:-1], [expected[0]] + expected[:-1]):
+ with self.assertRaises(ValueError): fixtures.map_db({}, 'owned-db', wrong, params)
+
+ def test_full_physical_snapshot_explicitly_includes_the_owned_db_bucket(self):
+ fleet = Fleet(['node1', 'node2'], Path('/unused'), 'gh-12345-1-h2')
+ owner = {'bucket': 'rv-heal-owned'}
+ def remote(node, action, **params):
+ self.assertEqual(action, 'snapshot')
+ return {'buckets': snapshot_buckets(owner, params), 'quiescent': params['quiescent']}
+ with patch.object(fleet, 'record', side_effect=lambda label, value: value), patch.object(fleet, 'remote', side_effect=remote) as call:
+ current = fleet.snapshot('baseline', quiescent=True, buckets=['rv-heal-owned', 'rv-heal-owned-db'])
+ self.assertEqual(set(current), {'node1', 'node2'})
+ self.assertTrue(all(value == {'buckets': ['rv-heal-owned', 'rv-heal-owned-db'], 'quiescent': True}
+ for value in current.values()))
+ self.assertEqual(call.call_count, 2)
+ default = fleet.snapshot('legacy-default')
+ self.assertTrue(all(value['buckets'] == ['rv-heal-owned'] for value in default.values()))
+ for buckets in (['rv-heal-owned-db'], ['rv-heal-owned', 'foreign-bucket']):
+ with self.assertRaises(ValueError): fleet.snapshot('refuse-escape', buckets=buckets)
+
+ def test_control_read_requires_full_payload_or_exact_source_bound_lock_refusal(self):
+ source = '1c7fc818dd858247c45b39542abd8291b1e6e31b'
+ contract = {'candidateCommit': source, 'recognized': True,
+ 'sourceFiles': {'rustfs/src/error.rs': network.GET_SOURCE_FILES[source]['rustfs/src/error.rs']},
+ 'unrecoverableGet': {'httpStatus': 503, 'code': 'SlowDownRead'}}
+ row = {'expected': {'key': 'control', 'versionId': 'null', 'payload': {'size': 1, 'sha256': 'digest'}},
+ 'target': {'logical': {'erasure': {'data_blocks': 1}}}, 'shape': 'inline'}
+ good = {'acknowledged': True, 'resultUnknown': False, 'payload': row['expected']['payload'],
+ 'response': {'ResponseMetadata': {'HTTPStatusCode': 200}}}
+ error = {'acknowledged': False, 'resultUnknown': False,
+ 'response': {'ResponseMetadata': {'HTTPStatusCode': 503}, 'Error': {'Code': 'ServiceUnavailable'}}}
+ for clients, observed, okay in [(2, good, True), (1, error, True), (1, good, False),
+ (2, {**good, 'payload': {'size': 0}}, False), (2, {**good, 'resultUnknown': True}, False)]:
+ with self.subTest(clients=clients, observation=observed), patch.object(network, 'partition_get', return_value=(observed, {'complete': True})):
+ args = (None, None, 'owned', [row], {'lockClients': clients, 'metadataAuthorities': 4}, {'sharedLockQuorum': 2}, contract)
+ if okay: self.assertEqual(len(network.partition_db(*args)), 1)
+ else:
+ with self.assertRaises(ValueError): network.partition_db(*args)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/test_validation_heal_network_load.py b/test_validation_heal_network_load.py
new file mode 100644
index 00000000..6b4cade0
--- /dev/null
+++ b/test_validation_heal_network_load.py
@@ -0,0 +1,268 @@
+"""DL ledger binding and actual client overlap; mocks are not live certification."""
+
+import copy
+from datetime import datetime, timezone, timedelta
+import hashlib
+import json
+from pathlib import Path
+import tempfile
+import threading
+import time
+import unittest
+
+from scripts.validation_heal import network_load as load
+from scripts.validation_heal.heal_probe import CLOCK_DOMAIN, save
+from scripts.validation_heal.heal_workload import Recorder
+
+
+class Probe:
+ def __init__(self, binding, endpoint='node1', *, fixture=None, role=None, rendezvous=None, unknown=False, readable=True):
+ self.binding, self.endpoint_name = binding, endpoint
+ self.fixture, self.role, self.rendezvous, self.unknown = fixture, role, rendezvous, unknown
+ self.readable = readable
+ self.calls, self.objects, self.sequence = [], {}, 0
+
+ def s3(self, operation, params, body=None):
+ self.calls.append((operation, copy.deepcopy(params)))
+ self.sequence += 1
+ started = time.monotonic_ns()
+ if self.rendezvous and self.sequence == 1:
+ mine, other = self.rendezvous[self.role], self.rendezvous['reader' if self.role == 'writer' else 'writer']
+ mine.set()
+ if not other.wait(2): raise ValueError('test client calls did not overlap')
+ status, result, uncertain = 200, {}, False
+ if self.role == 'writer':
+ status = 503
+ result = {'Error': {'Code': 'ServiceUnavailable'}}
+ if self.unknown: result, uncertain = {}, True
+ elif self.role == 'reader':
+ side = self.fixture['sides'][self.endpoint_name]
+ current = side['versions'][-1]
+ if operation in {'get_object', 'head_object'}:
+ result = {**side['heads'][current['versionId']], 'VersionId': current['versionId']}
+ if not self.readable:
+ status = 503
+ result = {'Error': {'Code': '503' if operation == 'head_object' else 'ServiceUnavailable'}}
+ else:
+ result = {'IsTruncated': False}
+ if operation == 'list_parts':
+ item = side['uploads']['complete']
+ result.update(Key=item['key'], UploadId=item['uploadId'], Parts=[
+ {'PartNumber': p['PartNumber'], 'ETag': p['ETag'], 'Size': p['payload']['size']} for p in item['parts']])
+ elif operation == 'get_bucket_versioning': result = {'Status': 'Enabled'}
+ elif operation == 'put_object':
+ version, etag = f'v{self.sequence}', hashlib.md5(body).hexdigest()
+ result = {'VersionId': version, 'ETag': etag}
+ self.objects[params['Key'], version] = {'VersionId': version, 'ETag': etag,
+ 'LastModified': datetime(2026, 10, 19, tzinfo=timezone.utc) + timedelta(seconds=self.sequence), 'ContentLength': len(body),
+ 'ContentType': params['ContentType'], 'Metadata': params['Metadata']}
+ elif operation == 'head_object': result = copy.deepcopy(self.objects[params['Key'], params['VersionId']])
+ elif operation == 'create_multipart_upload': result = {'UploadId': f'upload-{self.sequence}'}
+ elif operation == 'upload_part': result = {'ETag': hashlib.md5(body).hexdigest()}
+ response = {**result, 'ResponseMetadata': {'HTTPStatusCode': status}}
+ out = {'operation': operation, 'params': copy.deepcopy(params), 'endpoint': self.endpoint_name,
+ 'clientCallInvoked': True, 'acknowledged': status < 300 and not uncertain,
+ 'resultUnknown': uncertain, 'response': None if uncertain else response,
+ 'clockDomain': CLOCK_DOMAIN, 'callStartedMonotonicNs': started, 'callFinishedMonotonicNs': time.monotonic_ns()}
+ if body is not None: out['payload'] = load.digest(body)
+ if operation == 'get_object' and self.fixture and status == 200: out['payload'] = current['payload']
+ return out
+
+
+class NetworkLoadTests(unittest.TestCase):
+ def binding(self):
+ return {'runId': 'gh-12345-1-h2', 'candidateCommit': '1c7fc818dd858247c45b39542abd8291b1e6e31b',
+ 'bucketPrefix': 'rv-heal-owned', 'maxInventoryBytes': 512 * 1024**2,
+ 'maxInventoryObjects': 128, 'endpoints': {'node1': 'http://one:9000', 'node3': 'http://three:9000'}}
+
+ def fixture(self, root):
+ probe = Probe(self.binding())
+ recorder = Recorder(probe, root / 'seed')
+ fixture = load.seed_load(recorder, 'rv-heal-owned', ['node1', 'node3'])
+ return fixture, probe
+
+ def journals(self, root, fixture, *, unknown_endpoint=None):
+ journals, probes = {}, {}
+ for endpoint in fixture['sides']:
+ rendezvous = {'writer': threading.Event(), 'reader': threading.Event()}
+ for role in ('reader', 'writer'):
+ probe = Probe(self.binding(), endpoint, fixture=fixture, role=role, rendezvous=rendezvous,
+ unknown=endpoint == unknown_endpoint and role == 'writer')
+ probes[endpoint, role] = probe
+ journals[endpoint, role] = load.LoadJournal(probe, root / f'{endpoint}-{role}', role)
+ return journals, probes
+
+ def test_real_seed_preserves_three_distinct_versions_and_all_actual_mpu_ack_ids(self):
+ with tempfile.TemporaryDirectory() as d:
+ fixture, probe = self.fixture(Path(d))
+ self.assertEqual(len(fixture['expected']), 6)
+ self.assertEqual(len({row['payload']['sha256'] for row in fixture['expected']}), 6)
+ self.assertLess(fixture['seedPayloadBytes'], 32 * 1024**2)
+ uploads = [u for side in fixture['sides'].values() for u in side['uploads'].values()]
+ self.assertEqual(len({u['uploadId'] for u in uploads}), 6)
+ for side in fixture['sides'].values():
+ complete = side['uploads']['complete']
+ body = b''.join(load.part_body(fixture['bucket'], complete['key'], p['PartNumber'], p['payload']['size']) for p in complete['parts'])
+ self.assertEqual(complete['completePayload'], load.digest(body))
+ self.assertEqual([p['PartNumber'] for p in complete['parts']], [1, 2])
+ self.assertEqual(len(side['heads']), 3)
+ steps = load.mutation_steps(fixture, 'node1')
+ self.assertEqual([s['nonce'] for s in steps], list(range(3, 11)))
+ self.assertEqual(steps[2]['params']['CopySource']['VersionId'], fixture['sides']['node1']['versions'][0]['versionId'])
+ self.assertEqual(steps[3]['params']['VersionId'], fixture['sides']['node1']['versions'][0]['versionId'])
+ self.assertEqual(steps[6]['params']['MultipartUpload']['Parts'], [
+ {k: p[k] for k in ('PartNumber', 'ETag')} for p in fixture['sides']['node1']['uploads']['complete']['parts']])
+
+ def test_four_independent_journals_drain_full_matrix_with_actual_client_overlap(self):
+ with tempfile.TemporaryDirectory() as d:
+ root = Path(d);fixture, _ = self.fixture(root)
+ journals, probes = self.journals(root, fixture)
+ result = load.concurrent_load(journals, fixture, {'node1': True, 'node3': True})
+ self.assertTrue(result['complete'])
+ self.assertTrue(result['allCallsDrained'])
+ self.assertTrue(result['mutationReconciliationRequired'])
+ self.assertFalse(result['canonicalCasePassClaimed'])
+ self.assertEqual(sum(len(r['results']) for r in result['workers']), 28)
+ self.assertTrue(all(row['overlappingClientCalls'] and not row['serverCommitFenceClaimed'] for row in result['overlaps']))
+ for (endpoint, role), journal in journals.items():
+ rows = load.read_journal(journal.output, journal.probe.binding, endpoint, role)
+ self.assertEqual(len(rows), 8 if role == 'writer' else 6)
+ self.assertEqual(len(probes[endpoint, role].calls), len(rows))
+ self.assertTrue(all(r['ackPresent'] and r['requestSha256'] == r['ack']['requestSha256'] for r in rows))
+ self.assertTrue(all(r['resolution'] == 'REQUIRES_INDEPENDENT_RECONCILIATION' for r in rows))
+
+ def test_unknown_write_stops_that_worker_without_reusing_or_retrying_its_receipt(self):
+ with tempfile.TemporaryDirectory() as d:
+ root = Path(d);fixture, _ = self.fixture(root)
+ journals, probes = self.journals(root, fixture, unknown_endpoint='node3')
+ result = load.concurrent_load(journals, fixture, {'node1': True, 'node3': True})
+ self.assertFalse(result['complete'])
+ journal = journals['node3', 'writer']
+ rows = load.read_journal(journal.output, journal.probe.binding, 'node3', 'writer')
+ self.assertEqual(len(rows), 1)
+ self.assertEqual(rows[0]['resolution'], 'UNKNOWN_RESULT')
+ follow = journal.request(load.mutation_steps(fixture, 'node3')[1], 503, time.monotonic() + 1)
+ self.assertIsNone(follow['row'])
+ self.assertIsNotNone(follow['error'])
+ self.assertEqual(len(probes['node3', 'writer'].calls), 1)
+
+ def test_minority_head_uses_bodyless503_and_current_get_requires_lock_error(self):
+ with tempfile.TemporaryDirectory() as d:
+ root = Path(d);fixture, _ = self.fixture(root)
+ journals, probes = self.journals(root, fixture)
+ probes['node3', 'reader'].readable = False
+ result = load.concurrent_load(journals, fixture, {'node1': True, 'node3': False})
+ self.assertTrue(result['complete'])
+ rows = load.read_journal(journals['node3', 'reader'].output, self.binding(), 'node3', 'reader')
+ for row in rows[:2]:
+ self.assertEqual(load.validate_call({'row': row}, fixture, 'node3', readable=False)['kind'], 'READ_REFUSED')
+ wrong = copy.deepcopy(rows[0]);wrong['ack']['observation']['response']['Error']['Code'] = 'SlowDownRead'
+ with self.assertRaises(ValueError): load.validate_call({'row': wrong}, fixture, 'node3', readable=False)
+
+ def test_changed_url_source_or_run_binding_stops_before_any_invocation(self):
+ for kind in ('url', 'source', 'run', 'role'):
+ with self.subTest(kind=kind), tempfile.TemporaryDirectory() as d:
+ root = Path(d);fixture, _ = self.fixture(root)
+ journals, probes = self.journals(root, fixture)
+ probe = probes['node3', 'writer']
+ if kind == 'url': probe.binding['endpoints']['node3'] = 'http://foreign:9000'
+ elif kind == 'source': probe.binding['candidateCommit'] = '072b57b37559fbe626aeda8abf61617fdcbdd926'
+ elif kind == 'run': probe.binding['runId'] = 'gh-54321-1-h2'
+ else: journals['node3', 'writer'].role = 'reader'
+ with self.assertRaises(ValueError): load.concurrent_load(journals, fixture, {'node1': True, 'node3': True})
+ self.assertTrue(all(not p.calls for p in probes.values()))
+
+ def test_lost_ack_file_remains_unknown_and_cannot_become_a_refusal(self):
+ with tempfile.TemporaryDirectory() as d:
+ root = Path(d);fixture, _ = self.fixture(root)
+ probe = Probe(self.binding(), fixture=fixture, role='writer')
+ def fail_ack(path, value):
+ if path.name.endswith('-ack.json'): raise OSError('receipt disk unavailable')
+ save(path, value)
+ journal = load.LoadJournal(probe, root / 'lost-ack', 'writer', writer=fail_ack)
+ result = journal.request(load.mutation_steps(fixture, 'node1')[0], 503, time.monotonic() + 1)
+ self.assertFalse(result['row']['ackPresent'])
+ self.assertEqual(result['row']['resolution'], 'UNKNOWN_PREPARED_REQUEST')
+ with self.assertRaises(ValueError): load.validate_call(result, fixture, 'node1', readable=True)
+ self.assertEqual(len(probe.calls), 1)
+
+ def test_wrong_hash_selector_endpoint_or_actual_clock_cannot_bind_an_ack(self):
+ for kind in ('hash', 'endpoint', 'selector', 'clock', 'clock-range', 'missing-operation'):
+ with self.subTest(kind=kind), tempfile.TemporaryDirectory() as d:
+ root = Path(d);fixture, _ = self.fixture(root)
+ probe = Probe(self.binding(), fixture=fixture, role='writer')
+ journal = load.LoadJournal(probe, root / 'writer', 'writer')
+ journal.request(load.mutation_steps(fixture, 'node1')[0], 503, time.monotonic() + 1)
+ path = journal.output / '000001-ack.json';ack = json.loads(path.read_text())
+ if kind == 'hash': ack['requestSha256'] = 'f' * 64
+ elif kind == 'endpoint': ack['observation']['endpoint'] = 'node3'
+ elif kind == 'selector': ack['observation']['params']['Key'] = 'other'
+ elif kind == 'clock': ack['observation']['clockDomain'] = 'foreign'
+ elif kind == 'clock-range': ack['observation']['callStartedMonotonicNs'] = ack['collectorCallFinishedMonotonicNs'] + 1
+ else: ack['observation'].pop('operation')
+ path.write_text(json.dumps(ack))
+ with self.assertRaises(ValueError): load.read_journal(journal.output, journal.probe.binding, 'node1', 'writer')
+
+ def test_success_500_wrong_error_and_uninvoked_write_are_never_safe_refusals(self):
+ with tempfile.TemporaryDirectory() as d:
+ root = Path(d);fixture, _ = self.fixture(root)
+ journal = load.LoadJournal(Probe(self.binding(), fixture=fixture, role='writer'), root / 'writer', 'writer')
+ result = journal.request(load.mutation_steps(fixture, 'node1')[0], 503, time.monotonic() + 1)
+ self.assertFalse(load.validate_call(result, fixture, 'node1', readable=True)['rollbackClaimed'])
+ for kind in ('success', '500', 'read-code', 'uninvoked', 'unknown', 'collector-failed'):
+ bad = copy.deepcopy(result);obs = bad['row']['ack']['observation']
+ if kind == 'success': obs['acknowledged'] = True;obs['response']['ResponseMetadata']['HTTPStatusCode'] = 200
+ elif kind == '500': obs['response']['ResponseMetadata']['HTTPStatusCode'] = 500;obs['response']['Error']['Code'] = 'InternalError'
+ elif kind == 'read-code': obs['response']['Error']['Code'] = 'SlowDownRead'
+ elif kind == 'uninvoked': obs['clientCallInvoked'] = False
+ elif kind == 'collector-failed': bad['row']['ack']['operationCheck'] = 'FAIL'
+ else: obs['resultUnknown'] = True
+ with self.subTest(kind=kind), self.assertRaises(ValueError): load.validate_call(bad, fixture, 'node1', readable=True)
+
+ def test_rehashed_ack_cannot_substitute_an_unplanned_key_nonce_or_mpu_id(self):
+ for kind in ('key', 'nonce', 'body', 'mpu'):
+ with self.subTest(kind=kind), tempfile.TemporaryDirectory() as d:
+ root = Path(d);fixture, _ = self.fixture(root)
+ journal = load.LoadJournal(Probe(self.binding(), fixture=fixture, role='writer'), root / 'writer', 'writer')
+ steps = load.mutation_steps(fixture, 'node1')[:6 if kind == 'mpu' else 1]
+ for step in steps: journal.request(step, 503, time.monotonic() + 1)
+ sequence = len(steps)
+ request_path = journal.output / f'{sequence:06d}-request.json';ack_path = journal.output / f'{sequence:06d}-ack.json'
+ request, ack = json.loads(request_path.read_text()), json.loads(ack_path.read_text())
+ if kind == 'key': request['params']['Key'] = 'dl/node1/unplanned.bin'
+ elif kind == 'nonce': request['identity']['nonce'] = 4;ack['identity']['nonce'] = 4
+ elif kind == 'mpu': request['params']['UploadId'] = 'unacknowledged-upload-id'
+ else: request['payload']['sha256'] = 'f' * 64
+ ack['observation']['params'] = copy.deepcopy(request['params'])
+ request_path.write_text(json.dumps(request));ack['requestSha256'] = hashlib.sha256(request_path.read_bytes()).hexdigest()
+ ack_path.write_text(json.dumps(ack))
+ row = load.read_journal(journal.output, self.binding(), 'node1', 'writer')[-1]
+ with self.assertRaisesRegex(ValueError, 'planned request'): load.validate_call({'row': row}, fixture, 'node1', readable=True)
+
+ def test_finite_status_tuple_is_preserved_and_invalid_sets_fail_before_invocation(self):
+ with tempfile.TemporaryDirectory() as d:
+ probe = Probe(self.binding(), role='writer');recorder = Recorder(probe, Path(d) / 'statuses')
+ params = {'Bucket': 'rv-heal-owned', 'Key': 'one'}
+ recorder.call('put_object', params, b'body', expect_http=(200, 503))
+ request = json.loads((recorder.output / '000001-request.json').read_text())
+ self.assertEqual(request['expectHttp'], [200, 503])
+ self.assertEqual(len(probe.calls), 1)
+ for value in ((), (503, 503), (True,), (99,), (200, 201, 202, 503), [200, 503], '503'):
+ with self.subTest(value=value), self.assertRaises(ValueError): recorder.call('put_object', params, b'body', expect_http=value)
+ self.assertEqual(len(probe.calls), 1)
+
+ def test_wrapper_overlap_and_distinct_clock_domains_cannot_prove_sdk_overlap(self):
+ def row(name, start, end, clock=CLOCK_DOMAIN):
+ return {'requestSha256': name, 'ackPresent': True, 'ack': {'collectorCallStartedMonotonicNs': 1,
+ 'collectorCallFinishedMonotonicNs': 100, 'observation': {'clientCallInvoked': True,
+ 'clockDomain': clock, 'callStartedMonotonicNs': start, 'callFinishedMonotonicNs': end}}}
+ reader, writer = row('read', 10, 20), row('write', 21, 30)
+ self.assertEqual(load.client_overlap_pairs([reader], [writer]), [])
+ writer['ack']['observation']['callStartedMonotonicNs'] = 19
+ self.assertEqual(load.client_overlap_pairs([reader], [writer]), [{'readerRequestSha256': 'read', 'writerRequestSha256': 'write'}])
+ writer['ack']['observation']['clockDomain'] = 'foreign'
+ self.assertEqual(load.client_overlap_pairs([reader], [writer]), [])
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/test_validation_heal_network_load_repair.py b/test_validation_heal_network_load_repair.py
new file mode 100644
index 00000000..65801c98
--- /dev/null
+++ b/test_validation_heal_network_load_repair.py
@@ -0,0 +1,199 @@
+"""Exact historical DL deletion effects cannot hide corruption or new commits."""
+
+import copy
+import hashlib
+import unittest
+from unittest.mock import patch
+
+import test_validation_heal as oracle
+from scripts.validation_heal import network_load_repair as repair, network_load as load, network
+from scripts.validation_heal.shards import map_target
+
+
+class LoadRepairTests(unittest.TestCase):
+ def scene(self):
+ base, _ = oracle.HealReadQuorumBoundaryTest().scene(total=16, data=8)
+ bucket = oracle.HealReadQuorumBoundaryTest.bucket
+ snapshot = {node: {**copy.deepcopy(value), 'files': [], 'binarySha256': 'a' * 64} for node, value in base.items()}
+ fixture = {'bucket': bucket, 'runId': 'gh-12345-1-h2', 'sourceContract': load.source_reference(repair.CANDIDATE), 'sides': {}}
+ for side_index, endpoint in enumerate(('node1', 'node4')):
+ key = 'dl/' + endpoint + '/control.bin'
+ expected = [{'key': key, 'versionId': f'v{side_index}{nonce}', 'isLatest': nonce == 2,
+ 'deleteMarker': False, 'payload': {'size': 100, 'sha256': 'original' + str(nonce)}} for nonce in range(3)]
+ fixture['sides'][endpoint] = {'controlKey': key, 'versions': expected,
+ 'uploads': {name: {'key': f'dl/{endpoint}/{name}.bin', 'uploadId': name + '-original',
+ 'parts': [{'PartNumber': 1, 'ETag': 'original'}]} for name in ('complete', 'upload', 'abort')}}
+ for node, value in base.items():
+ for original in value['files']:
+ if 'metadata' not in original: continue
+ disk = original['disk'];meta = copy.deepcopy(original['metadata']);meta['key'] = key
+ template = meta['versions'][0];versions = []
+ for nonce, entry in enumerate(expected):
+ version = copy.deepcopy(template)
+ version.update(versionId=entry['versionId'], isLatest=entry['isLatest'],
+ dataDir=f'12345678-1234-1234-1234-{side_index * 10 + nonce:012d}')
+ versions.append(version)
+ snapshot[node]['files'].append({'disk': disk, 'path': f"{bucket}/{key}/{version['dataDir']}/part.1",
+ 'size': 82, 'sha256': hashlib.sha256(f'{node}/{disk}/{key}/{nonce}'.encode()).hexdigest()})
+ meta['versions'] = versions
+ snapshot[node]['files'].append({'disk': disk, 'path': f'{bucket}/{key}/xl.meta', 'size': 600,
+ 'sha256': hashlib.sha256(f'meta/{node}/{disk}/{key}'.encode()).hexdigest(), 'metadata': meta})
+ first = fixture['sides']['node1']['versions'][0]
+ target = map_target(snapshot, bucket, first['key'], 'data', 1, first['versionId'], latest_required=False)
+ target = {**target, **next(row for row in target['cohort'] if row['node'] == 'node4')}
+ plan = network.plan_for(snapshot, target, 'minority-partition')
+ workers = []
+ for endpoint in fixture['sides']:
+ step = load.mutation_steps(fixture, endpoint)[3]
+ request = {**copy.deepcopy(step), 'identity': {'role': 'writer', 'endpoint': endpoint, 'step': 4, 'nonce': 6},
+ 'expectHttp': 503}
+ row = {'request': request, 'ackPresent': True, 'ack': {'operationCheck': 'PASS', 'observation': {
+ 'clientCallInvoked': True, 'resultUnknown': False, 'acknowledged': False,
+ 'response': {'ResponseMetadata': {'HTTPStatusCode': 503}, 'Error': {'Code': 'ServiceUnavailable'}}}}}
+ workers.append({'endpoint': endpoint, 'role': 'writer', 'rows': [row]})
+ return snapshot, fixture, {'workers': workers}, plan
+
+ def deleted(self, snapshot, fixture, *, staged=True):
+ after = copy.deepcopy(snapshot);node, disk = 'node2', 0
+ key = fixture['sides']['node1']['controlKey'];bucket = fixture['bucket']
+ meta = next(row for row in after[node]['files'] if row['disk'] == disk and row['path'] == f'{bucket}/{key}/xl.meta')
+ original = copy.deepcopy(meta);version = meta['metadata']['versions'].pop(0);meta['sha256'] = 'new-metadata'
+ path = f"{bucket}/{key}/{version['dataDir']}/part.1"
+ after[node]['files'] = [row for row in after[node]['files'] if (row['disk'], row['path']) != (disk, path)]
+ if staged:
+ prefix = f'{bucket}/{key}/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/'
+ after[node]['files'].extend([
+ {'disk': disk, 'path': prefix + 'xl.meta.bkp', 'size': original['size'], 'sha256': original['sha256']},
+ {'disk': disk, 'path': prefix + 'delete-prepared', 'size': 300, 'sha256': 'prepared'},
+ {'disk': disk, 'path': prefix + 'delete-target', 'size': 500, 'sha256': 'target'}])
+ return after
+
+ def records(self, before, after, fixture):
+ keys = sorted(side['controlKey'] for side in fixture['sides'].values())
+ result = {node: {'readOnly': True, 'quiescent': True, 'runId': fixture['runId'], 'bucket': fixture['bucket'],
+ 'binarySha256': 'a' * 64, 'keys': keys, 'credentialContentsRead': False, 'records': []} for node in before}
+ for node, value in after.items():
+ for file in value['files']:
+ if not file['path'].endswith('/delete-prepared'): continue
+ path = file['path'];prefix, transaction, _ = path.rsplit('/', 2)
+ key = prefix[len(fixture['bucket']) + 1:]
+ original = next(row for row in before[node]['files'] if row['disk'] == file['disk'] and row['path'] == prefix + '/xl.meta')
+ target = next(row for row in value['files'] if row['disk'] == file['disk'] and row['path'] == prefix + '/' + transaction + '/delete-target')
+ versions = original['metadata']['versions'];first = versions[0]
+ result[node]['records'].append({'disk': file['disk'], 'key': key, 'path': path, 'size': file['size'],
+ 'sha256': file['sha256'], 'protocolVersion': 1, 'transaction': transaction,
+ 'requestVersion': first['versionId'], 'deletedVersions': [first['versionId']], 'dataDirs': [first['dataDir']],
+ 'originalMetadataSha256': original['sha256'], 'targetMetadataSha256': target['sha256'],
+ 'bucketIncarnation': 'original-incarnation',
+ 'originalVersions': [row['versionId'] for row in versions], 'originalDataDirs': [row['dataDir'] for row in versions]})
+ return result
+
+ def status(self, fixture, key, repaired=()):
+ expected = next(side['versions'] for side in fixture['sides'].values() if side['controlKey'] == key)
+ objects = [{'identity': {'bucket': fixture['bucket'], 'object': key, 'versionId': row['versionId'],
+ 'bucketIncarnationId': 'original-incarnation', 'poolIndex': 0, 'setIndex': 0},
+ 'disposition': {'state': 'repaired' if row['versionId'] in repaired else 'verified_healthy'}} for row in expected]
+ return {'summary': 'finished', 'truncated': False, 'outcome': {'execution': {'state': 'completed'},
+ 'coverage': 'complete', 'objectsTruncated': False, 'objects': objects, 'counters': {
+ 'processed': 3, 'healed': len(repaired), 'unchanged': 3 - len(repaired), 'skipped': 0,
+ 'failed': 0, 'unknown': 0, 'attemptFailures': 0, 'overflowed': False}}}
+
+ def test_source_contract_is_exact_and_no_change_means_both_three_version_keys_remain_healthy(self):
+ before, fixture, receipts, plan = self.scene()
+ damage = repair.classify_deletes(before, copy.deepcopy(before), fixture, receipts, plan, list(before), self.records(before, before, fixture))
+ self.assertEqual(damage['repairTargets'], [])
+ with self.assertRaises(ValueError): repair.source_reference('072b57b37559fbe626aeda8abf61617fdcbdd926')
+ calls = []
+ def task(probe, fleet, selection, mode, active, **kwargs):
+ calls.append(selection);self.assertTrue(kwargs['option_overrides']['recursive'])
+ return self.status(fixture, selection['key'])
+ with patch.object(repair, 'run_task', side_effect=task):
+ result = repair.recover_controls(None, None, {'endpoints': {f'node{i}': 'url' for i in range(1, 5)}}, fixture, damage, [])
+ self.assertEqual(len(calls), 2)
+ self.assertTrue(all(value['receipts'] == 3 and value['repaired'] == 0 for value in result.values()))
+
+ def test_staged_original_delete_can_be_product_settled_but_does_not_itself_claim_rollback(self):
+ before, fixture, receipts, plan = self.scene()
+ after = self.deleted(before, fixture)
+ damage = repair.classify_deletes(before, after, fixture, receipts, plan, list(before), self.records(before, after, fixture))
+ key = fixture['sides']['node1']['controlKey'];policy = damage['policies'][key]
+ self.assertFalse(policy['requiredRepair'] or damage['refusalProvesRollback'] or damage['canonicalCasePassClaimed'])
+ self.assertTrue(policy['productSettlementAllowed'] and policy['finalPhysicalProofRequired'])
+ self.assertEqual(policy['changedOriginalSlots'], [('node2', 0)])
+ self.assertEqual(len(damage['repairTargets']), 1)
+ for state in ('healthy-after-product-rollback', 'shard-repaired'):
+ with self.subTest(state=state):
+ def task(probe, fleet, selection, mode, active, **kwargs):
+ repaired = ['v00'] if state == 'shard-repaired' and selection['key'] == key else []
+ return self.status(fixture, selection['key'], repaired)
+ with patch.object(repair, 'run_task', side_effect=task):
+ result = repair.recover_controls(None, None, {'endpoints': {'node1': '1', 'node2': '2', 'node3': '3', 'node4': '4'}}, fixture, damage, [])
+ self.assertTrue(result[key]['productSettlementMayPrecedeShardCheck'])
+ self.assertTrue(result[key]['finalPhysicalProofRequired'])
+
+ def test_loss_without_exact_prepared_backup_requires_real_repaired_responsibility(self):
+ before, fixture, receipts, plan = self.scene();after = self.deleted(before, fixture, staged=False)
+ damage = repair.classify_deletes(before, after, fixture, receipts, plan, list(before), self.records(before, after, fixture))
+ key = fixture['sides']['node1']['controlKey']
+ self.assertTrue(damage['policies'][key]['requiredRepair'])
+ with patch.object(repair, 'run_task', return_value=self.status(fixture, key)):
+ with self.assertRaisesRegex(ValueError, 'repaired responsibility'):
+ repair.recover_controls(None, None, {'endpoints': {'node1': '1'}}, fixture, damage, [])
+
+ def test_other_versions_roots_foreign_files_and_bad_original_request_never_gain_an_exception(self):
+ for kind in ('other-version', 'changed-bytes', 'foreign-version', 'read-only-slot', 'disconnected-slot',
+ 'new-key', 'bad-backup', 'foreign-staged-version', 'unknown-transaction-file', 'wrong-request', 'unknown-response'):
+ with self.subTest(kind=kind):
+ before, fixture, receipts, plan = self.scene();after = self.deleted(before, fixture)
+ rows = after['node2']['files'];bucket = fixture['bucket'];key = fixture['sides']['node1']['controlKey']
+ if kind == 'other-version':
+ chosen = next(row for row in rows if row['disk'] == 0 and row['path'].endswith('000000000001/part.1'))
+ rows.remove(chosen)
+ elif kind == 'changed-bytes': next(row for row in rows if row['path'].endswith('000000000001/part.1'))['sha256'] = 'changed'
+ elif kind == 'foreign-version': next(row for row in rows if row.get('metadata'))['metadata']['versions'][0]['versionId'] = 'foreign'
+ elif kind in {'read-only-slot', 'disconnected-slot'}:
+ node = 'node1' if kind == 'read-only-slot' else 'node4'
+ after[node]['files'] = [row for row in after[node]['files'] if not (row['disk'] == 0 and row['path'].endswith('000000000000/part.1'))]
+ elif kind == 'new-key': rows.append({'disk': 0, 'path': bucket + '/dl/node1/copy.bin/part.1', 'size': 1, 'sha256': 'new'})
+ elif kind == 'bad-backup': next(row for row in rows if '/aaaaaaaa-' in row['path'] and row['path'].endswith('xl.meta.bkp'))['sha256'] = 'foreign'
+ elif kind == 'foreign-staged-version': rows.append({'disk': 0, 'path': f'{bucket}/{key}/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/12345678-1234-1234-1234-000000000001/part.1', 'size': 82, 'sha256': 'foreign'})
+ elif kind == 'unknown-transaction-file': rows.append({'disk': 0, 'path': f'{bucket}/{key}/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/unknown', 'size': 1, 'sha256': 'foreign'})
+ elif kind == 'wrong-request': receipts['workers'][0]['rows'][0]['request']['params']['VersionId'] = 'v01'
+ else: receipts['workers'][0]['rows'][0]['ack']['observation']['resultUnknown'] = True
+ with self.assertRaises(ValueError): repair.classify_deletes(before, after, fixture, receipts, plan, list(before), self.records(before, after, fixture))
+
+ def test_raw_journal_census_must_match_exact_original_ack_selector_backup_target_and_incarnation(self):
+ for kind in ('missing-peer', 'missing-record', 'wrong-selector', 'wrong-backup', 'wrong-target', 'wrong-generation', 'wrong-incarnation'):
+ with self.subTest(kind=kind):
+ before, fixture, receipts, plan = self.scene();after = self.deleted(before, fixture)
+ records = self.records(before, after, fixture);row = records['node2']['records'][0]
+ if kind == 'missing-peer': records.pop('node4')
+ elif kind == 'missing-record': records['node2']['records'] = []
+ elif kind == 'wrong-selector': row['requestVersion'] = 'unbound'
+ elif kind == 'wrong-backup': row['originalMetadataSha256'] = 'unbound'
+ elif kind == 'wrong-target': row['targetMetadataSha256'] = 'unbound'
+ elif kind == 'wrong-generation': row['originalDataDirs'][1] = 'unbound'
+ else: row['bucketIncarnation'] = 'foreign-incarnation'
+ if kind == 'wrong-incarnation':
+ damage = repair.classify_deletes(before, after, fixture, receipts, plan, list(before), records)
+ key = fixture['sides']['node1']['controlKey']
+ with patch.object(repair, 'run_task', return_value=self.status(fixture, key)), self.assertRaisesRegex(ValueError, 'incarnation'):
+ repair.recover_controls(None, None, {'endpoints': {'node1': 'url'}}, fixture, damage, [])
+ else:
+ with self.assertRaises(ValueError): repair.classify_deletes(before, after, fixture, receipts, plan, list(before), records)
+
+ def test_hidden_status_failure_wrong_counters_and_control_repairs_are_not_settlement(self):
+ for kind in ('unknown', 'counters', 'other-repair', 'missing-version'):
+ with self.subTest(kind=kind):
+ before, fixture, receipts, plan = self.scene();after = self.deleted(before, fixture)
+ damage = repair.classify_deletes(before, after, fixture, receipts, plan, list(before), self.records(before, after, fixture))
+ key = fixture['sides']['node1']['controlKey'];status = self.status(fixture, key)
+ if kind == 'unknown': status['outcome']['objects'][0]['disposition']['state'] = 'unknown'
+ elif kind == 'counters': status['outcome']['counters']['healed'] = 1
+ elif kind == 'other-repair': status['outcome']['objects'][1]['disposition']['state'] = 'repaired'
+ else: status['outcome']['objects'].pop()
+ with patch.object(repair, 'run_task', return_value=status), self.assertRaises(ValueError):
+ repair.recover_controls(None, None, {'endpoints': {'node1': '1'}}, fixture, damage, [])
+
+
+if __name__ == '__main__': unittest.main()
diff --git a/test_validation_heal_network_load_session.py b/test_validation_heal_network_load_session.py
new file mode 100644
index 00000000..37db91e9
--- /dev/null
+++ b/test_validation_heal_network_load_session.py
@@ -0,0 +1,272 @@
+"""DL lifecycle fencing, exact business ACK preservation and post-Abort physical closure."""
+
+import copy
+from datetime import datetime, timezone
+import hashlib
+import json
+from pathlib import Path
+import tempfile
+import threading
+import time
+import unittest
+
+from scripts.validation_heal import network_load_session as sessions, network_load as load
+from scripts.validation_heal.heal_probe import CLOCK_DOMAIN
+
+
+class Server:
+ def __init__(self, directory):
+ self.directory = directory
+ self.objects, self.uploads, self.calls, self.count = {}, {}, [], 0
+ self.stopped, self.fault, self.orphan, self.fail_create = False, False, False, False
+ self.lock = threading.Lock()
+ self.events = {endpoint: {role: threading.Event() for role in ('reader', 'writer')} for endpoint in ('node1', 'node3')}
+ self.seen = set()
+
+ def factory(self, binding, endpoint=None):
+ server = self
+ class Probe:
+ def __init__(self):
+ self.binding, self.endpoint_name = binding, endpoint or binding['defaultEndpoint']
+ def s3(self, operation, params, body=None):
+ assert not server.stopped, 'S3 observation while stopped'
+ assert (server.directory / 'intent.json').exists()
+ assert all((server.directory / (ep + '-' + role)).is_dir() for ep in ('node1', 'node3') for role in ('reader', 'writer'))
+ started = time.monotonic_ns()
+ with server.lock:
+ server.calls.append((self.endpoint_name, operation, copy.deepcopy(params)))
+ server.count += 1
+ serial = server.count
+ role = 'writer' if operation in load.MUTATIONS else 'reader'
+ event_key = self.endpoint_name, role
+ if server.fault and event_key not in server.seen:
+ server.seen.add(event_key)
+ server.events[self.endpoint_name][role].set()
+ assert server.events[self.endpoint_name]['reader' if role == 'writer' else 'writer'].wait(2)
+ code, status, response, unknown = None, 200, {}, False
+ key = params.get('Key')
+ if server.fault and operation in load.MUTATIONS:
+ status, code = 503, 'ServiceUnavailable'
+ elif operation == 'get_bucket_versioning': response = {'Status': 'Enabled'}
+ elif operation == 'put_object':
+ version = f'v{serial}'
+ for (k, v), item in server.objects.items():
+ if k == key: item['isLatest'] = False
+ properties = {'ETag': hashlib.md5(body).hexdigest(), 'ContentLength': len(body),
+ 'ContentType': params['ContentType'], 'Metadata': params['Metadata']}
+ server.objects[key, version] = {'key': key, 'versionId': version, 'isLatest': True,
+ 'deleteMarker': False, 'payload': load.digest(body), 'properties': properties,
+ 'tags': [{'Key': 'fixture', 'Value': 'DL'}, {'Key': 'suite', 'Value': 'heal'}]}
+ response = {'VersionId': version, 'ETag': properties['ETag']}
+ elif operation in {'head_object', 'get_object'}:
+ item = server.objects[key, params['VersionId']]
+ response = {**item['properties'], 'VersionId': item['versionId'], 'LastModified': datetime(2026, 10, 19, tzinfo=timezone.utc)}
+ elif operation == 'create_multipart_upload':
+ upload_id = f'upload-{serial}'
+ server.uploads[key, upload_id] = {}
+ response = {'UploadId': upload_id}
+ if server.fail_create: unknown, server.fail_create = True, False
+ elif operation == 'upload_part':
+ number = params['PartNumber'];etag = hashlib.md5(body).hexdigest()
+ server.uploads[key, params['UploadId']][number] = {'PartNumber': number, 'ETag': etag, 'Size': len(body)}
+ response = {'ETag': etag}
+ elif operation == 'list_multipart_uploads':
+ response = {'IsTruncated': False, 'Uploads': [{'Key': k, 'UploadId': u} for k, u in server.uploads if k.startswith(params['Prefix'])]}
+ elif operation == 'list_parts':
+ parts = server.uploads.get((key, params['UploadId']))
+ if parts is None: status, code = 404, 'NoSuchUpload'
+ else: response = {'Key': key, 'UploadId': params['UploadId'], 'IsTruncated': False, 'Parts': list(parts.values())}
+ elif operation == 'abort_multipart_upload':
+ if (key, params['UploadId']) in server.uploads:
+ del server.uploads[key, params['UploadId']];status = 204
+ else: status, code = 404, 'NoSuchUpload'
+ elif operation in {'list_objects_v2', 'list_object_versions'}: response = {'IsTruncated': False}
+ else: raise AssertionError('Unplanned operation: ' + operation)
+ if code: response = {'Error': {'Code': code}}
+ response['ResponseMetadata'] = {'HTTPStatusCode': status}
+ observed = {'operation': operation, 'params': copy.deepcopy(params), 'endpoint': self.endpoint_name,
+ 'clockDomain': CLOCK_DOMAIN, 'callStartedMonotonicNs': started,
+ 'clientCallInvoked': True, 'acknowledged': status < 300 and not unknown, 'resultUnknown': unknown,
+ 'response': None if unknown else response, 'callFinishedMonotonicNs': time.monotonic_ns()}
+ if body is not None: observed['payload'] = load.digest(body)
+ if operation == 'get_object': observed['payload'] = item['payload']
+ return observed
+ def inventory(self, bucket):
+ assert not server.stopped
+ return {'bucket': bucket, 'entries': sorted(copy.deepcopy(list(server.objects.values())), key=lambda r: (r['key'], r['versionId']))}
+ return Probe()
+
+ def business_scene(self, binding):
+ return {node: {'quiescent': self.stopped, 'pid': 0 if self.stopped else 101,
+ 'unit': 'rustfs-validation-heal-' + binding['runId'] + '.service',
+ 'binarySha256': binding['candidateBinarySha256'],
+ 'formats': [{'disk': disk, 'value': {'original': node + str(disk)}} for disk in range(4)],
+ 'files': [{'disk': 0, 'path': binding['bucketPrefix'] + '/dl/node1/control.bin/xl.meta',
+ 'size': 10, 'sha256': 'c' * 64}]}
+ for node in ('vm000', 'vm001', 'vm002', 'vm003')}
+
+ def fleet(self, binding):
+ server = self
+ class Fleet:
+ nodes = ['vm000', 'vm001', 'vm002', 'vm003']
+ def record(self, label, value): return value
+ def remote(self, node, action, **params):
+ assert server.stopped and action == 'mpu-scene'
+ keys, scopes, files = params['keys'], [], []
+ for disk in range(4):
+ for key in keys:
+ sha = hashlib.sha256((params['bucket'] + '/' + key).encode()).hexdigest()
+ existing = [(k, u) for k, u in server.uploads if k == key]
+ scopes.append({'disk': disk, 'key': key, 'keySha256': sha, 'present': bool(existing)})
+ for k, u in existing:
+ files.append({'disk': disk, 'key': key, 'path': f'.rustfs.sys/multipart/{sha}/{u}/xl.meta', 'size': 10, 'sha256': 'a' * 64})
+ if server.orphan:
+ key = keys[0];sha = hashlib.sha256((params['bucket'] + '/' + key).encode()).hexdigest()
+ files.append({'disk': 0, 'key': key, 'path': f'.rustfs.sys/multipart/{sha}/orphan/part.1', 'size': 1, 'sha256': 'b' * 64})
+ value = {'bucket': params['bucket'], 'keys': keys, 'scopes': scopes, 'directories': [],
+ 'files': files, 'bytesHashed': sum(x['size'] for x in files)}
+ return {**value, 'runId': binding['runId'], 'binarySha256': binding['candidateBinarySha256'],
+ 'quiescent': True, 'readOnly': True, 'originalPhysicalDrives': 4,
+ 'payloadIntegrityClaimed': False, 'sceneSha256': sessions.json_hash(value)}
+ return Fleet()
+
+
+class LoadSessionTests(unittest.TestCase):
+ def session(self, root):
+ directory = root / 'DL';server = Server(directory)
+ binding = {'runId': 'gh-12345-1-h2', 'candidateCommit': '1c7fc818dd858247c45b39542abd8291b1e6e31b',
+ 'bucketPrefix': 'rv-heal-owned', 'maxInventoryBytes': 512 * 1024**2, 'maxInventoryObjects': 128,
+ 'candidateBinarySha256': 'a' * 64, 'defaultEndpoint': 'node1',
+ 'endpoints': {f'node{i}': f'http://node{i}:9000' for i in range(1, 5)}}
+ value = sessions.LoadSession(binding, binding['bucketPrefix'], ['node1', 'node3'], directory, probe_factory=server.factory)
+ self.assertEqual(server.calls, [])
+ return value, server, binding
+
+ def prepared(self, root):
+ session, server, binding = self.session(root)
+ fixture = session.seed()
+ session.register_baseline(server.factory(binding).inventory(binding['bucketPrefix']))
+ server.stopped = True
+ session.register_business_baseline(server.business_scene(binding))
+ server.stopped = False
+ server.fault = True
+ self.assertTrue(session.run_load({'node1': True, 'node3': True})['complete'])
+ server.fault, server.stopped = False, True
+ session.capture_mpu(server.fleet(binding), 'pre-reconcile')
+ server.stopped = False
+ return session, server, binding
+
+ def test_full_closure_requires_distinct_recovery_acks_and_stopped_empty_mpu_scene(self):
+ with tempfile.TemporaryDirectory() as d:
+ value, server, binding = self.prepared(Path(d))
+ original = sessions.json_hash(value.registered_receipts())
+ public = value.reconcile()
+ self.assertFalse(public['namespaceCleanupAllowed'] or value.safe_to_cleanup)
+ self.assertEqual(len(public['retirements']), 6)
+ self.assertTrue(all(r['newRecoveryAbortAck'] and not r['originalAbortAckClaimed'] for r in public['retirements']))
+ with self.assertRaises(ValueError): value.finish_reconciliation(server.business_scene(binding), repair_targets=[])
+ server.stopped = True
+ value.capture_mpu(server.fleet(binding), 'post-reconcile')
+ closure = value.finish_reconciliation(server.business_scene(binding), repair_targets=[])
+ self.assertTrue(value.safe_to_cleanup and closure['namespaceCleanupAllowed'])
+ self.assertEqual(len(closure['requestEffectResolutions']), 36)
+ self.assertTrue(closure['allOriginalRequestsResolved'])
+ self.assertFalse(closure['canonicalCasePassClaimed'] or closure['originalUnknownResultsRelabeledAcknowledged'])
+ self.assertEqual(original, sessions.json_hash(value.registered_receipts()))
+ self.assertEqual(server.uploads, {})
+ count = len(server.calls)
+ with self.assertRaises(ValueError): value.run_load({'node1': True, 'node3': True})
+ self.assertEqual(len(server.calls), count)
+
+ def test_seed_failure_keeps_original_unknown_create_even_without_returned_fixture(self):
+ with tempfile.TemporaryDirectory() as d:
+ value, server, binding = self.session(Path(d));server.fail_create = True
+ with self.assertRaises(ValueError): value.seed()
+ self.assertIsNone(value.fixture)
+ self.assertEqual(value.phase, 'SEED_FAILED')
+ receipts = value.registered_receipts()
+ self.assertEqual(receipts['seed'][-1]['request']['operation'], 'create_multipart_upload')
+ self.assertEqual(receipts['seed'][-1]['resolution'], 'UNKNOWN_RESULT')
+ self.assertTrue(all(not r['rows'] for r in receipts['workers']))
+ count = len(server.calls)
+ with self.assertRaises(ValueError): value.reconcile()
+ self.assertFalse(value.safe_to_cleanup)
+ self.assertEqual(count, len(server.calls))
+ self.assertTrue(server.uploads)
+
+ def test_missing_original_version_or_unexpected_version_blocks_before_abort(self):
+ for kind in ('missing', 'extra'):
+ with self.subTest(kind=kind), tempfile.TemporaryDirectory() as d:
+ value, server, binding = self.prepared(Path(d))
+ pair = next(iter(server.objects))
+ if kind == 'missing': server.objects.pop(pair)
+ else:
+ row = copy.deepcopy(server.objects[pair]);row['versionId'] = 'unexpected';server.objects[row['key'], row['versionId']] = row
+ before = set(server.uploads)
+ with self.assertRaisesRegex(ValueError, 'versions/content/metadata/tags'): value.reconcile()
+ self.assertFalse(value.safe_to_cleanup)
+ self.assertEqual(set(server.uploads), before)
+ self.assertTrue((value.directory / 'reconcile-01/unresolved.json').exists())
+
+ def test_unknown_create_discovery_is_bound_to_original_request_and_never_fabricates_ack(self):
+ with tempfile.TemporaryDirectory() as d:
+ value, server, binding = self.prepared(Path(d))
+ server.uploads['dl/node1/create.bin', 'unacknowledged-original-id'] = {}
+ public = value.reconcile()
+ self.assertEqual(public['discoveredUploadIdsAreNotCreateAcks'], [('dl/node1/create.bin', 'unacknowledged-original-id')])
+ server.stopped = True;value.capture_mpu(server.fleet(binding), 'post-reconcile')
+ closure = value.finish_reconciliation(server.business_scene(binding), repair_targets=[])
+ create = [r for r in closure['requestEffectResolutions'] if r['actor'] == 'node1' and r['operation'] == 'create_multipart_upload']
+ self.assertEqual(len(create), 1)
+ self.assertFalse(create[0]['originalAcknowledged'])
+ self.assertTrue(value.safe_to_cleanup)
+
+ def test_unbound_pending_id_and_post_abort_orphan_never_allow_cleanup(self):
+ for kind in ('unbound-id', 'physical-orphan'):
+ with self.subTest(kind=kind), tempfile.TemporaryDirectory() as d:
+ value, server, binding = self.prepared(Path(d))
+ if kind == 'unbound-id':
+ server.uploads['dl/node1/complete.bin', 'foreign-id'] = {}
+ with self.assertRaisesRegex(ValueError, 'unbound'): value.reconcile()
+ else:
+ value.reconcile();server.stopped, server.orphan = True, True
+ value.capture_mpu(server.fleet(binding), 'post-reconcile')
+ with self.assertRaisesRegex(ValueError, 'multipart files remain'): value.finish_reconciliation(server.business_scene(binding), repair_targets=[])
+ self.assertFalse(value.safe_to_cleanup)
+
+ def test_hidden_business_orphan_or_changed_format_blocks_even_after_public_and_mpu_closure(self):
+ for kind in ('hidden-business-file', 'format', 'live', 'foreign-candidate'):
+ with self.subTest(kind=kind), tempfile.TemporaryDirectory() as d:
+ value, server, binding = self.prepared(Path(d));value.reconcile()
+ server.stopped = True;value.capture_mpu(server.fleet(binding), 'post-reconcile')
+ after = server.business_scene(binding)
+ if kind == 'hidden-business-file':
+ after['vm001']['files'].append({'disk': 1,
+ 'path': binding['bucketPrefix'] + '/dl/node1/copy.bin/uncommitted/part.1',
+ 'size': 1, 'sha256': 'd' * 64})
+ elif kind == 'format': after['vm001']['formats'][0]['value'] = {'foreign': True}
+ elif kind == 'live': after['vm001']['quiescent'] = False
+ else: after['vm001']['binarySha256'] = 'f' * 64
+ with self.assertRaises(ValueError):
+ value.finish_reconciliation(after, repair_targets=[])
+ self.assertFalse(value.safe_to_cleanup)
+
+ def test_changed_original_journal_or_missing_stopped_capture_blocks_closure(self):
+ with tempfile.TemporaryDirectory() as d:
+ value, server, binding = self.session(Path(d));value.seed()
+ value.register_baseline(server.factory(binding).inventory(binding['bucketPrefix']))
+ count = len(server.calls)
+ with self.assertRaises(ValueError): value.reconcile()
+ self.assertEqual(count, len(server.calls))
+ for suffix in ('request', 'ack'):
+ with self.subTest(suffix=suffix), tempfile.TemporaryDirectory() as d:
+ value, server, binding = self.prepared(Path(d));value.reconcile()
+ server.stopped = True;value.capture_mpu(server.fleet(binding), 'post-reconcile')
+ path = value.seed_recorder.output / ('000001-' + suffix + '.json')
+ path.write_text(path.read_text() + ' ')
+ with self.assertRaises(ValueError): value.finish_reconciliation(server.business_scene(binding), repair_targets=[])
+ self.assertFalse(value.safe_to_cleanup)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/test_validation_heal_network_retention.py b/test_validation_heal_network_retention.py
new file mode 100644
index 00000000..d76334d6
--- /dev/null
+++ b/test_validation_heal_network_retention.py
@@ -0,0 +1,223 @@
+"""Stopped I03 isolation binds business/MPU bytes and retains interrupted moves."""
+
+import copy
+import hashlib
+import json
+import os
+from pathlib import Path
+import shutil
+import tempfile
+import types
+import unittest
+from unittest.mock import patch
+
+from scripts.validation_heal import retained_scene as retained, mpu_scene, network_retention as controller, recover_environment as recovery
+import test_validation_heal_retained_scene as retained_tests
+
+
+class NetworkRetentionTests(unittest.TestCase):
+ def scene(self, directory):
+ helper = retained_tests.RetainedSceneTests()
+ state, roots, node, request = helper.scene(directory)
+ shutil.copytree(roots[1], roots[0])
+ owner = request['expectedOwner'];owner['runtimeProfile'] = 'lock-quorum'
+ for folder in (*roots, state): (folder / 'owner.json').write_text(json.dumps(owner))
+ keys = mpu_scene.keys_for(['node1', 'node3'])
+ for root in roots:
+ db = root / 'data' / (owner['bucket'] + '-db');db.mkdir()
+ (db / 'part.1').write_bytes(b'original DB payload')
+ for key in keys:
+ key_hash = hashlib.sha256((owner['bucket'] + '/' + key).encode()).hexdigest()
+ upload = root / 'data/.rustfs.sys/multipart' / key_hash / 'original-opaque-upload'
+ upload.mkdir(parents=True)
+ (upload / 'xl.meta').write_bytes(b'original uncompleted MPU metadata')
+ (upload / 'part.1').write_bytes(b'original full MPU part')
+ for suffix in ('original', 'intent.json', 'applied.json'):
+ (root / 'quarantine' / ('I03-missing-target.' + suffix)).write_bytes(('original fault ' + suffix).encode())
+ node['owned'] = lambda run: (state, roots, owner)
+ node['mpu_scene_backend'] = mpu_scene
+ def snapshot(value):
+ self.assertEqual(value['buckets'], [owner['bucket'], owner['bucket'] + '-db'])
+ files = []
+ for disk, root in enumerate(roots):
+ for bucket in value['buckets']:
+ for path in sorted((root / 'data' / bucket).rglob('*')):
+ if path.is_file(): files.append({'disk': disk, 'path': path.relative_to(root / 'data').as_posix(),
+ 'size': path.stat().st_size, 'sha256': hashlib.sha256(path.read_bytes()).hexdigest()})
+ return {'quiescent': True, 'pid': 0, 'unit': owner['unit'], 'binarySha256': owner['binarySha256'],
+ 'files': files, 'formats': [{'disk': disk, 'original': True} for disk in range(4)]}
+ node['snapshot'] = snapshot
+ scene = snapshot({'buckets': [owner['bucket'], owner['bucket'] + '-db']})
+ mpu = mpu_scene.run({'runId': request['runId'], 'action': 'mpu-scene', 'bucket': owner['bucket'],
+ 'keys': keys, 'expectedBinarySha256': owner['binarySha256']}, node)
+ request.update(expectedOriginalSceneSha256=retained.sha({name: scene[name] for name in ('files', 'formats')}),
+ expectedMpuSceneSha256=mpu['sceneSha256'], mpuKeys=keys)
+ return helper, state, roots, node, request
+
+ def test_original_business_mpu_backups_and_unread_credentials_survive_each_interrupted_move(self):
+ for interrupt_after in range(1, 6):
+ with self.subTest(interrupt_after=interrupt_after), tempfile.TemporaryDirectory() as directory:
+ helper, state, roots, node, request = self.scene(directory)
+ original = {p: (p.stat().st_ino, p.read_bytes()) for folder in (*roots, state)
+ for p in folder.rglob('*') if p.is_file()}
+ opened, real_open = [], os.open
+ def tracked(path, *args, **kwargs):
+ opened.append(str(path));return real_open(path, *args, **kwargs)
+ with helper.root_stats(), patch.object(os, 'open', side_effect=tracked):
+ before = retained.run(request, node)
+ self.assertEqual(before['baseline']['originalMpuSceneSha256'], request['expectedMpuSceneSha256'])
+ self.assertEqual(before['baseline']['archiveLayout'], 'per-run-parent')
+ change = {**request, 'action': 'retained-scene-isolate', 'expectedBaseline': before['baseline']}
+ count = 0
+ def interrupted(source, target):
+ nonlocal count
+ helper.local_rename(source, target)
+ if target.is_dir():
+ count += 1
+ if count == interrupt_after: raise RuntimeError('lost SSH after atomic rename')
+ with patch.object(retained, 'rename_new', side_effect=interrupted), self.assertRaises(RuntimeError):
+ retained.run(change, node)
+ with patch.object(retained, 'rename_new', side_effect=helper.local_rename):
+ result = retained.run(change, node)
+ self.assertFalse(retained.run(change, node)['mutated'])
+ self.assertTrue(result['activeNamespaceIsolated'])
+ self.assertFalse(result['originalSceneRestored'] or result['canonicalPassClaimed'] or result['credentialContentsRead'])
+ self.assertEqual(result['baseline'], before['baseline'])
+ self.assertEqual(result['locations'], ['retained'] * 5)
+ retained_state, retained_roots = retained.destinations(state, roots, per_run_parent=True)
+ for old, expected in original.items():
+ source = next(folder for folder in (*roots, state) if old.is_relative_to(folder))
+ target = retained_state if source == state else retained_roots[roots.index(source)]
+ moved = target / old.relative_to(source)
+ self.assertEqual((moved.stat().st_ino, moved.read_bytes()), expected)
+ self.assertTrue(all(p['uid'] == 0 and p['mode'] == 0o700 for p in result['privateArchiveParents']))
+ self.assertFalse(any(p.endswith('service.env') or p.endswith('/credential') for p in opened))
+ self.assertTrue(any('/multipart/' in p for p in opened))
+ self.assertTrue(any('-db/' in p for p in opened))
+ self.assertTrue(any('I03-missing-target.original' in p for p in opened))
+
+ def test_missing_or_changed_archived_business_mpu_scope_and_active_gates_cannot_move(self):
+ for kind in ('business-sha', 'mpu-sha', 'keys', 'missing-field', 'db-drift', 'mpu-drift', 'backup-drift', 'gate', 'missing-root'):
+ with self.subTest(kind=kind), tempfile.TemporaryDirectory() as directory:
+ helper, state, roots, node, request = self.scene(directory)
+ with helper.root_stats():
+ before = retained.run(request, node)
+ change = {**request, 'action': 'retained-scene-isolate', 'expectedBaseline': before['baseline']}
+ if kind == 'business-sha': change['expectedOriginalSceneSha256'] = 'f' * 64
+ elif kind == 'mpu-sha': change['expectedMpuSceneSha256'] = 'f' * 64
+ elif kind == 'keys': change['mpuKeys'] = change['mpuKeys'][:-1]
+ elif kind == 'missing-field': change.pop('expectedMpuSceneSha256')
+ elif kind == 'db-drift': (roots[1] / 'data' / (request['expectedOwner']['bucket'] + '-db') / 'part.1').write_bytes(b'changed')
+ elif kind == 'mpu-drift': next((roots[1] / 'data/.rustfs.sys/multipart').rglob('part.1')).write_bytes(b'changed')
+ elif kind == 'backup-drift': (roots[1] / 'quarantine/I03-missing-target.original').write_bytes(b'changed')
+ elif kind == 'gate': (state / 'network-gate.json').write_text('{}')
+ else: shutil.rmtree(roots[1])
+ with patch.object(retained, 'rename_new') as move, self.assertRaises(ValueError): retained.run(change, node)
+ move.assert_not_called()
+ self.assertTrue(state.exists())
+
+ def test_mpu_content_drift_in_retained_archive_is_not_reclassified_as_clean(self):
+ with tempfile.TemporaryDirectory() as directory:
+ helper, state, roots, node, request = self.scene(directory)
+ with helper.root_stats(), patch.object(retained, 'rename_new', side_effect=helper.local_rename):
+ baseline = retained.run(request, node)['baseline']
+ retained.run({**request, 'action': 'retained-scene-isolate', 'expectedBaseline': baseline}, node)
+ retained_roots = retained.destinations(state, roots, per_run_parent=True)[1]
+ next((retained_roots[0] / 'data/.rustfs.sys/multipart').rglob('part.1')).write_bytes(b'drift')
+ with self.assertRaisesRegex(ValueError, 'durable isolation intent'): retained.run(request, node)
+
+
+class NetworkRetentionControllerTests(unittest.TestCase):
+ def plan(self, folder):
+ helper, state, roots, node, request = NetworkRetentionTests().scene(str(folder))
+ owner = request['expectedOwner'];nodes = [f'vm00{i}' for i in range(4)]
+ binding = {'runId': request['runId'], 'candidateCommit': '1' * 40, 'harnessCommit': '2' * 40,
+ 'candidateBinarySha256': owner['binarySha256'], 'candidatePackageSha256': '3' * 64,
+ 'bucketPrefix': owner['bucket']}
+ evidence = folder / 'evidence';evidence.mkdir()
+ for i, name in enumerate(nodes):
+ (evidence / f'{i:05d}-reserved.json').write_text(json.dumps({'node': name, 'reserved': True,
+ 'owner': {**copy.deepcopy(owner), 'hostname': 'original-host-' + str(i)}}))
+ value = node['snapshot']({'buckets': [owner['bucket'], owner['bucket'] + '-db']})
+ mpu = mpu_scene.run({'runId': request['runId'], 'action': 'mpu-scene', 'bucket': owner['bucket'],
+ 'keys': request['mpuKeys'], 'expectedBinarySha256': owner['binarySha256']}, node)
+ fleet = types.SimpleNamespace(run_id=request['runId'], nodes=nodes, evidence=evidence,
+ network_gates=set(), quorum_gates=set(), record=lambda label, value: value)
+ plan = controller.prepare(fleet, binding, request['mpuKeys'],
+ {n: copy.deepcopy(value) for n in nodes}, {n: copy.deepcopy(mpu) for n in nodes})
+ return fleet, plan, binding
+
+ def test_all_four_preflights_precede_any_move_and_no_restoration_or_cleanup_is_issued(self):
+ for failure in (None, 'preflight-vm003', 'move-vm001', 'post-drift-vm002'):
+ with self.subTest(failure=failure), tempfile.TemporaryDirectory() as directory:
+ fleet, plan, binding = self.plan(Path(directory));events, moved = [], set()
+ def remote(node, action, **params):
+ self.assertEqual(params['expectedOwner'], plan['owners'][node])
+ self.assertEqual(params['expectedMpuSceneSha256'], plan['mpuSceneSha256'][node])
+ events.append((node, action))
+ if failure == 'preflight-' + node and action.endswith('audit') and not moved:
+ raise ValueError('last peer preflight failed')
+ if action.endswith('isolate'):
+ moved.add(node)
+ if failure == 'move-' + node: raise ValueError('lost SSH after move')
+ return {'baseline': {'original': node if failure != 'post-drift-' + node or len(moved) < 4 else 'drift'},
+ 'inspectionOnly': action.endswith('audit'), 'mutated': action.endswith('isolate'),
+ 'credentialContentsRead': False, 'activeNamespaceIsolated': node in moved,
+ 'originalSceneRestored': False, 'backupRestorationUsed': False, 'canonicalPassClaimed': False,
+ 'privateArchiveParents': [{'exists': True, 'uid': 0, 'mode': 0o700}] * 5}
+ fleet.remote = remote
+ if failure:
+ with self.assertRaises(ValueError): controller.isolate(fleet, plan)
+ else:
+ result = controller.isolate(fleet, plan)
+ self.assertEqual(result['cleanup'], 'ISOLATED')
+ self.assertFalse(result['originalSceneRestored'] or result['namespaceDeletionPerformed'] or result['canonicalPassClaimed'])
+ self.assertEqual(events[:4], [(n, 'retained-scene-audit') for n in fleet.nodes])
+ self.assertTrue(all(action in {'retained-scene-audit', 'retained-scene-isolate'} for n, action in events))
+ if failure == 'preflight-vm003': self.assertFalse(moved)
+
+ def test_archived_plan_requires_original_failed_case_binding_and_all_reserved_owners(self):
+ for kind in ('valid', 'source', 'helper', 'run', 'result', 'owner', 'other-retained'):
+ with self.subTest(kind=kind), tempfile.TemporaryDirectory() as directory:
+ root = Path(directory);fleet, plan, binding = self.plan(root)
+ case = 'heal.network-partition-and-lock-quorum';base = root / 'artifact/run' / case
+ folder = base / 'evidence' / case / 'H2/read-boundary';folder.mkdir(parents=True)
+ (base / 'cases').mkdir()
+ result = {'case_id': case, 'verdict': 'FAIL', 'cleanup_status': 'FAIL', 'provenance': {
+ 'source_commit': binding['candidateCommit'], 'candidate_sha256': binding['candidatePackageSha256'],
+ 'binary_sha256': binding['candidateBinarySha256'], 'testing_commit': binding['harnessCommit']}}
+ candidate = {'source_sha': binding['candidateCommit'], 'package_sha256': binding['candidatePackageSha256']}
+ summary = {'cleanup': 'FAIL', 'results': [{'topology': 'H2', 'branch': 'read-boundary', 'verdict': 'FAIL', 'cleanup': 'FAIL'}]}
+ if kind == 'source': candidate['source_sha'] = 'f' * 40
+ elif kind == 'helper': binding['harnessCommit'] = 'e' * 40
+ elif kind == 'run': plan['runId'] = 'gh-54321-1-h2'
+ elif kind == 'result': result['verdict'] = 'PASS'
+ elif kind == 'other-retained': summary['results'].append({'branch': 'rejoin', 'cleanup': 'FAIL'})
+ (base / 'cases' / (case + '.json')).write_text(json.dumps(result))
+ (folder.parent.parent / 'healthy-summary.json').write_text(json.dumps(summary))
+ (folder / '00001-network-retention-intent.json').write_text(json.dumps(plan))
+ (folder / '00002-binding.json').write_text(json.dumps(binding))
+ for p in fleet.evidence.glob('*-reserved.json'): shutil.copy2(p, folder / p.name)
+ if kind == 'owner':
+ p = next(folder.glob('*-reserved.json'));row = json.loads(p.read_text());row['owner']['runtimeProfile'] = 'manual';p.write_text(json.dumps(row))
+ if kind == 'valid': self.assertEqual(recovery.network_recovery_plan(root, 12345, candidate), [plan])
+ else:
+ with self.assertRaises(ValueError): recovery.network_recovery_plan(root, 12345, candidate)
+
+ def test_recovery_never_enters_already_clean_or_deletes_an_isolated_scene(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory);fleet, plan, binding = self.plan(root)
+ candidate = root / 'candidate.json';candidate.write_text('{}')
+ argv = ['recover', '--failed-run-id', '12345', '--artifact', str(root), '--candidate', str(candidate), '--evidence', str(root / 'out')]
+ result = {'cleanup': 'ISOLATED', 'originalSceneRestored': False, 'canonicalPassClaimed': False}
+ with patch('sys.argv', argv), patch.object(recovery, 'recovery_plan', return_value=[plan]), \
+ patch.object(recovery, 'isolate_network_scene', return_value=result) as isolate, \
+ patch.object(recovery.Fleet, 'remote', side_effect=AssertionError('ordinary cleanup/namespace shortcut')):
+ recovery.main()
+ isolate.assert_called_once()
+ output = json.loads((root / 'out/recovery-summary.json').read_text())
+ self.assertEqual(output['cleanup'], 'ISOLATED')
+ self.assertFalse(output['canonicalPassClaimed'])
+
+
+if __name__ == '__main__': unittest.main()
diff --git a/test_validation_heal_nolock.py b/test_validation_heal_nolock.py
new file mode 100644
index 00000000..1330f4b6
--- /dev/null
+++ b/test_validation_heal_nolock.py
@@ -0,0 +1,161 @@
+import copy
+from contextlib import redirect_stdout
+import io
+import json
+from pathlib import Path
+import tempfile
+import unittest
+from unittest.mock import patch
+
+from scripts.validation_heal import nolock
+from scripts.validation_heal.matrix import AUTOMATIC_CASES, select
+import test_validation_heal_race_native as native_fixtures
+
+HELPER = native_fixtures.HELPER
+
+
+class NolockTests(unittest.TestCase):
+ def test_source_specific_nolock_wire_true_and_effective_false_are_exact(self):
+ for source in (nolock.REFERENCE['candidateCommit'],nolock.COPY_CANDIDATE):
+ with self.subTest(source=source):
+ ref = nolock.source_contract({'source_sha':source})
+ self.assertEqual(ref['candidateCommit'],source)
+ self.assertIs(ref['requested']['nolock'],True)
+ self.assertIs(ref['effective']['nolock'],False)
+ value = {'httpStatus':200,'response':{'summary':'running','settings':copy.deepcopy(ref['effective'])}}
+ nolock.settings(value,ref)
+ for field,wrong in [('nolock',True),('pool',0),('set',False),('recursive',1),('readRepair',True)]:
+ bad = copy.deepcopy(value);bad['response']['settings'][field] = wrong
+ with self.subTest(field=field),self.assertRaises(ValueError):nolock.settings(bad,ref)
+
+ def test_exact_owner_group_admits_a09_only_and_never_executes_downloaded_native(self):
+ candidate = {'source_sha':nolock.COPY_CANDIDATE,'package_sha256':'b'*64}
+ with tempfile.TemporaryDirectory() as tmp:
+ root = Path(tmp)
+ native_fixtures.RaceNativeTests().fixture(root,source=nolock.COPY_CANDIDATE)
+ with patch('scripts.validation_heal.race_native.subprocess.run',side_effect=AssertionError('no executable')):
+ proof = nolock.native_preflight(candidate,HELPER,root)
+ self.assertEqual(proof['requiredRegressionLabels'],['default-heal-exclusive-owner'])
+ self.assertTrue(proof['requiredNativeSubsetPassed'])
+ self.assertFalse(proof['wholeCasePassClaimed'] or proof['canonicalCasePassClaimed'])
+ self.assertIsNone(nolock.native_preflight({'source_sha':nolock.REFERENCE['candidateCommit']},HELPER,''))
+ for source in ('e'*40,'bd0671ba781d8ebf509f4f03533228dfb44496f1'):
+ with self.subTest(source=source),self.assertRaises(ValueError):
+ nolock.native_preflight({**candidate,'source_sha':source},HELPER,root)
+ with self.assertRaises(ValueError):nolock.native_preflight(candidate,HELPER,'')
+ with self.assertRaises(ValueError):nolock.native_preflight(candidate,'f'*40,root)
+ summary = root/'native-evidence/race-native-results.json'
+ value = json.loads(summary.read_text())
+ value['results'] = [r for r in value['results'] if r['label'] != 'default-heal-exclusive-owner']
+ summary.write_text(json.dumps(value))
+ with self.assertRaisesRegex(ValueError,'omit or duplicate'):nolock.native_preflight(candidate,HELPER,root)
+
+ def test_real_preflight_cli_returns_before_fleet_creation(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ root = Path(tmp);native = root/'native';native.mkdir()
+ native_fixtures.RaceNativeTests().fixture(native,source=nolock.COPY_CANDIDATE)
+ candidate = root/'candidate.json'
+ candidate.write_text(json.dumps({'source_sha':nolock.COPY_CANDIDATE,'package_sha256':'b'*64}))
+ argv = ['nolock.py','preflight','--candidate',str(candidate),'--case-id',nolock.CASE_ID,'--testing-commit',HELPER]
+ output = io.StringIO()
+ with patch.object(nolock.sys,'argv',argv),patch.dict(nolock.os.environ,{'VALIDATION_HEAL_NATIVE_DIRECTORY':str(native)}),\
+ patch.object(nolock,'Fleet',side_effect=AssertionError('preflight must not create fleet')),redirect_stdout(output):
+ nolock.main()
+ proof = json.loads(output.getvalue())
+ self.assertFalse(proof['fleetMutationRequested'] or proof['canonicalCasePassClaimed'])
+ self.assertEqual(proof['nativeProof']['requiredRegressionLabels'],nolock.NATIVE_GROUP)
+
+ def test_real_main_keeps_every_public_branch_and_stops_after_dirty_cleanup(self):
+ for dirty in (False,True):
+ with self.subTest(dirty=dirty),tempfile.TemporaryDirectory() as tmp:
+ root=Path(tmp);native=root/'native';native.mkdir();evidence=root/'evidence';evidence.mkdir()
+ native_fixtures.RaceNativeTests().fixture(native,source=nolock.COPY_CANDIDATE)
+ candidate=root/'candidate.json'
+ candidate.write_text(json.dumps({'source_sha':nolock.COPY_CANDIDATE,'package_sha256':'b'*64}))
+ argv=['nolock.py','--candidate',str(candidate),'--rc-bin',str(root/'rc'),'--evidence',str(evidence),
+ '--tool-directory',str(root),'--case-id',nolock.CASE_ID,'--testing-commit',HELPER,'--binary-sha','c'*64]
+ calls=[]
+ def run(args,topology,nodes,branch):
+ calls.append((topology,len(nodes),branch))
+ return {'topology':topology,'branch':branch,'verdict':'FAIL' if dirty else 'PASS',
+ 'cleanup':'FAIL' if dirty else 'PASS','canonicalCasePassClaimed':False}
+ with patch.object(nolock.sys,'argv',argv),patch.object(nolock.signal,'signal'),\
+ patch.dict(nolock.os.environ,{'VALIDATION_HEAL_NATIVE_DIRECTORY':str(native),'GITHUB_RUN_ID':'12345',
+ 'RUSTFS_NODES':'vm000 vm001 vm002 vm003'}),\
+ patch.object(nolock,'run_branch',side_effect=run),\
+ patch.object(nolock,'Fleet',side_effect=AssertionError('mock public branches do not create fleet')):
+ if dirty:
+ with self.assertRaisesRegex(ValueError,'coverage incomplete'):nolock.main()
+ else:nolock.main()
+ summary=json.loads((evidence/'healthy-summary.json').read_text())
+ self.assertIs(summary['complete'],not dirty)
+ self.assertFalse(summary['canonicalCasePassClaimed'] or summary['nativePrerequisitesBlocked'])
+ self.assertEqual(len(calls),1 if dirty else 4)
+ if not dirty:self.assertEqual(calls,[(top,len(nodes),branch) for top,nodes in
+ [('H1',['vm000']),('H2',['vm000','vm001','vm002','vm003'])] for branch in nolock.BLACK_BOX])
+ native_rows=[r for r in summary['results'] if r['branch']==nolock.BRANCHES[-1]]
+ self.assertEqual(len(native_rows),2)
+ self.assertTrue(all(r['nativeFenceProved'] and not r['fleetExecutionClaimed']
+ and r['nativeProof']['requiredRegressionLabels']==nolock.NATIVE_GROUP for r in native_rows))
+
+ def test_source_branches_and_fixture_mapping_are_preserved(self):
+ recipe = select(nolock.CASE_ID)[0]
+ self.assertEqual(recipe["variants"], list(nolock.BRANCHES))
+ self.assertIn(nolock.CASE_ID, AUTOMATIC_CASES)
+ self.assertEqual(nolock.REFERENCE["requested"]["nolock"], True)
+ self.assertEqual(nolock.REFERENCE["effective"]["nolock"], False)
+ self.assertIsNone(nolock.REFERENCE["requested"]["pool"])
+ self.assertIsNone(nolock.REFERENCE["effective"]["set"])
+
+ def test_settings_require_exact_lock_safe_normalization(self):
+ status = {"httpStatus": 200, "response": {"summary": "running", "settings": nolock.REFERENCE["effective"]}}
+ self.assertEqual(nolock.settings(status), nolock.REFERENCE["effective"])
+ for changed in (
+ {**nolock.REFERENCE["effective"], "nolock": True},
+ {**nolock.REFERENCE["effective"], "recursive": False},
+ {**nolock.REFERENCE["effective"], "pool": 0},
+ {**nolock.REFERENCE["effective"], "set": False},
+ ):
+ with self.subTest(changed=changed), self.assertRaises(ValueError):
+ nolock.settings({"httpStatus": 200, "response": {"summary": "finished", "settings": changed}})
+
+ def test_blocked_native_fence_is_kept_in_the_denominator(self):
+ native = nolock.native_blocked({"source_sha": nolock.REFERENCE["candidateCommit"]}, "H1")
+ rows = [
+ {"topology": topology, "branch": branch, "verdict": "PASS", "cleanup": "PASS"}
+ for topology in ("H1", "H2") for branch in nolock.BLACK_BOX
+ ]
+ self.assertFalse(nolock.complete_results([*rows, native]))
+ self.assertFalse(nolock.complete_results(rows))
+ self.assertFalse(nolock.complete_results([*rows[:-1], native]))
+ failed = copy.deepcopy(rows)
+ failed[0]["verdict"] = "FAIL"
+ self.assertFalse(nolock.complete_results([*failed, native]))
+ self.assertFalse(native["canonicalCasePassClaimed"])
+
+ def test_run_case_dispatch_and_source_contract(self):
+ dispatch = Path("scripts/validation_heal/run_case.sh").read_text()
+ self.assertIn("heal.admin-nolock-safety) driver=nolock.py", dispatch)
+ catalog = json.loads(Path("validation-cases.json").read_text())
+ source = {case["id"]: case for module in catalog["modules"] for case in module["cases"]}[nolock.CASE_ID]
+ self.assertEqual(source["probes"]["designCode"], "A09")
+ self.assertEqual(source["probes"]["topology"], "H1,H2")
+
+ def test_foreground_plan_keeps_three_overwrites_or_one_exact_delete(self):
+ expected = [{"key": nolock.TARGET_KEY, "versionId": value, "isLatest": value == "v2",
+ "deleteMarker": False} for value in ("v0", "v1", "v2")]
+ overwrites = nolock.operation_plan("rv-heal-0123456789abcdef", "overwrite-nolock", expected)
+ self.assertEqual(len(overwrites), 3)
+ self.assertEqual([step["params"]["Metadata"]["nonce"] for step in overwrites], ["0", "1", "2"])
+ deletion = nolock.operation_plan("rv-heal-0123456789abcdef", "delete-nolock", expected)
+ self.assertEqual(deletion[0]["params"]["VersionId"], "v0")
+
+ def test_native_blocked_rows_cannot_become_a_canonical_pass(self):
+ row = nolock.native_blocked({"source_sha": nolock.REFERENCE["candidateCommit"]}, "H2")
+ self.assertEqual(row["verdict"], "BLOCKED")
+ self.assertFalse(row["canonicalCasePassClaimed"])
+ self.assertFalse(row["nativeFenceProved"])
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test_validation_heal_options.py b/test_validation_heal_options.py
new file mode 100644
index 00000000..924951a2
--- /dev/null
+++ b/test_validation_heal_options.py
@@ -0,0 +1,516 @@
+import copy
+from contextlib import ExitStack
+import json
+from pathlib import Path
+import tempfile
+import types
+import unittest
+from unittest.mock import patch
+
+from scripts.validation_heal import options
+from scripts.validation_heal.matrix import select, AUTOMATIC_CASES
+from scripts.validation_heal.healthy import Fleet
+import test_validation_heal_scopes as scope_fixtures
+
+
+class OptionsTests(unittest.TestCase):
+ bucket = 'rv-heal-0123456789abcdef'
+
+ def mapped(self):
+ rows = [(options.KEYS[0], 'null', 0, False), (options.KEYS[0], 'historic', 0, False),
+ (options.KEYS[1], 'child', 1, False), (options.KEYS[2], 'control', 1, False),
+ (options.KEYS[-1], 'marker', 0, True)]
+ return {(self.bucket, key, version): {'poolIndex': 0, 'setIndex': number, 'deleteMarker': marker,
+ 'isLatest': version != 'historic'}
+ for key, version, number, marker in rows}
+
+ def status(self, branch='selector-valid'):
+ _, request = options.paired_options(branch)
+ mapped = self.mapped()
+ selected = options.selection(branch, request, mapped)
+ repaired = {identity for identity in selected if identity[2] == 'historic'}
+ receipts = []
+ for identity in sorted(selected):
+ state = 'repaired' if identity in repaired else 'metadata_healthy' if mapped[identity]['deleteMarker'] else 'verified_healthy'
+ receipts.append({'identity': {'kind': 'object', 'bucket': identity[0], 'object': identity[1],
+ 'versionId': identity[2], 'poolIndex': request['pool'], 'setIndex': request['set'],
+ 'bucketIncarnationId': 'current-incarnation'}, 'disposition': {'state': state}})
+ status = {'summary': 'finished', 'settings': request, 'outcome': {'execution': {'state': 'completed'},
+ 'coverage': 'complete', 'objectsTruncated': False, 'objects': receipts,
+ 'counters': {'processed': len(selected), 'healed': len(repaired), 'unchanged': len(selected) - len(repaired),
+ 'failed': 0, 'unknown': 0, 'skipped': 0, 'attemptFailures': 0, 'overflowed': False}}}
+ return status, mapped, selected, repaired, request
+
+ def test_all_source_options_topologies_and_two_fixture_types_are_preserved(self):
+ self.assertEqual(select(options.CASE_ID)[0]['variants'], list(options.BRANCHES))
+ self.assertIn(options.CASE_ID, AUTOMATIC_CASES)
+ self.assertEqual(options.FIXTURES, ('DB', 'DV'))
+ self.assertEqual(options.REFERENCE['support']['readRepair'],
+ 'unsupported for Admin; HTTP 400 InvalidArgument before execution')
+
+ def test_request_pairs_do_not_change_dangerous_or_unrelated_defaults(self):
+ for branch in options.BRANCHES:
+ baseline, selected = options.paired_options(branch)
+ self.assertFalse(baseline['remove'])
+ self.assertEqual(selected['remove'], branch == 'remove-true')
+ self.assertFalse(baseline['nolock'])
+ self.assertFalse(selected['nolock'])
+ self.assertEqual(baseline['scanMode'], selected['scanMode'])
+ changed = {name for name in baseline if baseline[name] != selected[name]}
+ self.assertEqual(len(changed), 2 if branch.startswith('selector-') else 0 if branch == 'remove-false' else 1)
+
+ def test_remove_true_preserves_the_frozen_recursive_bucket_source_procedure(self):
+ baseline, selected = options.paired_options('remove-true')
+ self.assertTrue(baseline['recursive'])
+ self.assertTrue(selected['recursive'])
+ self.assertEqual(options.request_path('remove-true', self.bucket),
+ '/rustfs/admin/v3/heal/' + self.bucket)
+ self.assertEqual(options.source_phases('remove-true'), ('selected',))
+ self.assertEqual(options.source_phases('remove-false'), ('baseline', 'selected'))
+ with self.assertRaisesRegex(ValueError, 'source procedure'):
+ options.run_instance(None, 'H1', ['node'], 'remove-true', 'DV', 'baseline')
+
+ def orphan_scene(self):
+ import uuid
+ directory = str(uuid.UUID(int=11)); version = str(uuid.UUID(int=12))
+ authority = {'node': {'formats': [{'disk': disk, 'sha256': str(disk)} for disk in range(4)],
+ 'files': [{'disk': disk, 'path': self.bucket + '/control.bin/xl.meta', 'size': 7, 'sha256': 'control'}
+ for disk in range(4)]}}
+ original = copy.deepcopy(authority)
+ for disk in range(4):
+ for name in ('xl.meta', directory + '/part.1', directory + '/part.1.integrity'):
+ original['node']['files'].append({'disk': disk, 'path': self.bucket + '/' + options.orphan.KEY + '/' + name,
+ 'size': 100, 'sha256': str(disk) + name})
+ before = copy.deepcopy(authority)
+ before['node']['files'] += [copy.deepcopy(r) for r in original['node']['files']
+ if r['disk'] == 0 and '/' + options.orphan.KEY + '/' in r['path']]
+ target = {'node': 'node', 'disk': 0, 'dataDir': directory, 'versionId': version,
+ 'logical': {'erasure': {'data_blocks': 2, 'parity_blocks': 2}}}
+ return original, before, authority, target
+
+ def test_full_original_orphan_tree_and_one_below_quorum_member_are_required(self):
+ original, before, authority, target = self.orphan_scene()
+ options.validate_orphan_replay(original, before, authority, self.bucket, target)
+ for kind in ('data-only', 'sidecar', 'second-member', 'control', 'format', 'original-format', 'original-control', 'not-below-quorum'):
+ scene, changed, source = copy.deepcopy(before), copy.deepcopy(target), copy.deepcopy(original)
+ if kind == 'data-only': scene['node']['files'] = [r for r in scene['node']['files']
+ if not r['path'].endswith(options.orphan.KEY + '/xl.meta')]
+ elif kind == 'sidecar': scene['node']['files'] = [r for r in scene['node']['files']
+ if not r['path'].endswith('part.1.integrity')]
+ elif kind == 'second-member':
+ row = copy.deepcopy(scene['node']['files'][-1]); row['disk'] = 1; scene['node']['files'].append(row)
+ elif kind == 'control': scene['node']['files'][0]['sha256'] = 'changed'
+ elif kind == 'format': scene['node']['formats'][0]['sha256'] = 'changed'
+ elif kind == 'original-format': source['node']['formats'][0]['sha256'] = 'changed'
+ elif kind == 'original-control': source['node']['files'][0]['sha256'] = 'changed'
+ else: changed['logical']['erasure']['data_blocks'] = 1
+ with self.subTest(kind=kind), self.assertRaises(ValueError):
+ options.validate_orphan_replay(source, scene, authority, self.bucket, changed)
+
+ def test_source_orphan_replay_uses_actual_h2_sixteen_member_read_quorum(self):
+ original, before, authority, target = self.orphan_scene()
+ source = {}; injected = {}; deleted = {}
+ for index in range(4):
+ name = 'node' + str(index)
+ source[name] = copy.deepcopy(original['node'])
+ deleted[name] = copy.deepcopy(authority['node'])
+ injected[name] = copy.deepcopy(before['node'] if index == 0 else authority['node'])
+ target['node'] = 'node0'
+ target['logical']['erasure'] = {'data_blocks': 8, 'parity_blocks': 8}
+ result = options.validate_orphan_replay(source, injected, deleted, self.bucket, target)
+ self.assertEqual(result['replayedMembers'], 1)
+ self.assertEqual(result['independentlyDecodedReadQuorum'], 8)
+ injected['node1'] = copy.deepcopy(before['node'])
+ with self.assertRaises(ValueError): options.validate_orphan_replay(source, injected, deleted, self.bucket, target)
+
+ def test_single_deleted_orphan_generation_keeps_all_dv_controls_and_marker_acks(self):
+ calls = []
+ def call(op, params, body=None):
+ calls.append((op, dict(params)))
+ return {'VersionId': str(__import__('uuid').UUID(int=len(calls))),
+ **({'DeleteMarker': True} if op == 'delete_object' else {})}
+ recorder = types.SimpleNamespace(call=call, pending=None)
+ keys = (options.orphan.KEY, 'control.bin', 'marker-control.bin')
+ manifest = options.seed_fixture(recorder, self.bucket, 'DV', keys,
+ single_version_key=options.orphan.KEY)
+ orphan_rows = [r for r in manifest if r['key'] == options.orphan.KEY]
+ self.assertEqual(len(orphan_rows), 1)
+ self.assertTrue(orphan_rows[0]['isLatest'])
+ self.assertFalse(orphan_rows[0]['deleteMarker'])
+ self.assertEqual(len([r for r in manifest if r['key'] == 'control.bin']), 3)
+ self.assertEqual(len([r for r in manifest if r['key'] == 'marker-control.bin']), 4)
+ self.assertEqual(sum(r['deleteMarker'] for r in manifest), 1)
+
+ def test_source_orphan_execution_removes_whole_tree_before_reads_and_rescues_only_delete_authority(self):
+ for state, failure in (('repaired', None), ('authoritatively_absent', None),
+ ('repaired', 'leftover-metadata'), ('repaired', 'lost-install-ack'),
+ ('repaired', 'failed-authority-restore')):
+ original, before, authority, target = self.orphan_scene()
+ _, request = options.paired_options('remove-true')
+ version = target['versionId']; control_version = str(__import__('uuid').UUID(int=13))
+ manifest = [{'key': options.orphan.KEY, 'versionId': version, 'isLatest': True, 'deleteMarker': False},
+ {'key': 'control.bin', 'versionId': control_version, 'isLatest': True, 'deleteMarker': False}]
+ mapped = {(self.bucket, r['key'], r['versionId']): {
+ **(target if r['key'] == options.orphan.KEY else {}), 'poolIndex': 0, 'setIndex': 0,
+ 'deleteMarker': False, 'isLatest': True} for r in manifest}
+ status = {'summary': 'finished', 'settings': request, 'outcome': {
+ 'execution': {'state': 'completed'}, 'coverage': 'complete', 'objectsTruncated': False,
+ 'counters': {'processed': 2, 'healed': int(state == 'repaired'),
+ 'unchanged': 2 - int(state == 'repaired'), 'failed': 0, 'unknown': 0,
+ 'skipped': 0, 'attemptFailures': 0, 'overflowed': False},
+ 'objects': [{'identity': {'kind': 'object', 'bucket': self.bucket, 'object': r['key'],
+ 'versionId': r['versionId'], 'poolIndex': None, 'setIndex': None,
+ 'bucketIncarnationId': 'one-incarnation'}, 'disposition': {
+ 'state': state if r['key'] == options.orphan.KEY else 'verified_healthy'}} for r in manifest]}}
+ events = []
+ after = copy.deepcopy(authority)
+ if failure == 'leftover-metadata':
+ after['node']['files'].append(copy.deepcopy(next(r for r in before['node']['files']
+ if r['path'].endswith(options.orphan.KEY + '/xl.meta'))))
+ def snapshot(label, **kw):
+ events.append(label)
+ if label == 'A10-orphan-original-all-members': return original
+ if label in ('A10-orphan-fault-before-GET', 'A10-orphan-immediately-before-START'): return before
+ if label == 'A10-orphan-product-before-GET-or-rescue': return after
+ if label == 'A10-orphan-last-DELETE-restored' and failure == 'failed-authority-restore': return before
+ return authority
+ def remote(node, action, **params):
+ events.append((action, params))
+ self.assertEqual(action, 'orphan-install')
+ self.assertEqual(params['mode'], 'object')
+ if failure == 'lost-install-ack': raise RuntimeError('lost original install ACK')
+ return {'state': 'APPLIED', 'mode': 'object'}
+ fleet = types.SimpleNamespace(record=lambda label, value: value, snapshot=snapshot, remote=remote,
+ stop=lambda: None, start=lambda: None, orphan_faults=[])
+ recorder = types.SimpleNamespace(pending=None, call=unittest.mock.Mock(return_value={
+ 'VersionId': version, 'ResponseMetadata': {'HTTPStatusCode': 204}}))
+ probe = types.SimpleNamespace(inventory=lambda *a, **k: {})
+ peer = types.SimpleNamespace(s3=unittest.mock.Mock(return_value={
+ 'acknowledged': False, 'resultUnknown': False, 'response': {
+ 'ResponseMetadata': {'HTTPStatusCode': 404}, 'Error': {'Code': 'NoSuchVersion'}}}))
+ with self.subTest(state=state, failure=failure), ExitStack() as stack:
+ for name, value in [('seed_fixture', lambda *a, **k: copy.deepcopy(manifest)),
+ ('verify_seed', lambda *a: None), ('verify_parts', lambda *a: {}),
+ ('verify_public', lambda *a: None), ('idle_fleet', lambda *a: None),
+ ('Probe', lambda *a: peer)]: stack.enter_context(patch.object(options, name, value))
+ stack.enter_context(patch.object(options, 'map_fixture', side_effect=[mapped,
+ {k: v for k, v in mapped.items() if k[1] != options.orphan.KEY}]))
+ task = stack.enter_context(patch.object(options, 'run_request', return_value=status))
+ captured = stack.enter_context(patch.object(options.orphan, 'capture'))
+ armed = stack.enter_context(patch.object(options.orphan, 'arm_all'))
+ recovered = stack.enter_context(patch.object(options.orphan, 'recover_all'))
+ if failure:
+ with self.assertRaises((ValueError, RuntimeError)):
+ options.orphan_probe(fleet, probe, {'endpoints': ['peer']}, recorder,
+ self.bucket, 'H1', 'DV', request, [])
+ else:
+ result = options.orphan_probe(fleet, probe, {'endpoints': ['peer']}, recorder,
+ self.bucket, 'H1', 'DV', request, [])
+ self.assertTrue(result['physical']['wholeOrphanObjectRemoved'])
+ self.assertFalse(result['rescueIsProductRepair'])
+ captured.assert_called_once_with(fleet, original, self.bucket, 'old')
+ armed.assert_called_once_with(fleet, authority, self.bucket, 'authority')
+ recovered.assert_called_once_with(fleet)
+ self.assertEqual(fleet.a10_orphan_restored, failure != 'failed-authority-restore')
+ if failure in ('leftover-metadata', 'lost-install-ack'): peer.s3.assert_not_called()
+ if failure == 'lost-install-ack': task.assert_not_called()
+ else: self.assertEqual(task.call_args.args[2], '/rustfs/admin/v3/heal/' + self.bucket)
+ self.assertEqual(recorder.call.call_args.args, ('delete_object', {
+ 'Bucket': self.bucket, 'Key': options.orphan.KEY, 'VersionId': version}))
+
+ def test_remove_false_uses_original_repairable_scope_and_only_source_remove_true_uses_orphan_runtime(self):
+ for branch, phase in (('remove-false', 'baseline'), ('remove-false', 'selected'), ('remove-true', 'selected')):
+ with tempfile.TemporaryDirectory() as directory, ExitStack() as stack:
+ root = Path(directory); candidate = root / 'candidate.json'; rc = root / 'rc'; rc.write_bytes(b'runner')
+ candidate.write_text(json.dumps({'source_sha': options.REFERENCE['candidateCommit'], 'package_sha256': 'a' * 64}))
+ args = types.SimpleNamespace(evidence=root, candidate=candidate, rc_bin=rc, binary_sha='b' * 64,
+ tool_directory=root, testing_commit='c' * 40)
+ fleet = types.SimpleNamespace(record=lambda label, value: value, deploy=lambda *a: None, cleanup=lambda: True)
+ factory = stack.enter_context(patch.object(options, 'Fleet', return_value=fleet))
+ stack.enter_context(patch.object(options, 'verify_inspector', return_value={}))
+ stack.enter_context(patch.object(options, 'Probe', return_value=object()))
+ stack.enter_context(patch.object(options, 'FixtureRecorder', return_value=types.SimpleNamespace(pending=None)))
+ stack.enter_context(patch.dict(options.os.environ, {'GITHUB_RUN_ID': '12345', 'GITHUB_RUN_ATTEMPT': '1'}))
+ scope = stack.enter_context(patch.object(options, 'scope_probe', return_value={}))
+ orphan = stack.enter_context(patch.object(options, 'orphan_probe', return_value={}))
+ result = options.run_instance(args, 'H1', ['node'], branch, 'DB', phase)
+ self.assertEqual(result['verdict'], 'PASS')
+ self.assertEqual(factory.call_args.kwargs['runtime_profile'],
+ 'orphan-preserve' if branch == 'remove-true' else 'manual')
+ self.assertEqual(orphan.call_count, int(branch == 'remove-true'))
+ self.assertEqual(scope.call_count, int(branch == 'remove-false'))
+
+ def test_prefix_exact_and_recursive_scopes_do_not_include_adjacent_control(self):
+ mapped = self.mapped()
+ baseline, selected = options.paired_options('recursive-true')
+ self.assertEqual(options.selection('recursive-true', baseline, mapped), set())
+ identities = options.selection('recursive-true', selected, mapped)
+ self.assertEqual({identity[1] for identity in identities}, {options.KEYS[0], options.KEYS[1]})
+ self.assertEqual(len(identities), 3)
+ self.assertNotIn((self.bucket, options.KEYS[2], 'control'), identities)
+
+ def test_selector_uses_measured_sets_and_retains_historical_and_marker_identities(self):
+ status, mapped, selected, repaired, request = self.status()
+ proof = options.validate_outcome(status, mapped, selected, repaired, request)
+ self.assertEqual(proof['selectedACKs'], 3)
+ self.assertEqual(proof['repairedACKs'], 1)
+ self.assertIn((self.bucket, options.KEYS[-1], 'marker'), selected)
+
+ def test_exact_object_request_selects_only_the_acknowledged_current_version(self):
+ mapped = self.mapped()
+ for branch in ('recreate', 'update-parity', 'read-repair'):
+ for request in options.paired_options(branch):
+ with self.subTest(branch=branch, request=request):
+ selected = options.selection(branch, request, mapped)
+ self.assertEqual(selected, {(self.bucket, options.KEYS[0], 'null')})
+ self.assertIn((self.bucket, options.KEYS[0], 'historic'),
+ options.selection('recursive-true', {'recursive': True}, mapped))
+
+ def test_exact_object_scope_rejects_missing_or_ambiguous_current_ack(self):
+ for kind in ('missing', 'duplicate', 'untyped'):
+ mapped = self.mapped()
+ if kind == 'missing': mapped[(self.bucket, options.KEYS[0], 'null')]['isLatest'] = False
+ elif kind == 'duplicate': mapped[(self.bucket, options.KEYS[0], 'historic')]['isLatest'] = True
+ else: mapped[(self.bucket, options.KEYS[0], 'null')]['isLatest'] = 1
+ with self.subTest(kind=kind), self.assertRaisesRegex(ValueError, 'current ACK'):
+ options.selection('recreate', options.paired_options('recreate')[0], mapped)
+
+ def test_exact_object_fault_targets_current_ack_before_remote_mutation(self):
+ mapped = self.mapped()
+ mapped.pop((self.bucket, options.KEYS[-1], 'marker'))
+ mapped[(self.bucket, options.KEYS[3], 'other')] = {
+ 'poolIndex': 0, 'setIndex': 0, 'deleteMarker': False, 'isLatest': True}
+ # A historical generation appears first, as it does in the real PUT ledger.
+ current = (self.bucket, options.KEYS[0], 'null')
+ historical = (self.bucket, options.KEYS[0], 'historic')
+ mapped = {historical: mapped[historical], **{k: v for k, v in mapped.items() if k != historical}}
+ for number, (identity, target) in enumerate(mapped.items()):
+ target.update(bucket=identity[0], key=identity[1], versionId=identity[2], node='vm000', disk=0,
+ relativePath='generation-' + str(number), sha256='sha-' + identity[2], size=128,
+ cohort=[{'role': 'data'}, {'role': 'parity'}])
+ snapshots = types.SimpleNamespace(stop=lambda: None, snapshot=lambda *a, **k: {}, faults=[],
+ record=lambda label, value: value)
+ snapshots.remote = unittest.mock.Mock(side_effect=RuntimeError('remote mutation boundary'))
+ with patch.object(options, 'seed_fixture', return_value=[]), patch.object(options, 'verify_seed'), \
+ patch.object(options, 'map_fixture', return_value=mapped), patch.object(options, 'verify_parts'):
+ probe = types.SimpleNamespace(inventory=lambda *a, **k: {})
+ with self.assertRaisesRegex(RuntimeError, 'remote mutation boundary'):
+ options.scope_probe(snapshots, probe, {}, None, self.bucket, 'H1', 'DV', 'recreate',
+ options.paired_options('recreate')[0], [])
+ self.assertEqual(snapshots.remote.call_args.kwargs['relativePath'], mapped[current]['relativePath'])
+ self.assertEqual(snapshots.remote.call_args.kwargs['expectedSha256'], mapped[current]['sha256'])
+
+ def test_complete_oracle_rejects_lost_versions_wrong_scopes_markers_and_false_counters(self):
+ for kind in ('version', 'missing', 'duplicate', 'unknown', 'scope', 'null-scope', 'bool-scope',
+ 'incarnation', 'marker', 'counter', 'overflow', 'coverage', 'settings', 'truncated'):
+ with self.subTest(kind=kind):
+ status, mapped, selected, repaired, request = self.status()
+ receipt = status['outcome']['objects'][0]
+ if kind == 'version': receipt['identity']['versionId'] = 'unacknowledged'
+ elif kind == 'missing': status['outcome']['objects'].pop()
+ elif kind == 'duplicate': status['outcome']['objects'][-1] = copy.deepcopy(receipt)
+ elif kind == 'unknown': receipt['disposition']['state'] = 'unknown'
+ elif kind == 'scope': receipt['identity']['setIndex'] = 1
+ elif kind == 'null-scope': receipt['identity']['poolIndex'] = None
+ elif kind == 'bool-scope': receipt['identity']['setIndex'] = False
+ elif kind == 'incarnation': receipt['identity']['bucketIncarnationId'] = ''
+ elif kind == 'marker':
+ marker = next(row for row in status['outcome']['objects'] if row['identity']['versionId'] == 'marker')
+ marker['disposition']['state'] = 'verified_healthy'
+ elif kind == 'counter': status['outcome']['counters']['healed'] += 1
+ elif kind == 'overflow': status['outcome']['counters']['overflowed'] = True
+ elif kind == 'coverage': status['outcome']['coverage'] = 'partial'
+ elif kind == 'settings': status['settings'] = {**request, 'updateParity': True}
+ else: status['truncated'] = True
+ with self.assertRaises(ValueError): options.validate_outcome(status, mapped, selected, repaired, request)
+
+ def test_source_cleanup_receipts_keep_exact_versions_controls_and_counter_partition(self):
+ for state in ('repaired', 'authoritatively_absent'):
+ status, mapped, selected, repaired, request = self.status()
+ request = {**request, 'remove': True}; status['settings'] = request
+ target = next(iter(repaired))
+ receipt = next(r for r in status['outcome']['objects']
+ if r['identity']['versionId'] == target[2] and r['identity']['object'] == target[1])
+ receipt['disposition']['state'] = state
+ absent = {target} if state == 'authoritatively_absent' else set()
+ if absent:
+ status['outcome']['counters']['healed'] -= 1
+ status['outcome']['counters']['unchanged'] += 1
+ proof = options.validate_outcome(status, mapped, selected, repaired - absent, request, absent=absent)
+ self.assertEqual(proof['authoritativelyAbsentACKs'], len(absent))
+ for kind in ('missing-target', 'wrong-version', 'unknown', 'false-counter', 'marker', 'wrong-options'):
+ bad = copy.deepcopy(status)
+ if kind == 'missing-target': bad['outcome']['objects'].remove(bad['outcome']['objects'][status['outcome']['objects'].index(receipt)])
+ elif kind == 'wrong-version': bad['outcome']['objects'][status['outcome']['objects'].index(receipt)]['identity']['versionId'] = 'unacknowledged'
+ elif kind == 'unknown': bad['outcome']['objects'][status['outcome']['objects'].index(receipt)]['disposition']['state'] = 'unknown'
+ elif kind == 'false-counter': bad['outcome']['counters']['healed'] += 1
+ elif kind == 'marker':
+ next(r for r in bad['outcome']['objects'] if r['identity']['versionId'] == 'marker')['disposition']['state'] = 'authoritatively_absent'
+ else: bad['settings'] = {**request, 'remove': False}
+ with self.subTest(state=state, kind=kind), self.assertRaises(ValueError):
+ options.validate_outcome(bad, mapped, selected, repaired - absent, request, absent=absent)
+
+ def test_nullable_unrestricted_scope_keeps_the_independent_physical_binding(self):
+ status, mapped, selected, repaired, request = self.status('recursive-true')
+ before = copy.deepcopy(status)
+ options.validate_outcome(status, mapped, selected, repaired, request)
+ self.assertEqual(before, status)
+ receipt = status['outcome']['objects'][0]['identity']
+ identity = receipt['bucket'], receipt['object'], receipt['versionId']
+ receipt.update(poolIndex=0, setIndex=1 - mapped[identity]['setIndex'])
+ with self.assertRaises(ValueError): options.validate_outcome(status, mapped, selected, repaired, request)
+
+ def test_exact_missing_prefix_retains_the_requested_failure_instead_of_ack_objects(self):
+ baseline, request = options.paired_options('recursive-false')
+ detail = 'Heal task execution failed: Object not found: ' + self.bucket + '/scope/'
+ status = {'summary': 'stopped', 'settings': request, 'detail': detail,
+ 'outcome': {'coverage': 'complete', 'objectsTruncated': False, 'execution': {'state': 'completed_with_errors'},
+ 'objects': [{'identity': {'kind': 'object', 'bucket': self.bucket, 'object': 'scope/',
+ 'versionId': None, 'bucketIncarnationId': None, 'poolIndex': None, 'setIndex': None},
+ 'disposition': {'state': 'failed', 'details': 'permanent'}, 'detail': detail}],
+ 'counters': {'processed': 1, 'healed': 0, 'unchanged': 0, 'skipped': 0,
+ 'failed': 1, 'unknown': 0, 'attemptFailures': 1, 'overflowed': False}}}
+ proof = options.validate_outcome(status, self.mapped(), set(), set(), request)
+ self.assertEqual(proof['selectedACKs'], 0)
+ self.assertFalse(proof['repairPassClaimed'])
+ for kind in ('success', 'legacy-success', 'repair', 'unknown', 'receipt', 'missing-receipt', 'detail',
+ 'wrong-key', 'incarnation', 'selector', 'attempts', 'false-absence', 'missing-identity'):
+ changed = copy.deepcopy(status)
+ if kind == 'success': changed['outcome']['execution']['state'] = 'completed'
+ elif kind == 'legacy-success': changed['summary'] = 'finished'
+ elif kind == 'repair': changed['outcome']['counters']['healed'] = 1
+ elif kind == 'unknown': changed['outcome']['counters']['unknown'] = 1
+ elif kind == 'receipt': changed['outcome']['objects'] = [{'disposition': {'state': 'unknown'}}]
+ elif kind == 'missing-receipt': changed['outcome']['objects'] = []
+ elif kind == 'detail': changed['detail'] = ''
+ elif kind == 'wrong-key': changed['outcome']['objects'][0]['identity']['object'] = options.KEYS[0]
+ elif kind == 'incarnation': changed['outcome']['objects'][0]['identity']['bucketIncarnationId'] = 'invented'
+ elif kind == 'selector': changed['outcome']['objects'][0]['identity']['poolIndex'] = False
+ elif kind == 'attempts': changed['outcome']['counters']['attemptFailures'] = 0
+ elif kind == 'false-absence': changed['outcome']['objects'][0]['disposition']['state'] = 'authoritatively_absent'
+ else: changed['outcome']['objects'][0]['identity'].pop('bucketIncarnationId')
+ with self.assertRaises(ValueError): options.validate_outcome(changed, self.mapped(), set(), set(), request)
+
+ def physical_scene(self):
+ fixture = scope_fixtures.ScopeTests()
+ original, mapped = fixture.snapshot(), fixture.mapping()
+ targets = []
+ after = copy.deepcopy(original)
+ for number, target in enumerate(list(mapped.values())[:2]):
+ target = {**target, 'faultKind': 'missing' if number == 0 else 'bitflip',
+ 'faultBytes': None if number == 0 else {'sha256': 'flipped', 'size': target['size']}}
+ targets.append(target)
+ entries = after[target['node']]['files']
+ entry = next(row for row in entries if (row['disk'], row['path']) == (target['disk'], target['relativePath']))
+ if number == 0: entries.remove(entry)
+ else: entry['sha256'] = 'flipped'
+ return original, after, targets
+
+ def test_rejected_request_keeps_missing_and_corrupted_controls_exact(self):
+ original, after, targets = self.physical_scene()
+ proof = options.physical_selection(original, after, targets, set())
+ self.assertEqual(proof['selectedPhysicalRepairs'], 0)
+ self.assertEqual(proof['unselectedFaultsPreserved'], 2)
+ self.assertTrue(proof['beforeAnyTargetGETOrBackupRescue'])
+
+ def test_unselected_control_repair_is_not_hidden_by_selected_repair_or_backup_rescue(self):
+ for control in (0, 1):
+ original, after, targets = self.physical_scene()
+ target = targets[control]
+ entries = after[target['node']]['files']
+ entries[:] = [row for row in entries if (row['disk'], row['path']) != (target['disk'], target['relativePath'])]
+ entry = next(row for row in original[target['node']]['files']
+ if (row['disk'], row['path']) == (target['disk'], target['relativePath']))
+ entries.append(copy.deepcopy(entry))
+ with self.assertRaisesRegex(ValueError, 'unselected'): options.physical_selection(original, after, targets, set())
+
+ def test_selected_physical_repair_leaves_corrupted_unselected_control_unchanged(self):
+ original, after, targets = self.physical_scene()
+ target = targets[0]
+ entry = next(row for row in original[target['node']]['files']
+ if (row['disk'], row['path']) == (target['disk'], target['relativePath']))
+ after[target['node']]['files'].append(copy.deepcopy(entry))
+ selected = {(target['bucket'], target['key'], target['versionId'])}
+ proof = options.physical_selection(original, after, targets, selected)
+ self.assertEqual(proof['selectedPhysicalRepairs'], 1)
+ self.assertEqual(proof['unselectedFaultsPreserved'], 1)
+
+ def test_wrong_or_absent_put_identity_remains_pending(self):
+ for response in ({'VersionId': 'not-a-version', 'ETag': 'etag'}, {'VersionId': 'null'},
+ {'VersionId': '00000000-0000-0000-0000-000000000000', 'ETag': 'etag'}):
+ with tempfile.TemporaryDirectory() as name, patch.object(options.Recorder, 'call', return_value=response):
+ recorder = options.FixtureRecorder(types.SimpleNamespace(), Path(name) / 'ACKs')
+ with self.assertRaises(ValueError): recorder.call('put_object', {'Bucket': self.bucket, 'Key': 'owned.bin'}, b'body')
+ self.assertIsNotNone(recorder.pending)
+
+ def test_transport_or_receipt_failure_retains_original_request(self):
+ with tempfile.TemporaryDirectory() as name, patch.object(options.Recorder, 'call', side_effect=RuntimeError('lost ACK')):
+ recorder = options.FixtureRecorder(types.SimpleNamespace(), Path(name) / 'ACKs')
+ with self.assertRaises(RuntimeError): recorder.call('delete_object', {'Bucket': self.bucket, 'Key': 'owned.bin'})
+ self.assertEqual(recorder.pending['operation'], 'delete_object')
+
+ def test_wrong_exact_delete_or_marker_ack_never_clears_mutation_guard(self):
+ version = '11111111-1111-4111-8111-111111111111'
+ for params, response in (({'VersionId': version}, {'VersionId': '22222222-2222-4222-8222-222222222222'}),
+ ({}, {'VersionId': version, 'DeleteMarker': False}),
+ ({}, {'VersionId': 'null', 'DeleteMarker': True})):
+ with tempfile.TemporaryDirectory() as name, patch.object(options.Recorder, 'call', return_value=response):
+ recorder = options.FixtureRecorder(types.SimpleNamespace(), Path(name) / 'ACKs', fixture='DV')
+ with self.assertRaises(ValueError): recorder.call('delete_object', {'Bucket': self.bucket, 'Key': 'owned.bin', **params})
+ self.assertIsNotNone(recorder.pending)
+
+ def test_duplicate_put_ack_is_retained_even_with_successful_http(self):
+ response = {'VersionId': '11111111-1111-4111-8111-111111111111', 'ETag': 'etag'}
+ with tempfile.TemporaryDirectory() as name, patch.object(options.Recorder, 'call', return_value=response):
+ recorder = options.FixtureRecorder(types.SimpleNamespace(), Path(name) / 'ACKs', fixture='DV')
+ params = {'Bucket': self.bucket, 'Key': 'owned.bin'}
+ recorder.call('put_object', params, b'one')
+ self.assertIsNone(recorder.pending)
+ with self.assertRaises(ValueError): recorder.call('put_object', params, b'two')
+ self.assertIsNotNone(recorder.pending)
+
+ def test_positive_http_with_wrong_dv_mode_is_not_a_resolved_fixture_ack(self):
+ fake = types.SimpleNamespace(pending=None)
+ fake.call = lambda operation, params, body=None: {'VersionId': 'null', 'ETag': 'etag'}
+ with self.assertRaisesRegex(ValueError, 'DB/DV'): options.seed_fixture(fake, self.bucket, 'DV')
+ self.assertIsNotNone(fake.pending)
+
+ def test_no_single_branch_checkpoint_or_duplicate_can_certify_canonical_completion(self):
+ rows = [{'topology': topology, 'branch': branch, 'fixture': fixture, 'phase': phase,
+ 'verdict': 'PASS', 'cleanup': 'PASS', 'fullProbeCompleted': True}
+ for topology in ('H1', 'H2') for branch in options.BRANCHES for fixture in options.FIXTURES
+ for phase in options.source_phases(branch)]
+ self.assertEqual(len(rows), 68)
+ self.assertEqual({r['branch'] for r in rows}, set(options.BRANCHES))
+ for topology in ('H1', 'H2'):
+ for fixture in options.FIXTURES:
+ self.assertEqual({r['branch'] for r in rows if r['topology'] == topology
+ and r['fixture'] == fixture and r['phase'] == 'selected'}, set(options.BRANCHES))
+ with self.assertRaises(ValueError):
+ options.complete_results([*rows, {**rows[0], 'branch': 'remove-true', 'phase': 'baseline'}])
+ self.assertTrue(options.complete_results(rows))
+ self.assertFalse(options.complete_results(rows[:-1]))
+ for name, value in (('verdict', 'FAIL'), ('cleanup', 'FAIL'), ('fullProbeCompleted', False)):
+ changed = copy.deepcopy(rows); changed[-1][name] = value
+ self.assertFalse(options.complete_results(changed))
+ with self.assertRaises(ValueError): options.complete_results([*rows, rows[0]])
+
+ def test_rejected_successful_admission_is_retained_for_original_token_cancellation(self):
+ probe = types.SimpleNamespace(admin=lambda *args: {'httpStatus': 200, 'response': {'clientToken': 'wrong-admission'}})
+ fleet = types.SimpleNamespace(record=lambda label, value: value)
+ active = []
+ with self.assertRaises(ValueError): options.run_request(probe, fleet, '/rustfs/admin/v3/heal/',
+ options.paired_options('read-repair')[1], active, rejected=True)
+ self.assertEqual(active, [('/rustfs/admin/v3/heal/', 'wrong-admission')])
+
+ def test_a10_orphan_and_scope_node_programs_compile_with_the_actual_adapter(self):
+ def invoke(argv, **params):
+ compile(params['input'], 'remote-option-node', 'exec')
+ return types.SimpleNamespace(returncode=0, stdout='{"checked":true}', stderr='')
+ for profile in ('scope-isolation', 'orphan-preserve'):
+ with tempfile.TemporaryDirectory() as name, patch('scripts.validation_heal.healthy.subprocess.run', side_effect=invoke):
+ fleet = Fleet(['n0', 'n1', 'n2', 'n3'], Path(name), 'gh-1234-1-h2', runtime_profile=profile)
+ self.assertTrue(fleet.remote('n0', 'process-metrics')['checked'])
+
+
+if __name__ == '__main__': unittest.main()
diff --git a/test_validation_heal_orphan.py b/test_validation_heal_orphan.py
new file mode 100644
index 00000000..b484c698
--- /dev/null
+++ b/test_validation_heal_orphan.py
@@ -0,0 +1,459 @@
+"""C10 controlled-codec build identity and preservation guards."""
+
+import copy
+import json
+import os
+import shutil
+import subprocess
+import sys
+import types
+import uuid
+from unittest.mock import patch
+from pathlib import Path
+import tempfile
+import unittest
+
+from scripts.validation_heal import shared_fixture as shared
+from scripts.validation_heal import orphan, orphan_fault as backend, generation_fault as generation, heal_fault as fault
+from scripts.validation_heal.matrix import select, AUTOMATIC_CASES
+from scripts.validation_heal.healthy import BASE_OPTIONS, Fleet
+
+
+class SharedFixtureArtifactTests(unittest.TestCase):
+ def fixture(self, root):
+ binary = root / 'heal-shared-fixture'
+ binary.write_bytes(b'\x7fELF\x02\x01' + b'\0' * 12 + b'\x3e\x00' + b'fixed-codec')
+ lock = root / 'shared-Cargo.lock'; lock.write_text('frozen helper graph\n')
+ (root / 'shared-fixture-tests.log').write_text('running 3 tests\n' + ''.join(
+ 'test ' + name + ' ... ok\n' for name in shared.TESTS) + 'test result: ok. 3 passed; 0 failed; 0 ignored;\n')
+ run = {'id': 123, 'run_attempt': 1, 'status': 'completed', 'conclusion': 'success',
+ 'path': '.github/workflows/rustfs-validation-tools.yml'}
+ (root / 'tool-run.json').write_text(json.dumps(run))
+ manifest = {'schemaVersion': 1, 'built': True, 'sourceUnmodified': True, 'candidateCommit': shared.CANDIDATE,
+ 'candidateSourceTree': shared.SOURCE_TREE, 'candidateLockSha256': shared.CARGO_LOCK,
+ 'target': 'x86_64-unknown-linux-gnu', 'role': shared.ROLE, 'filesystemMutationPerformed': False,
+ 'physicalByteEqualityClaimed': False, 'runId': '123', 'runAttempt': '1',
+ 'helperSourceSha256': shared.sha(Path(shared.__file__).with_name('tools') / 'heal_shared_version_fixture.rs'),
+ 'binarySha256': shared.sha(binary), 'sharedLockSha256': shared.sha(lock)}
+ (root / 'heal-shared-fixture.json').write_text(json.dumps(manifest))
+ return manifest, run
+
+ def test_verified_codec_is_distinct_from_read_only_inspector_and_live_proof(self):
+ with tempfile.TemporaryDirectory() as folder:
+ root = Path(folder); self.fixture(root)
+ proof = shared.verify(root, {'source_sha': shared.CANDIDATE})
+ self.assertTrue(proof['exactCodecTestsPassed'])
+ self.assertFalse(proof['filesystemMutationPerformed'])
+ self.assertFalse(proof['physicalByteEqualityClaimed'])
+ self.assertFalse(proof['fleetPassClaimed'])
+
+ def test_foreign_source_artifact_role_commit_and_build_attempt_cannot_be_installed(self):
+ for field, value in (('role', 'read-only decoder; not the RustFS service under test'), ('candidateSourceTree', '0' * 40),
+ ('candidateLockSha256', '0' * 64), ('helperSourceSha256', '0' * 64), ('built', False),
+ ('physicalByteEqualityClaimed', True), ('runAttempt', '2')):
+ with self.subTest(field=field), tempfile.TemporaryDirectory() as folder:
+ root = Path(folder); manifest, _ = self.fixture(root)
+ manifest[field] = value
+ (root / 'heal-shared-fixture.json').write_text(json.dumps(manifest))
+ with self.assertRaises(ValueError): shared.verify(root, {'source_sha': shared.CANDIDATE})
+
+
+ def test_zero_filtered_ignored_or_incomplete_codec_tests_do_not_certify_a_build(self):
+ for log in ('running 0 tests\ntest result: ok. 0 passed; 0 failed; 0 ignored;\n',
+ 'running 3 tests\ntest result: ok. 2 passed; 0 failed; 1 ignored;\n',
+ 'running 3 tests\ntest result: ok. 3 passed; 0 failed; 0 ignored;\n'):
+ with self.subTest(log=log), tempfile.TemporaryDirectory() as folder:
+ root = Path(folder); self.fixture(root)
+ (root / 'shared-fixture-tests.log').write_text(log)
+ with self.assertRaises(ValueError): shared.verify(root, {'source_sha': shared.CANDIDATE})
+
+ def test_changed_executable_or_failed_remote_build_is_refused(self):
+ for kind in ('binary', 'run', 'symlink'):
+ with self.subTest(kind=kind), tempfile.TemporaryDirectory() as folder:
+ root = Path(folder); _, run = self.fixture(root)
+ binary = root / 'heal-shared-fixture'
+ if kind == 'binary': binary.write_bytes(b'foreign')
+ elif kind == 'run':
+ run['conclusion'] = 'failure'; (root / 'tool-run.json').write_text(json.dumps(run))
+ else:
+ binary.rename(root / 'outside'); binary.symlink_to(root / 'outside')
+ with self.assertRaises(ValueError): shared.verify(root, {'source_sha': shared.CANDIDATE})
+
+
+
+
+class OrphanDescriptorTests(unittest.TestCase):
+ bucket = 'rv-heal-0123456789abcdef'
+ old_dir, live_dir = str(uuid.UUID(int=11)), str(uuid.UUID(int=12))
+
+ def setUp(self):
+ self.temporary = tempfile.TemporaryDirectory()
+ self.root = Path(self.temporary.name).resolve()
+ self.object = self.root / 'data' / self.bucket / orphan.KEY
+ self.object.mkdir(parents=True)
+ (self.root / 'quarantine').mkdir()
+ self.owner = {'runId': 'gh-123-1-h1', 'bucket': self.bucket, 'runtimeProfile': 'orphan-preserve', 'binarySha256': 'a' * 64}
+ self.request = {'disk': 0, 'bucket': self.bucket, 'key': orphan.KEY}
+ self.mount = patch.object(fault, 'mount_id', return_value=7); self.mount.start()
+ self.seed(self.old_dir, b'old')
+
+ def tearDown(self):
+ self.mount.stop(); self.temporary.cleanup()
+
+ def seed(self, directory, raw):
+ (self.object / 'xl.meta').write_bytes(b'XL2 ' + raw)
+ (self.object / directory).mkdir(exist_ok=True)
+ (self.object / directory / 'part.1').write_bytes(raw * 256)
+ (self.object / directory / ('part.1.integrity.' + str(uuid.UUID(int=77)))).write_bytes(raw + b'-commitment')
+
+ def invoke(self, name, **kwargs):
+ with fault.directory(self.root) as root:
+ return getattr(backend, name)(root, self.owner, {**self.request, **kwargs}, fault, generation)
+
+ def capture(self, label):
+ with fault.directory(self.root) as root:
+ present, _, files = backend.scene(root, self.owner, self.request, fault, generation)
+ return self.invoke('capture', generation=label, expectedFiles=files, expectedPresent=present)
+
+ def authority(self, absent=False):
+ self.capture('old')
+ shutil.rmtree(self.object)
+ if not absent:
+ self.object.mkdir(); self.seed(self.live_dir, b'live')
+ saved = self.capture('authority')
+ self.invoke('arm', generation='authority')
+ return saved
+
+ def recover(self, label='authority'):
+ actual = os.fstat
+ def root_owned(fd):
+ info = actual(fd)
+ fields = {name: getattr(info, name) for name in dir(info) if name.startswith('st_')}
+ return types.SimpleNamespace(**{**fields, 'st_uid': 0})
+ with patch.object(backend.os, 'fstat', side_effect=root_owned):
+ return self.invoke('recover', generation=label)
+
+ def test_post_delete_recovery_never_reinstates_old_version_and_preserves_sidecars(self):
+ saved = self.authority()
+ self.invoke('install', mode='object', dataDir=self.old_dir)
+ self.assertEqual((self.object / 'xl.meta').read_bytes(), b'XL2 old')
+ receipt = self.recover()
+ self.assertTrue(receipt['lastACKAuthorityRestored'])
+ self.assertEqual(receipt['originalTreeSha256'], saved['treeSha256'])
+ self.assertEqual((self.object / 'xl.meta').read_bytes(), b'XL2 live')
+ self.assertFalse((self.object / self.old_dir).exists())
+ self.assertTrue((self.root / 'quarantine/C10-authority-failed-current' / self.old_dir).is_dir())
+ self.assertEqual(receipt, self.recover())
+
+ def test_absent_post_delete_authority_remains_absent_after_metadata_less_replay(self):
+ self.authority(absent=True)
+ self.invoke('install', mode='data', dataDir=self.old_dir)
+ self.assertFalse((self.object / 'xl.meta').exists())
+ self.assertTrue((self.object / self.old_dir).exists())
+ self.recover()
+ self.assertFalse(self.object.exists())
+ self.assertTrue((self.root / 'quarantine/C10-authority-failed-current' / self.old_dir).is_dir())
+
+ def test_live_reference_and_commitment_remain_exact_during_data_only_replay(self):
+ self.authority()
+ original = (self.object / self.live_dir / 'part.1').read_bytes()
+ self.invoke('install', mode='data', dataDir=self.old_dir)
+ self.assertEqual((self.object / 'xl.meta').read_bytes(), b'XL2 live')
+ self.assertEqual((self.object / self.live_dir / 'part.1').read_bytes(), original)
+ self.assertEqual((self.object / self.old_dir / ('part.1.integrity.' + str(uuid.UUID(int=77)))).read_bytes(), b'old-commitment')
+
+ def test_old_generation_is_never_a_recovery_authority_and_foreign_identity_is_refused(self):
+ self.authority()
+ with self.assertRaises(ValueError): self.invoke('arm', generation='old')
+ for field, value in (('key', '../escape'), ('bucket', 'foreign'), ('disk', 4)):
+ with self.subTest(field=field), self.assertRaises(ValueError): self.invoke('capture', **{field: value},
+ generation='authority', expectedFiles=[], expectedPresent=False)
+ with patch.object(fault, 'mount_id', return_value=8), self.assertRaises(ValueError): self.recover()
+ self.owner['binarySha256'] = 'b' * 64
+ with self.assertRaises(ValueError): self.recover()
+
+ def test_partial_restore_is_retained_and_never_certified_clear(self):
+ self.authority()
+ self.invoke('install', mode='object', dataDir=self.old_dir)
+ self.object.rename(self.root / 'quarantine/C10-authority-failed-current')
+ self.object.mkdir(); (self.object / 'xl.meta').write_bytes(b'partial')
+ with self.assertRaises(ValueError): self.recover()
+ self.assertFalse((self.root / 'quarantine/C10-authority-recovered.json').exists())
+ self.assertTrue((self.root / 'quarantine/C10-authority-failed-current' / self.old_dir).exists())
+
+ def test_sparse_mode_does_not_relax_the_original_c07_generation_contract(self):
+ shutil.rmtree(self.object / self.old_dir)
+ with fault.directory(self.object) as fd:
+ with self.assertRaises(ValueError): generation.tree(fd, fault)
+ self.assertEqual(set(generation.tree(fd, fault, allow_sparse=True)[0]), {'xl.meta'})
+ (self.object / 'xl.meta').unlink()
+ with fault.directory(self.object) as fd:
+ with self.assertRaises(ValueError): generation.tree(fd, fault)
+ self.assertEqual(generation.tree(fd, fault, allow_sparse=True), ({}, []))
+
+ def test_symlinks_hardlinks_and_payload_less_commitments_are_not_archived(self):
+ part = self.object / self.old_dir / 'part.1'
+ for kind in ('symlink', 'hardlink', 'missing-part'):
+ raw = part.read_bytes()
+ if kind == 'symlink':
+ part.unlink(); part.symlink_to(self.root / 'outside')
+ elif kind == 'hardlink': os.link(part, self.root / 'outside')
+ else: part.unlink()
+ with self.subTest(kind=kind), self.assertRaises((ValueError, OSError)): self.capture('seed')
+ if kind == 'hardlink': (self.root / 'outside').unlink()
+ else:
+ if part.is_symlink(): part.unlink()
+ part.write_bytes(raw)
+
+ def test_encoder_refuses_nonidentical_physical_parts_before_it_can_execute(self):
+ self.seed(self.live_dir, b'different')
+ self.capture('seed'); self.invoke('arm', generation='seed')
+ with fault.directory(self.root) as root, patch.object(backend.subprocess, 'run') as execute, self.assertRaisesRegex(ValueError, 'identical physical'):
+ backend.convert(root, self.owner, {**self.request, 'sourceDataDir': self.old_dir, 'destinationDataDir': self.live_dir,
+ 'sourceVersion': str(uuid.UUID(int=1)), 'destinationVersion': str(uuid.UUID(int=2))}, fault, generation, self.root / 'unavailable')
+ execute.assert_not_called()
+
+ def test_retired_shared_seed_cannot_be_recovered_after_later_ack_is_allowed(self):
+ self.capture('seed'); self.invoke('arm', generation='seed')
+ with fault.directory(self.root) as root:
+ manifest = backend.scene(root, self.owner, self.request, fault, generation)[2]
+ self.invoke('disarm', generation='seed', expectedFiles=manifest)
+ with self.assertRaisesRegex(ValueError, 'retired'): self.recover('seed')
+
+ def test_codec_conversion_preserves_retired_bytes_and_seed_rescue_can_restore_both_ack_generations(self):
+ self.seed(self.live_dir, b'old')
+ self.capture('seed'); self.invoke('arm', generation='seed')
+ source, destination = str(uuid.UUID(int=1)), str(uuid.UUID(int=2))
+ receipt = {'sourceVersion': source, 'destinationVersion': destination, 'sharedDataDir': self.old_dir,
+ 'retiredDataDir': self.live_dir, 'filesystemMutationPerformed': False, 'physicalByteEqualityClaimed': False}
+ result = subprocess.CompletedProcess([], 0, b'controlled-encoded-meta', json.dumps(receipt).encode())
+ with fault.directory(self.root) as root, patch.object(backend.subprocess, 'run', return_value=result):
+ proof = backend.convert(root, self.owner, {**self.request, 'sourceDataDir': self.old_dir, 'destinationDataDir': self.live_dir,
+ 'sourceVersion': source, 'destinationVersion': destination}, fault, generation, self.root / 'verified-codec')
+ self.assertTrue(proof['physicalByteEqualityVerified'])
+ self.assertEqual((self.object / 'xl.meta').read_bytes(), b'controlled-encoded-meta')
+ self.assertFalse((self.object / self.live_dir).exists())
+ self.assertEqual((self.root / 'quarantine/C10-retired-shared-data/part.1').read_bytes(), b'old' * 256)
+ self.recover('seed')
+ self.assertEqual((self.object / 'xl.meta').read_bytes(), b'XL2 old')
+ self.assertEqual((self.object / self.live_dir / 'part.1').read_bytes(), b'old' * 256)
+
+ def test_mpu_census_is_complete_read_only_and_refuses_aliases(self):
+ folder = self.root / 'data/.rustfs.sys/multipart/hash/upload'; folder.mkdir(parents=True)
+ (folder / 'part.1').write_bytes(b'pending-part')
+ (folder / 'xl.meta').write_bytes(b'pending-metadata')
+ with fault.directory(self.root) as root:
+ result = backend.multipart(root, fault)
+ self.assertTrue(result['readOnly']); self.assertEqual(len(result['files']), 2)
+ self.assertEqual(result, backend.multipart(root, fault))
+ (folder / 'alias').symlink_to(folder / 'part.1')
+ with self.assertRaises(OSError): backend.multipart(root, fault)
+
+
+class OrphanOracleTests(unittest.TestCase):
+ bucket = 'rv-heal-0123456789abcdef'
+ def status(self, branch):
+ status = {'summary': 'finished', 'truncated': False, 'settings': {**BASE_OPTIONS, 'scanMode': 2,
+ 'recursive': branch != 'remove-false', 'remove': branch != 'remove-false'}, 'outcome': {
+ 'execution': {'state': 'completed'}, 'coverage': 'complete', 'objectsTruncated': False,
+ 'counters': {'processed': 1, 'healed': 0, 'failed': 0, 'unknown': 0}, 'objects': []}}
+ if branch == 'remove-false':
+ status['summary'] = 'stopped'
+ status['detail'] = 'Object not found: ' + self.bucket + '/' + orphan.KEY
+ status['outcome']['counters'] = {'processed': 1, 'healed': 0, 'unchanged': 0, 'skipped': 0,
+ 'failed': 1, 'unknown': 0, 'attemptFailures': 1, 'overflowed': False}
+ status['outcome']['execution']['state'] = 'completed_with_errors'
+ status['outcome']['objects'] = [{'identity': {'kind': 'object', 'bucket': self.bucket, 'object': orphan.KEY,
+ 'versionId': None, 'bucketIncarnationId': None, 'poolIndex': status['settings']['pool'],
+ 'setIndex': status['settings']['set']}, 'disposition': {'state': 'failed', 'details': 'permanent'},
+ 'detail': status['detail']}]
+ if branch == 'remove-true-before-grace':
+ status['outcome']['objects'] = [{'identity': {'bucket': self.bucket, 'object': orphan.KEY},
+ 'disposition': {'state': 'deferred', 'details': {'reason': 'dangling_delete_grace'}}}]
+ return status
+
+ def test_all_exact_terminal_branches_and_typed_grace_are_required(self):
+ for branch in orphan.BRANCHES:
+ status = self.status(branch)
+ orphan.terminal_safety(status, branch, self.bucket, status['settings'])
+ for mutation in ('reason', 'key', 'partial', 'unknown', 'truncated', 'settings'):
+ status = self.status('remove-true-before-grace'); options = copy.deepcopy(status['settings'])
+ if mutation == 'reason': status['outcome']['objects'][0]['disposition']['details']['reason'] = 'retry'
+ elif mutation == 'key': status['outcome']['objects'][0]['identity']['object'] = 'other'
+ elif mutation == 'partial': status['outcome']['coverage'] = 'partial'
+ elif mutation == 'unknown': status['outcome']['counters']['unknown'] = 1
+ elif mutation == 'truncated': status['outcome']['objectsTruncated'] = True
+ else: status['settings']['remove'] = False
+ with self.subTest(mutation=mutation), self.assertRaises(ValueError): orphan.terminal_safety(status, 'remove-true-before-grace', self.bucket, options)
+
+ def test_source_case_has_all_five_branches_on_both_required_topologies(self):
+ source = select(orphan.CASE_ID)[0]
+ self.assertEqual(source['variants'], list(orphan.BRANCHES))
+ self.assertEqual(source['contract']['probes']['topology'], 'H1,H2')
+ self.assertIn(orphan.CASE_ID, AUTOMATIC_CASES)
+
+ def test_protected_orphan_refusal_requires_original_failed_identity_and_truthful_terminal(self):
+ status = self.status('remove-false')
+ proof = orphan.terminal_safety(status, 'remove-false', self.bucket, status['settings'])
+ self.assertFalse(proof['repairPassClaimed'])
+ for mutation in ('finished', 'running', 'cancelled', 'partial', 'unknown', 'repaired', 'missing', 'wrong-version',
+ 'wrong-key', 'wrong-selector', 'hidden-attempt', 'duplicate'):
+ changed = copy.deepcopy(status)
+ outcome = changed['outcome']
+ if mutation in ('finished', 'running'): changed['summary'] = mutation
+ elif mutation == 'cancelled': outcome['execution'] = {'state': 'aborted', 'reason': 'cancelled'}
+ elif mutation == 'partial': outcome['coverage'] = 'partial'
+ elif mutation == 'unknown': outcome['counters']['unknown'] = 1
+ elif mutation == 'repaired': outcome['objects'][0]['disposition']['state'] = 'repaired'
+ elif mutation == 'missing': outcome['objects'] = []
+ elif mutation == 'wrong-version': outcome['objects'][0]['identity']['versionId'] = str(uuid.UUID(int=9))
+ elif mutation == 'wrong-key': outcome['objects'][0]['identity']['object'] = 'control.bin'
+ elif mutation == 'wrong-selector': outcome['objects'][0]['identity']['setIndex'] = 1
+ elif mutation == 'hidden-attempt': outcome['counters']['attemptFailures'] = 0
+ else: outcome['objects'].append(copy.deepcopy(outcome['objects'][0]))
+ with self.subTest(mutation=mutation), self.assertRaises(ValueError):
+ orphan.terminal_safety(changed, 'remove-false', self.bucket, status['settings'])
+
+ def test_shared_paths_are_streamed_through_both_exact_ack_version_ids(self):
+ mapping = {}
+ for version in (str(uuid.UUID(int=1)), str(uuid.UUID(int=2))):
+ mapping[(self.bucket, orphan.KEY, version)] = {'bucket': self.bucket, 'key': orphan.KEY, 'versionId': version,
+ 'cohort': [{'node': 'node1', 'disk': disk, 'relativePath': 'shared/part.1'} for disk in range(4)]}
+ with patch.object(orphan, 'verify_parts', return_value={'exactStreamingParts': 4}) as verify:
+ proof = orphan.verify_live_parts(object(), mapping, 'exact-shared-ACK')
+ self.assertEqual(verify.call_count, 2)
+ self.assertEqual({next(iter(call.args[1]))[2] for call in verify.call_args_list}, {identity[2] for identity in mapping})
+ self.assertEqual(proof['versionAddressedStreamingParts'], 8)
+ self.assertEqual(proof['distinctPhysicalParts'], 4)
+ self.assertFalse(proof['sharedPathsCountedAsIndependentDisks'])
+ next(iter(mapping.values()))['versionId'] = 'foreign'
+ with self.assertRaises(ValueError): orphan.verify_live_parts(object(), mapping, 'foreign-ACK')
+
+ def scene(self, branch):
+ live, stale = str(uuid.UUID(int=12)), str(uuid.UUID(int=11))
+ version, doomed = str(uuid.UUID(int=1)), str(uuid.UUID(int=2))
+ expected = [] if branch == 'remove-false' else [{'key': orphan.KEY, 'versionId': version, 'deleteMarker': False}]
+ versions = [] if not expected else [{'versionId': version, 'deleted': False, 'dataDir': live, 'inline': None}]
+ if branch == 'shared-version-reference':
+ shared_version = str(uuid.UUID(int=3))
+ versions.append({'versionId': shared_version, 'deleted': False, 'dataDir': live, 'inline': None})
+ expected.append({'key': orphan.KEY, 'versionId': shared_version, 'deleteMarker': False})
+ authority = {'node1': {'formats': [{'disk': disk, 'sha256': str(disk)} for disk in range(4)], 'files': []}}
+ for disk in range(4):
+ authority['node1']['files'].append({'disk': disk, 'path': self.bucket + '/control.bin/xl.meta', 'sha256': 'control', 'size': 7})
+ if versions:
+ authority['node1']['files'] += [
+ {'disk': disk, 'path': self.bucket + '/' + orphan.KEY + '/xl.meta', 'sha256': 'authority', 'size': 7,
+ 'metadata': {'bucket': self.bucket, 'key': orphan.KEY, 'versions': copy.deepcopy(versions)}},
+ {'disk': disk, 'path': self.bucket + '/' + orphan.KEY + '/' + live + '/part.1', 'sha256': 'live-part', 'size': 256},
+ {'disk': disk, 'path': self.bucket + '/' + orphan.KEY + '/' + live + '/part.1.integrity.' + str(uuid.UUID(int=77)), 'sha256': 'live-commitment', 'size': 100}]
+ before = copy.deepcopy(authority)
+ if branch != 'pending-mpu':
+ before['node1']['files'].append({'disk': 0, 'path': self.bucket + '/' + orphan.KEY + '/' + stale + '/part.1',
+ 'sha256': 'stale-part', 'size': 256})
+ if branch.startswith('remove-true-'):
+ meta = next(row for row in before['node1']['files'] if row['disk'] == 0 and row['path'].endswith(orphan.KEY + '/xl.meta'))
+ meta['metadata']['versions'].append({'versionId': doomed, 'deleted': False, 'dataDir': stale, 'inline': None})
+ meta['sha256'] = 'old-authority'
+ after = copy.deepcopy(before if branch in {'remove-false', 'remove-true-before-grace', 'pending-mpu'} else authority)
+ return before, after, authority, expected, doomed, stale
+
+ def test_actual_cleanup_preservation_and_all_original_controls_are_required(self):
+ for branch in orphan.BRANCHES:
+ before, after, authority, expected, doomed, stale = self.scene(branch)
+ orphan.physical_safety(before, after, authority, self.bucket, expected, branch, doomed, stale, ('node1', 0))
+ for kind in ('control', 'format', 'member', 'sidecar', 'orphan'):
+ changed = copy.deepcopy(after)
+ if kind == 'control': changed['node1']['files'][0]['sha256'] = 'modified'
+ elif kind == 'format': changed['node1']['formats'][0]['sha256'] = 'modified'
+ elif kind == 'member': changed['node1']['formats'].pop()
+ elif kind == 'sidecar':
+ if branch == 'remove-false': continue
+ next(row for row in changed['node1']['files'] if '.integrity.' in row['path'])['sha256'] = 'modified'
+ else:
+ if branch == 'pending-mpu': continue
+ if branch in {'remove-true-after-grace', 'shared-version-reference'}:
+ changed['node1']['files'].append(copy.deepcopy(before['node1']['files'][-1]))
+ else: changed['node1']['files'] = [row for row in changed['node1']['files'] if '/' + stale + '/' not in row['path']]
+ with self.subTest(branch=branch, kind=kind), self.assertRaises(ValueError):
+ orphan.physical_safety(before, changed, authority, self.bucket, expected, branch, doomed, stale, ('node1', 0))
+
+ def test_partial_original_member_recovery_blocks_cleanup_and_retains_the_namespace(self):
+ with tempfile.TemporaryDirectory() as folder:
+ fleet = Fleet(['node1'], Path(folder), 'gh-123-1-h1', runtime_profile='orphan-preserve')
+ fleet.reserved = ['node1']
+ fleet.orphan_faults = [('node1', {'disk': 0, 'bucket': self.bucket, 'key': orphan.KEY, 'generation': 'authority'})]
+ calls = []
+ def remote(node, action, **params):
+ calls.append(action)
+ if action == 'orphan-recover': return {'state': 'DIRTY', 'lastACKAuthorityRestored': False}
+ return {}
+ fleet.remote = remote
+ self.assertFalse(fleet.cleanup())
+ self.assertNotIn('cleanup', calls)
+ self.assertEqual(len(fleet.orphan_faults), 1)
+
+ def test_unknown_request_or_invalid_version_ack_never_restores_old_authority_or_cleans(self):
+ for failure in ('transport', 'wrong-version'):
+ with self.subTest(failure=failure), tempfile.TemporaryDirectory() as folder:
+ root = Path(folder)
+ candidate = root / 'candidate.json'; candidate.write_text(json.dumps({'source_sha': orphan.CANDIDATE, 'package_sha256': 'a' * 64}))
+ rc = root / 'rc'; rc.write_bytes(b'rc')
+ args = types.SimpleNamespace(evidence=root / 'evidence', candidate=candidate, rc_bin=rc,
+ tool_directory=root, shared_directory=root, testing_commit='b' * 40, binary_sha='c' * 64)
+ with patch.dict(os.environ, {'GITHUB_RUN_ID': '123', 'GITHUB_RUN_ATTEMPT': '1'}), \
+ patch.object(orphan, 'verify_inspector', return_value={}), patch.object(orphan, 'verify_shared', return_value={}), \
+ patch.object(orphan, 'Fleet') as fleet, patch.object(orphan, 'Recorder') as recorder:
+ fleet.return_value.record.side_effect = lambda label, value: value
+ recorder.return_value.call.side_effect = [TimeoutError('unknown original request')] if failure == 'transport' else [{}, {}, {'VersionId': 'foreign'}]
+ result = orphan.run_branch(args, 'H1', ['node1'], 'remove-false')
+ self.assertEqual(result['verdict'], 'FAIL'); self.assertEqual(result['cleanup'], 'FAIL')
+ fleet.return_value.cleanup.assert_not_called()
+ fleet.return_value.stop.assert_called_once()
+ self.assertFalse(any(call.args[1] == 'orphan-recover' for call in fleet.return_value.remote.call_args_list))
+
+
+class OrphanLinuxTests(unittest.TestCase):
+ @unittest.skipUnless(sys.platform == 'linux', 'requires real Linux descriptors, root ownership and private bind mounts')
+ def test_actual_absent_authority_recovery_and_nested_mount_refusal(self):
+ program = '''import json,shutil,subprocess,tempfile,uuid
+from pathlib import Path
+from scripts.validation_heal import orphan_fault as backend,generation_fault as generation,heal_fault as fault
+with tempfile.TemporaryDirectory() as folder:
+ root=Path(folder).resolve();bucket='rv-heal-0123456789abcdef';obj=root/'data'/bucket/'orphan.bin'
+ obj.mkdir(parents=True);(root/'quarantine').mkdir();directory=str(uuid.UUID(int=11));(obj/directory).mkdir()
+ (obj/'xl.meta').write_bytes(b'old-meta');(obj/directory/'part.1').write_bytes(b'old-part')
+ (obj/directory/('part.1.integrity.'+str(uuid.UUID(int=77)))).write_bytes(b'old-commitment')
+ owner={'runId':'gh-123-1-h1','bucket':bucket,'runtimeProfile':'orphan-preserve','binarySha256':'a'*64}
+ request={'disk':0,'bucket':bucket,'key':'orphan.bin'}
+ with fault.directory(root) as fd:
+ present,_,files=backend.scene(fd,owner,request,fault,generation)
+ backend.capture(fd,owner,{**request,'generation':'old','expectedPresent':present,'expectedFiles':files},fault,generation)
+ shutil.rmtree(obj)
+ backend.capture(fd,owner,{**request,'generation':'authority','expectedPresent':False,'expectedFiles':[]},fault,generation)
+ backend.arm(fd,owner,{**request,'generation':'authority'},fault,generation)
+ backend.install(fd,owner,{**request,'mode':'data','dataDir':directory},fault,generation)
+ assert not (obj/'xl.meta').exists() and (obj/directory/'part.1').read_bytes()==b'old-part'
+ receipt=backend.recover(fd,owner,{**request,'generation':'authority'},fault,generation)
+ assert receipt['lastACKAuthorityRestored'] and not obj.exists()
+ assert backend.recover(fd,owner,{**request,'generation':'authority'},fault,generation)==receipt
+ assert (root/'quarantine/C10-authority-failed-current'/directory/('part.1.integrity.'+str(uuid.UUID(int=77)))).read_bytes()==b'old-commitment'
+ obj.mkdir();outside=root/'outside';outside.mkdir()
+ subprocess.run(['mount','--bind',str(outside),str(obj)],check=True)
+ try:
+ with fault.directory(root) as fd:
+ try: backend.scene(fd,owner,request,fault,generation)
+ except ValueError as error: assert 'mount' in str(error)
+ else: raise AssertionError('nested mount accepted')
+ finally: subprocess.run(['umount',str(obj)],check=True)
+print(json.dumps({'realPostDeleteAuthorityRestored':True,'nestedBindRefused':True}))
+'''
+ result = subprocess.run(['sudo', '-n', 'unshare', '-m', sys.executable, '-B', '-c', program], capture_output=True, text=True, timeout=60)
+ self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
+ self.assertTrue(json.loads(result.stdout)['realPostDeleteAuthorityRestored'])
+
+
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/test_validation_heal_outcome.py b/test_validation_heal_outcome.py
new file mode 100644
index 00000000..7a4ec00f
--- /dev/null
+++ b/test_validation_heal_outcome.py
@@ -0,0 +1,733 @@
+"""J02 rejects fabricated truth and checks faults before fixture recovery."""
+
+import copy
+import json
+import os
+from pathlib import Path
+import tempfile
+import types
+import unittest
+from unittest.mock import patch
+
+from scripts.validation_heal import outcome
+from scripts.validation_heal.healthy import BASE_OPTIONS, physical_content
+from scripts.validation_heal.shards import files
+from test_validation_heal import versioned_scene
+
+
+class MixedOutcomeTest(unittest.TestCase):
+ bucket = "rv-heal-0123456789abcdef"
+ incarnation = "12345678-1234-1234-1234-123456789abc"
+ options = {**BASE_OPTIONS, "recursive": True}
+
+ def mixed(self):
+ expected = [{"key": "objects/0000.bin", "versionId": "v0", "state": "verified_healthy"},
+ {"key": "objects/0001.bin", "versionId": "v1", "state": "repaired"},
+ {"key": "objects/0002.bin", "versionId": "v2", "state": "failed"}]
+ status = {"summary": "stopped", "settings": self.options, "truncated": False,
+ "objectsHealed": 77, "outcome": {"execution": {"state": "completed_with_errors"},
+ "coverage": "complete", "objectsTruncated": False,
+ "counters": {"processed": 3, "healed": 1, "unchanged": 1, "skipped": 0, "failed": 1,
+ "unknown": 0, "attemptFailures": 4, "overflowed": False},
+ "objects": [{"identity": {"kind": "object", "bucket": self.bucket, "object": entry["key"], "versionId": entry["versionId"],
+ "poolIndex": 0, "setIndex": 0, "bucketIncarnationId": self.incarnation},
+ "disposition": {"state": entry["state"], **({"details": "retry_exhausted"} if entry["state"] == "failed" else {})}}
+ for entry in expected]}}
+ return expected, status
+
+ def check(self, expected, status, options=None, attempts=4):
+ return outcome.reconcile(status, self.bucket, expected, options or self.options, self.incarnation,
+ attempt_failures=attempts)
+
+ def test_full_mixed_truth_keeps_failure_and_ignores_legacy_repair_count(self):
+ expected, status = self.mixed()
+ proof = self.check(expected, status)
+ self.assertEqual(proof["canonicalCounters"]["failed"], 1)
+ self.assertFalse(proof["failedIsRepair"])
+ self.assertFalse(proof["legacyObjectsHealedUsedAsRepairEvidence"])
+
+ def test_each_identity_boundary_rejects_wrong_or_ambiguous_receipts(self):
+ expected, source = self.mixed()
+ for field, values in {"kind": ["bucket", None], "bucket": ["foreign"], "object": ["foreign"], "versionId": [None, "v9"],
+ "poolIndex": [True, 1, None], "setIndex": [False, 1],
+ "bucketIncarnationId": [None, "", "new-incarnation"]}.items():
+ for value in values:
+ with self.subTest(field=field, value=value):
+ status = copy.deepcopy(source)
+ status["outcome"]["objects"][1]["identity"][field] = value
+ with self.assertRaises(ValueError): self.check(expected, status)
+ status = copy.deepcopy(source)
+ status["outcome"]["objects"][1] = copy.deepcopy(status["outcome"]["objects"][0])
+ with self.assertRaises(ValueError): self.check(expected, status)
+
+ def test_wrong_terminals_or_partial_coverage_cannot_certify_negative_truth(self):
+ expected, source = self.mixed()
+ for name, value in (("summary", "finished"), ("summary", "running"), ("truncated", True),
+ ("settings", {**self.options, "remove": True})):
+ status = copy.deepcopy(source); status[name] = value
+ with self.subTest(name=name, value=value), self.assertRaises(ValueError): self.check(expected, status)
+ for name, value in (("execution", {"state": "completed"}), ("coverage", "partial"), ("objectsTruncated", True)):
+ status = copy.deepcopy(source); status["outcome"][name] = value
+ with self.subTest(name=name, value=value), self.assertRaises(ValueError): self.check(expected, status)
+
+ def test_unknown_skipped_deferred_and_false_health_never_substitute_for_failed(self):
+ expected, source = self.mixed()
+ for state in ("unknown", "skipped", "deferred", "verified_healthy", "metadata_healthy", "repaired"):
+ status = copy.deepcopy(source); status["outcome"]["objects"][2]["disposition"]["state"] = state
+ with self.subTest(state=state), self.assertRaises(ValueError): self.check(expected, status)
+ for details in ("recoverable", "permanent", None):
+ status = copy.deepcopy(source); status["outcome"]["objects"][2]["disposition"]["details"] = details
+ with self.subTest(details=details), self.assertRaises(ValueError): self.check(expected, status)
+
+ def test_every_counter_rejects_inflation_understatement_and_bool_coercion(self):
+ expected, source = self.mixed()
+ for name, original in source["outcome"]["counters"].items():
+ for value in (None, True if name != "overflowed" else 0, original + 1 if type(original) is int else True):
+ status = copy.deepcopy(source); status["outcome"]["counters"][name] = value
+ with self.subTest(counter=name, value=value), self.assertRaises(ValueError): self.check(expected, status)
+
+ def test_dry_run_requires_all_original_members_without_repairs_or_hidden_attempts(self):
+ expected, status = self.mixed()
+ for entry, receipt in zip(expected, status["outcome"]["objects"]):
+ entry["state"] = "dry_run_observed"; receipt["disposition"] = {"state": "dry_run_observed"}
+ options = {**self.options, "dryRun": True}
+ status.update(summary="finished", settings=options)
+ status["outcome"].update(execution={"state": "completed"},
+ counters={"processed": 3, "healed": 0, "unchanged": 0, "skipped": 3, "failed": 0,
+ "unknown": 0, "attemptFailures": 0, "overflowed": False})
+ self.check(expected, status, options, 0)
+ status["outcome"]["objects"].pop()
+ with self.assertRaises(ValueError): self.check(expected, status, options, 0)
+
+ def test_key_level_deferral_cannot_invent_an_incarnation_or_version(self):
+ expected = [{"key": "objects/0002.bin", "versionId": "null", "state": "deferred", "keyLevelRefusal": True}]
+ options = {**self.options, "recursive": False}
+ status = {"summary": "finished", "settings": options, "truncated": False, "outcome": {
+ "execution": {"state": "completed"}, "coverage": "complete", "objectsTruncated": False,
+ "counters": {"processed": 1, "healed": 0, "unchanged": 0, "skipped": 1,
+ "failed": 0, "unknown": 0, "attemptFailures": 1, "overflowed": False},
+ "objects": [{"identity": {"kind": "object", "bucket": self.bucket, "object": expected[0]["key"], "versionId": None,
+ "bucketIncarnationId": None, "poolIndex": 0, "setIndex": 0}, "disposition": {
+ "state": "deferred", "details": {"reason": "transient_existence_check", "retryNotBefore": None}}}]}}
+ self.check(expected, status, options, 1)
+ for field, value in (("bucketIncarnationId", self.incarnation), ("versionId", "null"), ("versionId", "v0")):
+ changed = copy.deepcopy(status); changed["outcome"]["objects"][0]["identity"][field] = value
+ with self.subTest(field=field, value=value), self.assertRaises(ValueError): self.check(expected, changed, options, 1)
+ changed = copy.deepcopy(status)
+ changed["outcome"]["objects"][0]["disposition"]["details"]["reason"] = "dangling_delete_grace"
+ with self.assertRaises(ValueError): self.check(expected, changed, options, 1)
+ for details in ({"reason": "transient_existence_check"},
+ {"reason": "transient_existence_check", "retry_not_before": None},
+ {"reason": "transient_existence_check", "retryNotBefore": {"secs_since_epoch": 123, "nanos_since_epoch": 0}},
+ {"reason": "transient_existence_check", "retryNotBefore": None, "retry_not_before": None}):
+ changed = copy.deepcopy(status)
+ changed["outcome"]["objects"][0]["disposition"]["details"] = details
+ with self.subTest(details=details), self.assertRaises(ValueError):
+ self.check(expected, changed, options, 1)
+
+ def test_planner_uses_actual_erasure_geometry_and_preserves_unselected_versions(self):
+ scene, bucket, expected = versioned_scene([f"objects/{index:04d}.bin" for index in range(3)])
+ for branch in outcome.BRANCHES[:3]:
+ with self.subTest(branch=branch):
+ plan = outcome.fault_plan(scene, bucket, expected, branch)
+ self.assertEqual(len(plan["retainedMembers"]), plan["readQuorum"] - 1)
+ self.assertEqual(len(plan["manifest"]), 1 if branch == "deferred" else len(expected))
+ if branch == "mixed-healthy-repaired-failed":
+ states = {outcome.version_identity(entry): entry["state"] for entry in plan["manifest"]}
+ self.assertEqual(states[("objects/0002.bin", "v0")], "failed")
+ self.assertEqual(states[("objects/0002.bin", "v1")], "verified_healthy")
+ if branch == "deferred":
+ self.assertTrue(all(target["relativePath"].endswith("/xl.meta") for target in plan["faults"]))
+
+ def test_physical_oracle_rejects_false_repair_and_any_change_to_a_control(self):
+ original, bucket, expected = versioned_scene([f"objects/{index:04d}.bin" for index in range(3)])
+ plan = outcome.fault_plan(original, bucket, expected, "mixed-healthy-repaired-failed")
+ faulted = copy.deepcopy(original)
+ for target in plan["faults"]:
+ faulted[target["node"]]["files"] = [entry for entry in faulted[target["node"]]["files"]
+ if (entry["disk"], entry["path"]) != (target["disk"], target["relativePath"])]
+ after = copy.deepcopy(faulted)
+ target = plan["repair"]
+ after[target["node"]]["files"].append(files(original)[target["node"], target["disk"], target["relativePath"]])
+ before_digest = physical_content(after)
+ proof = outcome.retained_failure_scene(original, faulted, after, plan, "mixed-healthy-repaired-failed")
+ self.assertTrue(proof["oracleViewOnly"])
+ self.assertEqual(physical_content(after), before_digest)
+ with self.assertRaises(ValueError):
+ outcome.retained_failure_scene(original, faulted, faulted, plan, "mixed-healthy-repaired-failed")
+ altered = copy.deepcopy(after); altered[target["node"]]["files"][0]["sha256"] = "changed-control"
+ with self.assertRaises(ValueError):
+ outcome.retained_failure_scene(original, faulted, altered, plan, "mixed-healthy-repaired-failed")
+ with self.assertRaises(ValueError):
+ outcome.retained_failure_scene(original, faulted, original, plan, "mixed-healthy-repaired-failed")
+
+ def test_dry_run_and_deferred_require_exact_physical_silence(self):
+ original, bucket, expected = versioned_scene([f"objects/{index:04d}.bin" for index in range(3)])
+ for branch in ("dry-run", "deferred"):
+ plan = outcome.fault_plan(original, bucket, expected, branch)
+ self.assertTrue(outcome.retained_failure_scene(original, original, original, plan, branch)["physicalUnchanged"])
+ changed = copy.deepcopy(original); changed["node1"]["files"].pop()
+ with self.assertRaises(ValueError): outcome.retained_failure_scene(original, original, changed, plan, branch)
+
+ def test_lost_rescue_ack_keeps_registered_fault_for_independent_recovery(self):
+ fleet = types.SimpleNamespace(faults=[("node1", {"disk": 0, "faultId": "J02-mixed-0"})],
+ remote=lambda *args, **kwargs: (_ for _ in ()).throw(RuntimeError("lost ACK")), record=lambda *args: None)
+ with self.assertRaises(RuntimeError): outcome.rescue(fleet)
+ self.assertEqual(len(fleet.faults), 1)
+
+ def test_each_reviewed_candidate_requires_native_evidence_before_fleet_creation(self):
+ self.assertEqual(outcome.REVIEWED_CANDIDATES,
+ {outcome.CANDIDATE, "1c7fc818dd858247c45b39542abd8291b1e6e31b",
+ "7951bb629821246868ee3761b321dd07c06db45c"})
+ for commit in outcome.REVIEWED_CANDIDATES:
+ with self.subTest(commit=commit), tempfile.TemporaryDirectory() as tmp:
+ root = Path(tmp); candidate = root / "candidate.json"
+ manifest = {"source_sha": commit}; candidate.write_text(json.dumps(manifest))
+ args = types.SimpleNamespace(candidate=candidate)
+ with patch.object(outcome.argparse.ArgumentParser, "parse_args", return_value=args), \
+ patch.object(outcome, "verify_manager", side_effect=ValueError("wrong immutable evidence")) as native, \
+ patch.object(outcome, "Fleet") as fleet, patch.object(outcome.signal, "signal"):
+ with self.assertRaisesRegex(ValueError, "immutable evidence"): outcome.main()
+ native.assert_called_once_with(os.environ.get("VALIDATION_HEAL_MANAGER_DIRECTORY", ""), manifest, outcome.CASE_ID)
+ fleet.assert_not_called()
+
+ def test_unreviewed_candidate_is_rejected_before_native_or_fleet_activity(self):
+ for commit in ("f" * 40, "1aa9be87d18cf941e5020f19e8622c26ba4b10ac", "main"):
+ with self.subTest(commit=commit), tempfile.TemporaryDirectory() as tmp:
+ candidate = Path(tmp) / "candidate.json"; candidate.write_text(json.dumps({"source_sha": commit}))
+ with patch.object(outcome.argparse.ArgumentParser, "parse_args", return_value=types.SimpleNamespace(candidate=candidate)), \
+ patch.object(outcome, "verify_manager") as native, patch.object(outcome, "Fleet") as fleet, \
+ patch.object(outcome.signal, "signal"):
+ with self.assertRaisesRegex(ValueError, "not reviewed"): outcome.main()
+ native.assert_not_called(); fleet.assert_not_called()
+
+ def test_unknown_branch_requires_real_executor_for_each_topology_and_fixture(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ root = Path(tmp); candidate = root / "candidate.json"
+ candidate.write_text(json.dumps({"source_sha": outcome.CANDIDATE}))
+ args = types.SimpleNamespace(candidate=candidate, evidence=root / "evidence", binary_sha="a" * 64)
+ def branch(args, topology, nodes, fixture, name, candidate):
+ return {"topology": topology, "fixture": fixture, "branch": name, "verdict": "BLOCKED" if name == "unknown-negative" else "PASS", "cleanup": "PASS"}
+ with patch.object(outcome.argparse.ArgumentParser, "parse_args", return_value=args), \
+ patch.object(outcome, "verify_manager", return_value={"canonicalFleetBranchesExecuted": False}), \
+ patch.object(outcome, "run_branch", side_effect=branch) as executed, patch.object(outcome.signal, "signal"), \
+ patch.dict(os.environ, {"GITHUB_RUN_ID": "1234", "RUSTFS_NODES": "vm000 vm001 vm002 vm003"}):
+ self.assertEqual(outcome.main(), 1)
+ summary = json.loads((args.evidence / "healthy-summary.json").read_text())
+ self.assertEqual(executed.call_count, 20)
+ self.assertEqual(len(summary["results"]), 20)
+ self.assertFalse(summary["complete"])
+ self.assertEqual(summary["cleanup"], "PASS")
+ negative = [row for row in summary["results"] if row["branch"] == "unknown-negative"]
+ self.assertEqual(len(negative), 4)
+ self.assertTrue(all(row["verdict"] == "BLOCKED" for row in negative))
+
+ def test_all_diagnostic_branches_passing_cannot_omit_original_source_procedures(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ root = Path(tmp); candidate = root / "candidate.json"
+ candidate.write_text(json.dumps({"source_sha": outcome.CANDIDATE}))
+ args = types.SimpleNamespace(candidate=candidate, evidence=root / "evidence", binary_sha="a" * 64)
+ def branch(args, topology, nodes, fixture, name, candidate):
+ return {"topology": topology, "fixture": fixture, "branch": name, "verdict": "PASS", "cleanup": "PASS"}
+ with patch.object(outcome.argparse.ArgumentParser, "parse_args", return_value=args), \
+ patch.object(outcome, "verify_manager", return_value={}), patch.object(outcome, "run_branch", side_effect=branch), \
+ patch.object(outcome.signal, "signal"), patch.dict(os.environ, {"GITHUB_RUN_ID": "1234"}):
+ self.assertEqual(outcome.main(), 1)
+ summary = json.loads((args.evidence / "healthy-summary.json").read_text())
+ self.assertTrue(summary["diagnosticBranchesPassed"])
+ self.assertFalse(summary["complete"])
+ self.assertFalse(summary["canonicalCasePassClaimed"])
+ self.assertEqual(summary["sourceAssertionGaps"], [])
+ self.assertFalse(summary["canonicalAdmissionEnabled"])
+ self.assertTrue(summary["liveSourceProcedureAcceptancePending"])
+
+ def test_dirty_cleanup_stops_before_next_fixture_or_topology(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ root = Path(tmp); candidate = root / "candidate.json"
+ candidate.write_text(json.dumps({"source_sha": outcome.CANDIDATE}))
+ args = types.SimpleNamespace(candidate=candidate, evidence=root / "evidence", binary_sha="a" * 64)
+ row = {"topology": "H1", "fixture": "DB", "branch": outcome.BRANCHES[0], "verdict": "FAIL", "cleanup": "FAIL"}
+ with patch.object(outcome.argparse.ArgumentParser, "parse_args", return_value=args), \
+ patch.object(outcome, "verify_manager", return_value={}), \
+ patch.object(outcome, "run_branch", return_value=row) as run, patch.object(outcome.signal, "signal"), \
+ patch.dict(os.environ, {"GITHUB_RUN_ID": "1234", "RUSTFS_NODES": "vm000 vm001 vm002 vm003"}):
+ self.assertEqual(outcome.main(), 1)
+ self.assertEqual(run.call_count, 1)
+ summary = json.loads((args.evidence / "healthy-summary.json").read_text())
+ self.assertFalse(summary["complete"])
+ self.assertEqual(summary["cleanup"], "FAIL")
+
+
+
+class LegacyUnknownTests(unittest.TestCase):
+ def physical(self):
+ original, bucket, expected = versioned_scene(["objects/0000.bin", "legacy/0000.bin"])
+ for scene in original.values():
+ for row in scene["files"]:
+ for version in row.get("metadata", {}).get("versions", []):
+ for part in version["parts"]:
+ part["integrityDescriptorPresent"] = not row["metadata"]["key"].startswith("legacy/")
+ return original, bucket, expected
+
+ def test_unknown_oracle_requires_independent_complete_physical_legacy_and_protected_control(self):
+ original, bucket, expected = self.physical()
+ plan = outcome.legacy_manifest(original, bucket, expected)
+ incarnation = MixedOutcomeTest.incarnation
+ options = {**BASE_OPTIONS, "recursive": True}
+ receipts = [{"identity": {"kind": "object", "bucket": bucket, "object": row["key"], "versionId": row["versionId"],
+ "bucketIncarnationId": incarnation, "poolIndex": 0, "setIndex": 0},
+ "disposition": {"state": row["state"]}} for row in plan["manifest"]]
+ counts = {"processed": 6, "healed": 0, "unchanged": 3, "skipped": 3, "failed": 0,
+ "unknown": 3, "attemptFailures": 0, "overflowed": False}
+ status = {"summary": "finished", "truncated": False, "settings": options, "objectsHealed": 99,
+ "outcome": {"execution": {"state": "completed"}, "coverage": "complete", "objectsTruncated": False,
+ "objects": receipts, "counters": counts}}
+ unknown = [tuple(pair) for pair in plan["unknownIdentities"]]
+ proof = outcome.reconcile(status, bucket, plan["manifest"], options, incarnation, attempt_failures=0, legacy_unknown=unknown)
+ self.assertFalse(proof["unknownIsRepair"])
+ self.assertEqual(proof["canonicalCounters"]["healed"], 0)
+ counters = proof["canonicalCounters"]
+ self.assertEqual(counters["unknown"], 3)
+ self.assertEqual(counters["skipped"], 3)
+ self.assertEqual(counters["processed"], sum(counters[key] for key in ("healed", "unchanged", "skipped", "failed")))
+ with self.assertRaises(ValueError): outcome.reconcile(status, bucket, plan["manifest"], options, incarnation, attempt_failures=0)
+ for mutation in ("repaired", "identity", "failed", "incomplete", "unknown-count", "attempt-failure"):
+ changed = copy.deepcopy(status)
+ if mutation == "repaired": changed["outcome"]["objects"][-1]["disposition"]["state"] = "repaired"
+ elif mutation == "identity": changed["outcome"]["objects"][-1]["identity"]["bucketIncarnationId"] = "foreign"
+ elif mutation == "failed": changed["outcome"]["execution"]["state"] = "completed_with_errors"
+ elif mutation == "incomplete": changed["outcome"]["coverage"] = "partial"
+ elif mutation == "unknown-count": changed["outcome"]["counters"]["unknown"] = 0
+ else: changed["outcome"]["counters"]["attemptFailures"] = 1
+ with self.subTest(mutation=mutation), self.assertRaises(ValueError):
+ outcome.reconcile(changed, bucket, plan["manifest"], options, incarnation, attempt_failures=0, legacy_unknown=unknown)
+
+ def test_descriptor_missing_true_integer_or_a_missing_cohort_never_creates_unknown_fixture(self):
+ for mutation in ("missing", "protected", "coerced", "missing-member", "no-control"):
+ original, bucket, expected = self.physical()
+ row = next(x for x in original["node1"]["files"] if x.get("metadata", {}).get("key", "").startswith("legacy/"))
+ if mutation == "missing": row["metadata"]["versions"][0]["parts"][0].pop("integrityDescriptorPresent")
+ elif mutation == "protected": row["metadata"]["versions"][0]["parts"][0]["integrityDescriptorPresent"] = True
+ elif mutation == "coerced": row["metadata"]["versions"][0]["parts"][0]["integrityDescriptorPresent"] = 0
+ elif mutation == "missing-member": original["node1"]["files"].remove(row)
+ else: expected = [x for x in expected if x["key"].startswith("legacy/")]
+ with self.subTest(mutation=mutation), self.assertRaises(ValueError): outcome.legacy_manifest(original, bucket, expected)
+
+ def test_legacy_append_uses_actual_sdk_calls_after_all_node_write_mode_switches(self):
+ events = []
+ def call(operation, params, body=None):
+ events.append((operation, params))
+ if operation == "put_object": return {"VersionId": str(len(events))}
+ return {"VersionId": str(len(events)), "DeleteMarker": True}
+ probe = types.SimpleNamespace()
+ fleet = types.SimpleNamespace(nodes=["a", "b"], remote=lambda node, action: events.append((action, node)) or {
+ "runId": "gh-12345-1-h1", "binarySha256": "a" * 64, "phase": "legacy", "oneWay": True,
+ "candidateUnchanged": True, "otherRuntimeSettingsUnchanged": True, "credentialsDisclosed": False},
+ record=lambda label, value: value, start=lambda: events.append(("start", None)),
+ snapshot=lambda label: {node: {"backgroundConfig": {"RUSTFS_SHARD_INTEGRITY_WRITE": "false",
+ "RUSTFS_HEAL_MRF_ENABLE": "false", "RUSTFS_SCANNER_ENABLED": "false"}} for node in ("a", "b")})
+ with patch.object(outcome, "Recorder", return_value=types.SimpleNamespace(call=call)):
+ value = outcome.append_legacy_fixture(probe, fleet, Path("/unused"),
+ {"bucketPrefix": MixedOutcomeTest.bucket, "runId": "gh-12345-1-h1", "candidateBinarySha256": "a" * 64},
+ [{"key": "protected-control"}], "DV")
+ self.assertEqual([name for name,_ in events[:3]], ["outcome-legacy-phase", "outcome-legacy-phase", "start"])
+ self.assertEqual(len(value), 8)
+ self.assertEqual(sum(name == "put_object" for name,_ in events), 6)
+ self.assertEqual(sum(name == "delete_object" for name,_ in events), 1)
+ self.assertEqual(sum(row.get("deleteMarker", False) for row in value), 1)
+
+
+class LegacyPhaseTests(unittest.TestCase):
+ def test_one_way_private_configuration_commit_survives_each_completed_write_boundary(self):
+ from scripts.validation_heal import fleet_node as node, heal_fault as fault
+ from test_validation_heal_retained_scene import RetainedSceneTests
+ for cut in (None, "J02-legacy-intent.json", "J02-service.env.new", "J02-phase.new", "rename-service.env", "rename-J02-phase.json"):
+ with self.subTest(cut=cut), tempfile.TemporaryDirectory() as temp:
+ state = Path(temp).resolve(); binary = b"candidate"; (state / "rustfs").write_bytes(binary)
+ owner = {"runId": "gh-12345-1-h1", "unit": "owned.service", "runtimeProfile": "outcome-legacy",
+ "binarySha256": __import__('hashlib').sha256(binary).hexdigest()}
+ config = {**node.runtime_settings(owner), "RUSTFS_ADDRESS": ":9000", "RUSTFS_SECRET_KEY": "private-must-not-escape"}
+ env = state / "service.env"; env.write_text("".join(f"{key}={value}\n" for key,value in config.items())); env.chmod(0o600)
+ request = {"runId": owner["runId"], "action": "outcome-legacy-phase"}
+ def owned(run):
+ value = {**owner, "outcomePhase": "legacy"} if (state / "J02-phase.json").exists() else owner
+ return state, [], value
+ original_stat = os.stat
+ def root_stat(path, *args, **kwargs):
+ info = original_stat(path, *args, **kwargs)
+ values = {name: getattr(info,name) for name in dir(info) if name.startswith("st_")}
+ values["st_uid"] = 0
+ return types.SimpleNamespace(**values)
+ original = fault.new_file
+ original_rename = os.rename
+ fired = False
+ def write(fd, name, value):
+ nonlocal fired
+ original(fd, name, value)
+ if cut == name and not fired:
+ fired = True; raise RuntimeError("lost acknowledgement")
+ def rename(source, target, **kwargs):
+ nonlocal fired
+ original_rename(source, target, **kwargs)
+ if cut == "rename-" + target and not fired:
+ fired = True; raise RuntimeError("lost acknowledgement after rename")
+ with patch.object(node, "owned", side_effect=owned), patch.object(node, "stopped"), \
+ patch.object(node, "fault_module", return_value=fault), patch.object(fault, "mount_id", return_value=7), \
+ RetainedSceneTests().root_stats(), patch.object(os, "stat", side_effect=root_stat), \
+ patch.object(fault, "new_file", side_effect=write), patch.object(os, "rename", side_effect=rename):
+ if cut:
+ with self.assertRaises(RuntimeError): node.outcome_legacy_phase(request)
+ if not (state / "J02-phase.json").exists():
+ with patch.object(node, "command") as command, self.assertRaisesRegex(ValueError, "incomplete"):
+ node.start({"runId": owner["runId"], "action": "start"})
+ self.assertNotIn("systemd-run", [call.args[0] for call in command.call_args_list])
+ result = node.outcome_legacy_phase(request)
+ self.assertTrue(result["oneWay"])
+ self.assertNotIn(config["RUSTFS_SECRET_KEY"], json.dumps(result))
+ actual = dict(line.split("=",1) for line in env.read_text().splitlines())
+ self.assertEqual(actual, {**config, "RUSTFS_SHARD_INTEGRITY_WRITE": "false"})
+ self.assertEqual(node.runtime_settings(owned(owner["runId"])[2])["RUSTFS_SHARD_INTEGRITY_WRITE"], "false")
+ self.assertTrue(node.outcome_legacy_phase(request)["otherRuntimeSettingsUnchanged"])
+ env.write_text(env.read_text().replace("private-must-not-escape", "foreign"))
+ with self.assertRaises(ValueError): node.outcome_legacy_phase(request)
+
+ def test_actual_owned_reader_rejects_uncommitted_or_foreign_phase_markers(self):
+ from scripts.validation_heal import fleet_node as node
+ from test_validation_heal_retained_scene import RetainedSceneTests
+ with tempfile.TemporaryDirectory() as directory:
+ state, roots, scope, request = RetainedSceneTests().scene(directory)
+ owner = {**request["expectedOwner"], "runtimeProfile": "outcome-legacy"}
+ for folder in (*roots, state):
+ folder.mkdir(parents=True, exist_ok=True)
+ (folder / "owner.json").write_text(json.dumps(owner))
+ phase = state / "J02-phase.json"
+ with RetainedSceneTests().root_stats(), patch.object(node, "paths", return_value=(state, roots)), \
+ patch.object(node, "mounts", return_value=owner["mounts"]):
+ self.assertNotIn("outcomePhase", node.owned(owner["runId"])[2])
+ phase.write_text(json.dumps({"runId": owner["runId"], "phase": "legacy"}))
+ self.assertEqual(node.owned(owner["runId"])[2]["outcomePhase"], "legacy")
+ for value in ({"runId": owner["runId"], "phase": "protected"},
+ {"runId": "foreign", "phase": "legacy"},
+ {"runId": owner["runId"], "phase": "legacy", "extra": True}):
+ phase.write_text(json.dumps(value))
+ with self.subTest(value=value), self.assertRaises(ValueError): node.owned(owner["runId"])
+
+ def test_phase_adapter_loads_real_fault_backend(self):
+ from scripts.validation_heal.healthy import Fleet
+ def execute(*args, **kwargs):
+ scope = {"__name__": "__J02_node__"}
+ exec(compile(kwargs["input"].rsplit("\nmain(",1)[0], "J02-remote", "exec"), scope)
+ self.assertTrue(callable(scope["fault"].new_file))
+ self.assertTrue(callable(scope["outcome_legacy_phase"]))
+ return types.SimpleNamespace(returncode=0, stdout="{}", stderr="")
+ with tempfile.TemporaryDirectory() as directory, patch("scripts.validation_heal.healthy.subprocess.run", side_effect=execute):
+ Fleet(["node"], Path(directory), "gh-12345-1-h1", runtime_profile="outcome-legacy").remote("node", "outcome-legacy-phase")
+
+
+
+class ReadonlySourceFlowTests(unittest.TestCase):
+ bucket = MixedOutcomeTest.bucket
+ key = "objects/0001.bin"
+ options = {**BASE_OPTIONS, "recursive": False}
+
+ def failure(self):
+ return {"summary": "stopped", "settings": self.options, "truncated": False, "outcome": {
+ "execution": {"state": "completed_with_errors"}, "coverage": "complete", "objectsTruncated": False,
+ "counters": {"processed": 1, "healed": 0, "unchanged": 0, "skipped": 0,
+ "failed": 1, "unknown": 0, "attemptFailures": 1, "overflowed": False},
+ "objects": [{"identity": {"kind": "object", "bucket": self.bucket, "object": self.key,
+ "versionId": None, "bucketIncarnationId": None, "poolIndex": 0, "setIndex": 0},
+ "disposition": {"state": "failed", "details": "permanent"}}]}}
+
+ def test_permanent_terminal_failure_must_not_invent_a_verified_version_or_incarnation(self):
+ status = self.failure()
+ self.assertFalse(outcome.readonly_failure(status, self.bucket, self.key, self.options)["repairClaimed"])
+ for mutation in ("version", "incarnation", "kind", "pool-bool", "attempts", "healed", "unknown", "deferred", "recoverable", "settings"):
+ changed = copy.deepcopy(status); receipt = changed["outcome"]["objects"][0]
+ if mutation == "version": receipt["identity"]["versionId"] = "null"
+ elif mutation == "incarnation": receipt["identity"]["bucketIncarnationId"] = MixedOutcomeTest.incarnation
+ elif mutation == "kind": receipt["identity"]["kind"] = "bucket"
+ elif mutation == "pool-bool": receipt["identity"]["poolIndex"] = False
+ elif mutation == "attempts": changed["outcome"]["counters"]["attemptFailures"] = 0
+ elif mutation == "healed": changed["outcome"]["counters"]["healed"] = 1
+ elif mutation == "unknown": receipt["disposition"] = {"state": "unknown"}
+ elif mutation == "deferred": receipt["disposition"] = {"state": "deferred", "details": {"reason": "deadline"}}
+ elif mutation == "recoverable": receipt["disposition"]["details"] = "retry_exhausted"
+ else: changed["settings"] = {**self.options, "remove": True}
+ with self.subTest(mutation=mutation), self.assertRaises(ValueError):
+ outcome.readonly_failure(changed, self.bucket, self.key, self.options)
+
+ def test_lost_readonly_arm_ack_keeps_gate_registered_for_final_cleanup(self):
+ original, bucket, expected = versioned_scene([f"objects/{i:04d}.bin" for i in range(3)])
+ actual_bucket = "rv-heal-" + __import__("hashlib").sha256(b"gh-12345-1-h1").hexdigest()[:16]
+ for scene in original.values():
+ for row in scene["files"]:
+ row["path"] = row["path"].replace(bucket,actual_bucket)
+ if "metadata" in row: row["metadata"]["bucket"] = actual_bucket
+ bucket = actual_bucket
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory); rc = root / "rc"; rc.write_bytes(b"test-cli")
+ args = types.SimpleNamespace(evidence=root / "evidence", rc_bin=rc, binary_sha="b" * 64,
+ testing_commit="a" * 40, tool_directory=root / "tools")
+ gates, current, cleanups = set(), copy.deepcopy(original), []
+ def remote(node, action, **params):
+ if action == "apply-fault":
+ current[node]["files"] = [row for row in current[node]["files"] if row["path"] != params["relativePath"] or row["disk"] != params["disk"]]
+ return {}
+ if action == "drive-gate":
+ self.assertIn(node, gates)
+ raise RuntimeError("lost readonly gate ACK")
+ return {}
+ def cleanup():
+ cleanups.append(set(gates)); return True
+ fleet = types.SimpleNamespace(nodes=["node1"], drive_gates=gates, reserved=["node1"], faults=[], remote=remote,
+ deploy=lambda *args: None, snapshot=lambda *args,**kwargs: copy.deepcopy(current),
+ record=lambda label,value: value, stop=lambda: None, start=lambda: None, cleanup=cleanup)
+ probe = types.SimpleNamespace(inventory=lambda *args,**kwargs: {"bucket":bucket,"entries":expected})
+ with patch.object(outcome,"Fleet",return_value=fleet), patch.object(outcome,"Probe",return_value=probe), \
+ patch.object(outcome,"Recorder"), patch.object(outcome,"seed",return_value=expected), \
+ patch.object(outcome,"verify_inspector",return_value={}), patch.object(outcome,"verify_quorum_contract",return_value={}), \
+ patch.object(outcome,"run_task",return_value={}), patch.object(outcome,"validate_version_receipts",return_value={"bucketIncarnationId":MixedOutcomeTest.incarnation}), \
+ patch.object(outcome,"idle_fleet"), patch.object(outcome,"validate_fault"), patch.object(outcome,"verify_proof"), \
+ patch.dict(os.environ,{"GITHUB_RUN_ID":"12345","GITHUB_RUN_ATTEMPT":"1"}):
+ result = outcome.run_branch(args,"H1",["node1"],"DV","readonly-terminal-failure",
+ {"source_sha":outcome.CANDIDATE,"package_sha256":"c" * 64})
+ self.assertEqual(result["verdict"],"FAIL")
+ self.assertIn("lost readonly gate ACK",result["failure"]["message"])
+ self.assertEqual(result["cleanup"],"PASS")
+ self.assertEqual(cleanups,[{"node1"}])
+
+ def test_readonly_plan_selects_exact_current_version_without_a_below_R_fault(self):
+ original, bucket, expected = versioned_scene([f"objects/{i:04d}.bin" for i in range(3)])
+ plan = outcome.fault_plan(original, bucket, expected, "readonly-terminal-failure")
+ self.assertEqual(len(plan["faults"]), 1)
+ self.assertEqual(plan["repair"]["versionId"], "v2")
+ self.assertTrue(plan["repair"]["logical"]["isLatest"])
+ self.assertEqual(plan["faults"][0], plan["repair"])
+
+ def test_compensation_observes_readonly_failure_then_distinct_new_token_without_replacing_original(self):
+ from scripts.validation_heal.shards import files
+ for variant in (None, "fake-gate", "same-token", "changed-original", "changed-control", "bad-control"):
+ with self.subTest(variant=variant):
+ original, bucket, expected = versioned_scene([f"objects/{i:04d}.bin" for i in range(3)])
+ plan = outcome.fault_plan(original, bucket, expected, "readonly-terminal-failure")
+ target = plan["repair"]; faulted = copy.deepcopy(original)
+ faulted[target["node"]]["files"] = [row for row in faulted[target["node"]]["files"]
+ if (target["node"], row["disk"], row["path"]) != (target["node"], target["disk"], target["relativePath"])]
+ repaired_scene = copy.deepcopy(original)
+ if variant == "changed-control": repaired_scene["node1"]["files"][0]["sha256"] = "changed"
+ negative = self.failure()
+ positive = {"summary": "finished", "settings": self.options, "truncated": False, "outcome": {
+ "execution": {"state": "completed"}, "coverage": "complete", "objectsTruncated": False,
+ "counters": {"processed": 1, "healed": 1, "unchanged": 0, "skipped": 0,
+ "failed": 0, "unknown": 0, "attemptFailures": 0, "overflowed": False},
+ "objects": [{"identity": {"kind": "object", "bucket": bucket, "object": self.key,
+ "versionId": target["versionId"], "bucketIncarnationId": MixedOutcomeTest.incarnation,
+ "poolIndex": 0, "setIndex": 0}, "disposition": {"state": "repaired"}}]}}
+ baseline = {"bucket": bucket, "entries": expected}; events = []; queries = []
+ def remote(node, action, **params):
+ events.append(action)
+ if action == "drive-gate-probe": return {"disk": target["disk"], "errno": 13 if variant == "fake-gate" else 30,
+ "name": "EROFS", "sameServiceIdentity": True, "privateMountNamespace": True, "hostMountRemounted": False}
+ if action == "drive-gate": return {"state": "CLEAR"}
+ return {"restored": False}
+ def snapshot(label, **kwargs):
+ events.append(label)
+ return copy.deepcopy(faulted if label in {"readonly-original-failure-physical", "readonly-released-before-new-token", "readonly-after-gated-control-before-admission"} else repaired_scene)
+ fleet = types.SimpleNamespace(nodes=["node1"], record=lambda label,value: value, remote=remote,
+ drive_gates={target["node"]}, faults=[], stop=lambda: events.append("stop"), start=lambda: events.append("start"), snapshot=snapshot)
+ def admin(path, query, body):
+ queries.append(query); result = copy.deepcopy(negative)
+ if variant == "changed-original" and len(queries) == 2: result["outcome"]["counters"]["failed"] = 0
+ return {"httpStatus": 200, "response": result}
+ probe = types.SimpleNamespace(admin=admin)
+ def task(probe,fleet,scope,mode,active,**kwargs):
+ number = len([item for item in events if item == "admit"]); events.append("admit")
+ if number == 0:
+ control = copy.deepcopy(positive)
+ control["outcome"]["counters"].update(healed=0,unchanged=1)
+ control["outcome"]["objects"][0]["identity"]["object"] = "objects/0000.bin"
+ control["outcome"]["objects"][0]["disposition"] = {"state":"unknown" if variant == "bad-control" else "verified_healthy"}
+ return control
+ kwargs["admissions"].append({"path": "/heal", "clientToken": "old" if number == 1 or variant == "same-token" else "new"})
+ return negative if number == 1 else positive
+ binding = {"bucketPrefix":bucket,"endpoints":{"node1":"http://unused"}}
+ with patch.object(outcome,"run_task",side_effect=task), patch.object(outcome,"idle_fleet"), \
+ patch.object(outcome,"verify_proof",side_effect=lambda *args: events.append("bitrot-proof")), \
+ patch.object(outcome,"Probe",return_value=types.SimpleNamespace(inventory=lambda *args,**kwargs: baseline)):
+ if variant:
+ with self.assertRaises(ValueError): outcome.readonly_compensation(probe,fleet,binding,original,faulted,plan,expected,baseline,MixedOutcomeTest.incarnation,[])
+ else:
+ value=outcome.readonly_compensation(probe,fleet,binding,original,faulted,plan,expected,baseline,MixedOutcomeTest.incarnation,[])
+ self.assertFalse(value["sameTokenRecoveryClaimed"])
+ self.assertTrue(value["originalFailureUnchanged"])
+ self.assertNotEqual(value["originalToken"],value["compensatingToken"])
+ self.assertEqual(sum(item == "admit" for item in events),3)
+ self.assertLess(events.index("readonly-original-failure-physical"),events.index("drive-gate"))
+ self.assertLess(events.index("readonly-released-before-new-token"),len(events)-1)
+ self.assertEqual(len(queries),2)
+
+
+
+class OriginalOpaqueMetadataTests(unittest.TestCase):
+ bucket = 'rv-heal-0123456789abcdef'
+ key = 'opaque/source.bin'
+ options = {**outcome.BASE_OPTIONS, 'recursive': False}
+
+ def status(self, state='unknown'):
+ failed = state == 'failed'
+ return {'summary': 'stopped' if failed else 'finished', 'settings': self.options, 'truncated': False,
+ 'detail': 'legacy incomplete outcome; unknown is not proof of repair' if state == 'unknown' else None,
+ 'outcome': {'execution': {'state': 'completed_with_errors' if failed else 'completed'},
+ 'coverage': 'complete', 'objectsTruncated': False,
+ 'counters': {'processed': 1, 'healed': 0, 'unchanged': 0, 'skipped': 0 if failed else 1,
+ 'failed': int(failed), 'unknown': int(state == 'unknown'), 'attemptFailures': int(state != 'unknown'), 'overflowed': False},
+ 'objects': [{'identity': {'kind': 'object', 'bucket': self.bucket, 'object': self.key,
+ 'versionId': None, 'bucketIncarnationId': None, 'poolIndex': 0, 'setIndex': 0},
+ 'disposition': {'state': state}}]}}
+
+ def test_original_metadata_unknown_observation_and_nonobservation_are_distinct(self):
+ for state in ('unknown', 'deferred', 'failed'):
+ result = outcome.opaque_metadata_outcome(self.status(state), self.bucket, self.key, self.options, "original-anchor")
+ self.assertEqual(result['verdict'], 'PASS' if state == 'unknown' else 'NOT_OBSERVED')
+ self.assertEqual(result['unknownObserved'], state == 'unknown')
+ self.assertFalse(result['canonicalPassClaimed'])
+ self.assertFalse(result['productRepairClaimed'])
+
+ def test_known_original_anchor_is_allowed_without_inventing_a_new_incarnation(self):
+ status = self.status()
+ status["outcome"]["objects"][0]["identity"]["bucketIncarnationId"] = "original-anchor"
+ self.assertTrue(outcome.opaque_metadata_outcome(status, self.bucket, self.key, self.options, "original-anchor")["unknownObserved"])
+
+ def test_false_repair_unknown_counters_or_identity_are_rejected(self):
+ for poison in ('healthy', 'absent', 'repaired', 'unknown-partition', 'boolean-counter', 'overflow', 'omitted',
+ 'wrong-key', 'invented-version', 'invented-incarnation', 'boolean-pool', 'warning', 'running', 'partial', 'execution'):
+ with self.subTest(poison=poison):
+ value = self.status(); body = value['outcome']; obj = body['objects'][0]
+ if poison in ('healthy', 'absent', 'repaired'):
+ obj['disposition']['state'] = {'healthy': 'verified_healthy', 'absent': 'authoritatively_absent', 'repaired': 'repaired'}[poison]
+ elif poison == 'unknown-partition': body['counters']['skipped'] = 0
+ elif poison == 'boolean-counter': body['counters']['processed'] = True
+ elif poison == 'overflow': body['counters']['overflowed'] = True
+ elif poison == 'omitted': body['objects'] = []
+ elif poison == 'wrong-key': obj['identity']['object'] = 'other'
+ elif poison == 'invented-version': obj['identity']['versionId'] = 'new-version'
+ elif poison == 'invented-incarnation': obj['identity']['bucketIncarnationId'] = 'fabricated'
+ elif poison == 'boolean-pool': obj['identity']['poolIndex'] = False
+ elif poison == 'warning': value['detail'] = None
+ elif poison == 'running': value['summary'] = 'running'
+ elif poison == 'partial': body['coverage'] = 'partial'
+ elif poison == 'execution': body['execution']['state'] = 'completed_with_errors'
+ with self.assertRaises(ValueError):
+ outcome.opaque_metadata_outcome(value, self.bucket, self.key, self.options, "original-anchor")
+
+ def test_original_probe_mutates_all_four_metadata_copies_and_restores_before_reads(self):
+ original, bucket, entries = versioned_scene([self.key, 'objects/control.bin'])
+ expected = [entry for entry in entries if entry['key'] == 'objects/control.bin']
+ current = copy.deepcopy(original)
+ events, registrations = [], {}
+ active = []
+ status = self.status()
+ fleet = types.SimpleNamespace(nodes=['node1'], faults=[])
+ fleet.start = lambda: events.append('start')
+ fleet.stop = lambda: events.append('stop')
+ def record(label, value):
+ events.append(label); return value
+ fleet.record = record
+ def snapshot(label, **params):
+ events.append(label)
+ if any('metadataError' in row for row in current['node1']['files']):
+ self.assertTrue(params.get('metadata_faults'))
+ from scripts.validation_heal import fleet_node
+ with patch.object(fleet_node, 'command', return_value=types.SimpleNamespace(returncode=1, stderr='Error: FileCorrupt\n')):
+ for row in current['node1']['files']:
+ if 'metadataError' in row:
+ witness = fleet_node.inspect_metadata(Path('/inspector'), Path('/xl.meta'), bucket, self.key,
+ row, params['metadata_faults']['node1'])
+ self.assertTrue(witness['metadataError']['exactWitnessedFault'])
+ return copy.deepcopy(current)
+ fleet.snapshot = snapshot
+ def remote(node, action, **params):
+ events.append(action)
+ if action == 'apply-fault':
+ self.assertEqual(fleet.faults[-1], (node, {'disk': params['disk'], 'faultId': params['faultId']}))
+ self.assertEqual((params['kind'], params['offset']), ('bitflip', 0))
+ self.assertTrue(params['relativePath'].endswith('/opaque/source.bin/xl.meta'))
+ row = next(row for row in current[node]['files'] if row['disk'] == params['disk'] and row['path'] == params['relativePath'])
+ registrations[params['faultId']] = copy.deepcopy(row)
+ before = {key: row[key] for key in ('sha256', 'size')}
+ row['sha256'] = 'corrupt-' + str(row['disk'])
+ row.pop('metadata')
+ row['metadataError'] = {'exactWitnessedFault': True, 'validMetadataClaimed': False}
+ return {'kind': 'bitflip', 'offset': 0, 'relativePath': row['path'], 'before': before,
+ 'fault': {key: row[key] for key in ('sha256', 'size')}}
+ self.assertEqual(action, 'recover-fault')
+ saved = registrations[params['faultId']]
+ row = next(row for row in current[node]['files'] if row['disk'] == saved['disk'] and row['path'] == saved['path'])
+ row.clear(); row.update(copy.deepcopy(saved))
+ return {'physicalRepairPassAllowed': False}
+ fleet.remote = remote
+ def inventory(*args, **kwargs):
+ events.append('business-GET-inventory')
+ self.assertEqual(physical_content(current), physical_content(original))
+ return {'entries': []}
+ probe = types.SimpleNamespace(inventory=inventory, admin=lambda *args: {'httpStatus': 200, 'response': status})
+ def task(probe, fleet, scope, mode, active, **params):
+ events.append('task:' + scope['key'])
+ if 'admissions' in params:
+ params['admissions'].append({'path': '/heal/opaque', 'clientToken': 'original-opaque-token'})
+ return status
+ return {}
+ binding = {'bucketPrefix': bucket, 'endpoints': {'node1': 'http://node1'}}
+ with tempfile.TemporaryDirectory() as directory, patch.object(outcome, 'Recorder') as recorder, \
+ patch.object(outcome, 'Probe', return_value=probe), patch.object(outcome, 'run_task', side_effect=task), \
+ patch.object(outcome, 'validate_version_receipts', return_value={'bucketIncarnationId': 'incarnation'}), \
+ patch.object(outcome, 'verify_seed', return_value={}), patch.object(outcome, 'compare_inventories'), \
+ patch.object(outcome, 'verify_proof'), patch.object(outcome, 'payload', return_value=b'opaque-fixture'), \
+ patch.object(outcome, 'idle_fleet'):
+ recorder.return_value.call.return_value = {'VersionId': 'v2'}
+ updated, baseline, result = outcome.original_opaque_probe(
+ probe, fleet, binding, Path(directory), expected, 'DV', 'incarnation', active)
+ self.assertEqual(result['originalMetadataCopiesCorrupted'], 4)
+ self.assertTrue(result['unknownObserved'])
+ self.assertTrue(result['originalTokenUnchanged'])
+ self.assertEqual(len(updated), len(expected) + 1)
+ self.assertEqual(fleet.faults, [])
+ self.assertEqual(events.count('apply-fault'), 4)
+ self.assertEqual(events.count('recover-fault'), 4)
+ self.assertLess(events.index('original-opaque-terminal-before-rescue'), events.index('recover-fault'))
+ self.assertLess(events.index('original-opaque-restored-original-bytes'), events.index('original-opaque-restored-full-S3'))
+
+
+class OmittedFalseTruncationTests(unittest.TestCase):
+ def test_original_wire_omission_does_not_relax_canonical_manifest_or_counter_checks(self):
+ mixed = MixedOutcomeTest(); expected, status = mixed.mixed()
+ readonly = ReadonlySourceFlowTests(); opaque = OriginalOpaqueMetadataTests()
+ oracles = [(status, lambda value: mixed.check(expected, value)),
+ (readonly.failure(), lambda value: outcome.readonly_failure(value, readonly.bucket, readonly.key, readonly.options)),
+ (opaque.status(), lambda value: outcome.opaque_metadata_outcome(value, opaque.bucket, opaque.key, opaque.options, 'original-anchor'))]
+ for source, check in oracles:
+ source.pop('truncated'); check(source)
+ for value in (None, True, 0, 'false'):
+ with self.subTest(value=value), self.assertRaises(ValueError): check({**source, 'truncated': value})
+ for field, value in (('objectsTruncated', True), ('coverage', 'partial')):
+ changed = copy.deepcopy(source); changed['outcome'][field] = value
+ with self.assertRaises(ValueError): check(changed)
+ changed = copy.deepcopy(source); changed['outcome'].pop('objectsTruncated')
+ with self.assertRaises(ValueError): check(changed)
+ changed = copy.deepcopy(source); changed['outcome']['objects'].pop()
+ with self.assertRaises(ValueError): check(changed)
+
+ def test_repeated_original_token_accepts_only_omitted_false_or_explicit_false(self):
+ status = ReadonlySourceFlowTests().failure(); status.pop('truncated')
+ response = dict(httpStatus=200, response=copy.deepcopy(status))
+ probe = types.SimpleNamespace(admin=lambda *args: response)
+ fleet = types.SimpleNamespace(record=lambda label, value: value)
+ admission = dict(path='/heal', clientToken='original')
+ outcome.unchanged_token(probe, fleet, admission, status, 'original')
+ for value in (None, True):
+ response['response']['truncated'] = value
+ with self.assertRaises(ValueError): outcome.unchanged_token(probe, fleet, admission, status, 'changed')
+
+
+if __name__ == "__main__": unittest.main()
diff --git a/test_validation_heal_outcome_grace.py b/test_validation_heal_outcome_grace.py
new file mode 100644
index 00000000..38be2d41
--- /dev/null
+++ b/test_validation_heal_outcome_grace.py
@@ -0,0 +1,608 @@
+"""Exercise real grace oracles and source-procedure ordering without a live fleet."""
+
+import copy
+import hashlib
+import json
+import os
+from datetime import datetime, timezone
+from pathlib import Path
+import tempfile
+import subprocess
+import types
+import unittest
+import uuid
+from contextlib import ExitStack
+from unittest.mock import patch
+
+from scripts.validation_heal import fleet_node, healthy, outcome, outcome_grace as grace
+from scripts.validation_heal.healthy import BASE_OPTIONS, physical_content
+from test_validation_heal import versioned_scene
+import test_validation_heal_orphan as orphan_tests
+
+
+BUCKET = 'rv-heal-0123456789abcdef'
+INCARNATION = str(uuid.UUID(int=30))
+OLD, CURRENT = str(uuid.UUID(int=21)), str(uuid.UUID(int=22))
+OPTIONS = {**BASE_OPTIONS, 'recursive': True, 'remove': True}
+
+
+def terminal(*, cleanup=False, healthy_only=False):
+ old_state = {'state': 'repaired' if cleanup else 'verified_healthy'} if cleanup or healthy_only else {
+ 'state': 'deferred', 'details': {'reason': 'dangling_delete_grace',
+ 'retryNotBefore': {'secs_since_epoch': 1120, 'nanos_since_epoch': 0}}}
+ return {'summary': 'finished', 'settings': OPTIONS, 'truncated': False, 'outcome': {
+ 'execution': {'state': 'completed'}, 'coverage': 'complete', 'objectsTruncated': False,
+ 'counters': dict(processed=2, healed=int(cleanup), unchanged=2 if healthy_only else 1,
+ skipped=int(not cleanup and not healthy_only), failed=0, unknown=0,
+ attemptFailures=int(not cleanup and not healthy_only), overflowed=False),
+ 'objects': [{'identity': dict(kind='object', bucket=BUCKET, object=grace.KEY, versionId=version,
+ bucketIncarnationId=INCARNATION, poolIndex=0, setIndex=0), 'disposition': disposition}
+ for version, disposition in ((OLD, old_state), (CURRENT, {'state': 'verified_healthy'}))]}}
+
+
+def scene(nodes=1):
+ initial, bucket, entries = versioned_scene([grace.KEY, 'control.bin'])
+ for entry in initial['node1']['files']:
+ if not entry['path'].startswith(bucket + '/' + grace.KEY + '/'): continue
+ if 'metadata' in entry:
+ versions = entry['metadata']['versions'][:2]
+ for index, version in enumerate(versions):
+ version.update(versionId=(OLD, CURRENT)[index], isLatest=index == 1)
+ entry['metadata']['versions'] = versions
+ old_dir = next(entry['metadata']['versions'][0]['dataDir'] for entry in initial['node1']['files']
+ if entry['path'] == bucket + '/' + grace.KEY + '/xl.meta')
+ unused = '12345678-1234-1234-1234-000000000002'
+ initial['node1']['files'] = [entry for entry in initial['node1']['files']
+ if not entry['path'].startswith(bucket + '/' + grace.KEY + '/' + unused + '/')]
+ for index in range(2, nodes + 1): initial['node' + str(index)] = copy.deepcopy(initial['node1'])
+ authority = copy.deepcopy(initial)
+ for value in authority.values():
+ value['files'] = [entry for entry in value['files']
+ if not entry['path'].startswith(bucket + '/' + grace.KEY + '/' + old_dir + '/')]
+ for entry in value['files']:
+ if entry['path'] == bucket + '/' + grace.KEY + '/xl.meta':
+ entry['metadata']['versions'] = entry['metadata']['versions'][1:]
+ entry['sha256'] = 'e' * 64
+ target = ('node' + str(nodes), 0)
+ injected = copy.deepcopy(authority)
+ prefix = bucket + '/' + grace.KEY + '/'
+ injected[target[0]]['files'] = [entry for entry in injected[target[0]]['files']
+ if not (entry['disk'] == 0 and entry['path'].startswith(prefix))]
+ injected[target[0]]['files'] += [copy.deepcopy(entry) for entry in initial[target[0]]['files']
+ if entry['disk'] == 0 and entry['path'].startswith(prefix)]
+ return initial, authority, injected, target, old_dir
+
+
+class GraceOracleTest(unittest.TestCase):
+ old = {'versionId': OLD, 'lastModifiedEpoch': 1000.0}
+ current = {'versionId': CURRENT}
+
+ def check(self, value, observed=1010, cleanup=False):
+ return grace.grace_outcome(value, BUCKET, self.old, self.current, OPTIONS, INCARNATION, observed, cleanup=cleanup)
+
+ def test_exact_future_deferral_and_distinct_post_expiry_repair(self):
+ self.assertEqual(self.check(terminal())['retryNotBeforeEpoch'], 1120)
+ self.assertTrue(self.check(terminal(cleanup=True), 1120, True)['newTaskProductCleanup'])
+ with self.assertRaises(ValueError): self.check(terminal(cleanup=True), 1119.999, True)
+ with self.assertRaises(ValueError): self.check(terminal(), 1120)
+
+ def test_source_omits_false_public_truncation_without_losing_canonical_coverage(self):
+ status = terminal(); status.pop('truncated')
+ self.assertEqual(self.check(status)['retryNotBeforeEpoch'], 1120)
+ for value in (None, True, 0, 'false'):
+ with self.subTest(value=value), self.assertRaises(ValueError): self.check({**status, 'truncated': value})
+ for value in (None, True):
+ changed = copy.deepcopy(status)
+ if value is None: changed['outcome'].pop('objectsTruncated')
+ else: changed['outcome']['objectsTruncated'] = value
+ with self.assertRaises(ValueError): self.check(changed)
+
+ def test_wrong_truth_and_legacy_counts_cannot_replace_original_responsibility(self):
+ for path, values in (
+ (('summary',), ['running', 'stopped']), (('truncated',), [True, None]),
+ (('outcome', 'coverage'), ['partial', None]),
+ (('outcome', 'execution'), [{'state': 'completed_with_errors'}]),
+ (('outcome', 'objectsTruncated'), [True, None]),
+ (('settings',), [{**OPTIONS, 'remove': False}])):
+ for value in values:
+ changed = terminal(); parent = changed
+ for name in path[:-1]: parent = parent[name]
+ parent[path[-1]] = value
+ with self.subTest(path=path, value=value), self.assertRaises(ValueError): self.check(changed)
+ for name, original in terminal()['outcome']['counters'].items():
+ for value in (None, not original if type(original) is bool else original + 1,
+ 0 if type(original) is bool else bool(original)):
+ changed = terminal(); changed['outcome']['counters'][name] = value
+ with self.subTest(counter=name, value=value), self.assertRaises(ValueError): self.check(changed)
+ changed = terminal(); changed['outcome']['counters']['legacyHealed'] = 88
+ with self.assertRaises(ValueError): self.check(changed)
+
+ def test_every_identity_boundary_and_duplicate_version_is_rejected(self):
+ for name, values in {'kind': ['bucket'], 'bucket': ['foreign'], 'object': ['other.bin'],
+ 'versionId': [None, CURRENT], 'bucketIncarnationId': [None, 'new'],
+ 'poolIndex': [True, 1], 'setIndex': [False, 1]}.items():
+ for value in values:
+ changed = terminal(); changed['outcome']['objects'][0]['identity'][name] = value
+ with self.subTest(name=name, value=value), self.assertRaises(ValueError): self.check(changed)
+ for items in ([], terminal()['outcome']['objects'][:1], terminal()['outcome']['objects'] * 2):
+ changed = terminal(); changed['outcome']['objects'] = items
+ with self.assertRaises(ValueError): self.check(changed)
+
+ def test_exact_retry_wire_shape_bounds_and_original_age_are_required(self):
+ for value in (None, 1120, {}, {'secs_since_epoch': True, 'nanos_since_epoch': 0},
+ {'secs_since_epoch': -1, 'nanos_since_epoch': 0},
+ {'secs_since_epoch': 2**64, 'nanos_since_epoch': 0},
+ {'secs_since_epoch': 1120, 'nanos_since_epoch': -1},
+ {'secs_since_epoch': 1120, 'nanos_since_epoch': 10**9},
+ {'secs_since_epoch': 1120, 'nanos_since_epoch': False},
+ {'secs_since_epoch': 1120, 'nanos_since_epoch': 0, 'extra': 0},
+ {'secs_since_epoch': 1126, 'nanos_since_epoch': 0}):
+ changed = terminal(); changed['outcome']['objects'][0]['disposition']['details']['retryNotBefore'] = value
+ with self.subTest(value=value), self.assertRaises(ValueError): self.check(changed)
+ for details in ({'reason': 'dangling_delete_grace', 'retry_not_before': None},
+ {'reason': 'transient_existence_check', 'retryNotBefore': None}):
+ changed = terminal(); changed['outcome']['objects'][0]['disposition']['details'] = details
+ with self.assertRaises(ValueError): self.check(changed)
+ for value in (True, -1, float('inf'), float('nan')):
+ with self.assertRaises(ValueError): self.check(terminal(), value)
+
+ def test_false_health_unknown_skip_or_repair_cannot_certify_deferral(self):
+ for index in (0, 1):
+ for state in ('unknown', 'skipped', 'failed', 'repaired', 'metadata_healthy'):
+ changed = terminal(); changed['outcome']['objects'][index]['disposition'] = {'state': state}
+ with self.subTest(index=index, state=state), self.assertRaises(ValueError): self.check(changed)
+
+ def test_all_original_members_and_current_logical_metadata_remain_exact(self):
+ for count in (1, 4):
+ initial, authority, injected, target, directory = scene(count)
+ proof = grace.grace_fixture(initial, authority, injected, BUCKET, self.old, self.current, target, directory)
+ self.assertEqual(proof['allOriginalMembers'], count * 4)
+ for field, value in (('dataDir', str(uuid.UUID(int=99))), ('deleted', True), ('isLatest', False), ('size', 999)):
+ changed = copy.deepcopy(authority)
+ meta = next(entry for entry in changed['node1']['files']
+ if entry['path'] == BUCKET + '/' + grace.KEY + '/xl.meta')
+ meta['metadata']['versions'][0][field] = value
+ with self.subTest(nodes=count, field=field), self.assertRaises(ValueError):
+ grace.grace_fixture(initial, changed, None, BUCKET, self.old, self.current, target, directory)
+ for source, label in ((authority, 'authority'), (injected, 'injected')):
+ for kind in ('control', 'format', 'extra-old'):
+ changed = copy.deepcopy(source)
+ if kind == 'control':
+ next(entry for entry in changed['node1']['files'] if '/control.bin/' in entry['path'])['sha256'] = 'b' * 64
+ elif kind == 'format': changed['node1']['formats'][0]['sha256'] = 'b' * 64
+ else:
+ extra = next(entry for entry in initial['node1']['files'] if '/' + directory + '/' in entry['path'])
+ # An additional replay on a different disk is forbidden, including H1.
+ extra = copy.deepcopy(extra); extra['disk'] = 1
+ changed['node1']['files'].append(extra)
+ with self.subTest(nodes=count, source=label, poison=kind), self.assertRaises(ValueError):
+ grace.grace_fixture(initial, changed if label == 'authority' else authority,
+ changed if label == 'injected' else None, BUCKET, self.old, self.current, target, directory)
+
+
+class GraceRuntimeTest(unittest.TestCase):
+ def identity(self):
+ return {**{name: name for name in fleet_node.PROCESS_FIELDS}, 'pid': 123,
+ 'binarySha256': 'a' * 64, 'settings': fleet_node.runtime_config('outcome-grace')}
+
+ def test_owned_clock_is_read_only_fixed_policy_and_process_bound(self):
+ identity = self.identity()
+ request = {'runId': 'gh-123-1-h1', 'action': 'process-clock'}
+ with patch.object(fleet_node, 'owned', return_value=(None, None, {'runtimeProfile': 'outcome-grace'})), \
+ patch.object(fleet_node, 'process', return_value=identity), patch.object(fleet_node.time, 'time', return_value=1000):
+ witness = fleet_node.process_clock(request)
+ self.assertEqual(witness, dict(identity=identity, nodeEpoch=1000, readOnly=True, clockChanged=False))
+ for name in ('pid', 'clock', 'path'):
+ with self.assertRaises(ValueError): fleet_node.process_clock({**request, name: 1})
+ for name in fleet_node.PROCESS_FIELDS:
+ changed = copy.deepcopy(identity); changed[name] = 'changed'
+ with patch.object(fleet_node, 'owned', return_value=(None, None, {'runtimeProfile': 'outcome-grace'})), \
+ patch.object(fleet_node, 'process', side_effect=[identity, changed]), self.subTest(field=name), self.assertRaises(ValueError):
+ fleet_node.process_clock(request)
+ for profile in ('manual', 'orphan-before'):
+ with patch.object(fleet_node, 'owned', return_value=(None, None, {'runtimeProfile': profile})), self.assertRaises(ValueError):
+ fleet_node.process_clock(request)
+ for changes in ({'pid': 0}, {'pid': True}, {'settings': {'RUSTFS_HEAL_DANGLING_DELETE_GRACE_SECS': '0'}}):
+ with patch.object(fleet_node, 'owned', return_value=(None, None, {'runtimeProfile': 'outcome-grace'})), \
+ patch.object(fleet_node, 'process', return_value={**identity, **changes}), self.assertRaises(ValueError):
+ fleet_node.process_clock(request)
+
+ def test_grace_descriptor_reuses_last_ack_authority_without_reviving_old(self):
+ helper = orphan_tests.OrphanDescriptorTests()
+ helper.setUp(); self.addCleanup(helper.tearDown)
+ helper.owner['runtimeProfile'] = 'outcome-grace'
+ helper.authority()
+ helper.invoke('install', mode='object', dataDir=helper.old_dir)
+ self.assertTrue((helper.object / helper.old_dir).is_dir())
+ self.assertTrue(helper.recover()['lastACKAuthorityRestored'])
+ self.assertFalse((helper.object / helper.old_dir).exists())
+ self.assertEqual((helper.object / 'xl.meta').read_bytes(), b'XL2 live')
+ for field, value in (('key', '../escape'), ('disk', True), ('disk', 4), ('bucket', 'foreign')):
+ with self.subTest(field=field, value=value), self.assertRaises(ValueError):
+ helper.invoke('capture', **{field: value}, generation='authority', expectedFiles=[], expectedPresent=False)
+
+ def test_grace_runtime_cannot_access_other_orphan_mutators(self):
+ owner = {'runtimeProfile': 'outcome-grace', 'binarySha256': 'a' * 64}
+ with patch.object(fleet_node, 'owned', return_value=(Path('/owned'), (), owner)), \
+ patch.object(fleet_node, 'stopped'), patch.object(fleet_node, 'digest', return_value='a' * 64), \
+ patch.object(fleet_node, 'fault_module'), \
+ patch.dict(fleet_node.__dict__, generation_backend=object(), orphan_backend=object()):
+ for action in ('orphan-convert', 'orphan-install-tool', 'orphan-disarm', 'orphan-multipart'):
+ with self.subTest(action=action), self.assertRaises(ValueError):
+ fleet_node.orphan_operation({'runId': 'gh-123-1-h1', 'action': action})
+
+ def test_controller_rejects_clock_skew_policy_change_and_process_replacement(self):
+ identity = self.identity()
+ original = dict(identity=identity, nodeEpoch=1000, readOnly=True, clockChanged=False)
+ fleet = types.SimpleNamespace(nodes=['node1'], record=lambda label, value: value,
+ remote=lambda *a: original)
+ binding = {'candidateBinarySha256': 'a' * 64}
+ with patch.object(grace, 'time', types.SimpleNamespace(time=lambda: 1000)):
+ previous = grace.clocks(fleet, binding, 'before')
+ grace.clocks(fleet, binding, 'after', previous)
+ for name, value in (('nodeEpoch', 1004), ('nodeEpoch', float('nan')),
+ ('nodeEpoch', True), ('readOnly', False), ('clockChanged', True)):
+ changed = {**original, name: value}; fleet.remote = lambda *a: changed
+ with self.subTest(name=name, value=value), self.assertRaises(ValueError):
+ grace.clocks(fleet, binding, 'poison')
+ for name in fleet_node.PROCESS_FIELDS:
+ changed = {**original, 'identity': {**identity, name: 'changed'}}
+ fleet.remote = lambda *a: changed
+ with self.subTest(field=name), self.assertRaises(ValueError):
+ grace.clocks(fleet, binding, 'replaced', previous)
+
+ def test_remote_adapter_contains_registered_clock_and_descriptor_backends(self):
+ with tempfile.TemporaryDirectory() as temporary:
+ fleet = healthy.Fleet(['node1'], Path(temporary), 'gh-123-1-h1', runtime_profile='outcome-grace')
+ for action in ('process-clock', 'orphan-observe', 'orphan-capture', 'orphan-arm', 'orphan-install', 'orphan-recover'):
+ with patch.object(healthy.subprocess, 'run', return_value=types.SimpleNamespace(returncode=0, stdout='{}', stderr='')) as remote:
+ fleet.remote('node1', action)
+ source = remote.call_args.kwargs['input']
+ compile(source, '', 'exec')
+ self.assertIn('actions["process-clock"] = process_clock', source)
+ if action.startswith('orphan-'):
+ self.assertIn('orphan_backend = types.ModuleType', source)
+ self.assertIn('generation_backend = types.ModuleType', source)
+
+
+class GraceIntegrationTest(unittest.TestCase):
+ def test_uncertain_original_grace_procedure_retains_namespace_and_stops_next_branch(self):
+ with tempfile.TemporaryDirectory() as temporary:
+ root = Path(temporary); rc = root / 'rc'; rc.write_bytes(b'cli')
+ args = types.SimpleNamespace(evidence=root / 'evidence', rc_bin=rc, binary_sha='b' * 64,
+ testing_commit='a' * 40, tool_directory=root / 'tools')
+ with ExitStack() as stack:
+ factory = stack.enter_context(patch.object(outcome, 'Fleet'))
+ fleet = factory.return_value
+ fleet.reserved = ['node1']; fleet.record.side_effect = lambda label, value: value
+ stack.enter_context(patch.dict(os.environ, GITHUB_RUN_ID='12345', GITHUB_RUN_ATTEMPT='1'))
+ for name in ('Probe', 'Recorder', 'verify_inspector', 'verify_quorum_contract', 'validate_topology', 'verify_seed', 'idle_fleet', 'run_task'):
+ stack.enter_context(patch.object(outcome, name))
+ stack.enter_context(patch.object(outcome, 'seed', return_value=[]))
+ stack.enter_context(patch.object(outcome, 'validate_version_receipts', return_value={'bucketIncarnationId': INCARNATION}))
+ stack.enter_context(patch.object(outcome, 'original_grace_probe', side_effect=TimeoutError('lost replay ACK')))
+ result = outcome.run_branch(args, 'H1', ['node1'], 'DB', 'deferred',
+ {'source_sha': outcome.CANDIDATE, 'package_sha256': 'c' * 64})
+ self.assertEqual(factory.call_args.kwargs['runtime_profile'], 'outcome-grace')
+ self.assertEqual((result['verdict'], result['cleanup']), ('FAIL', 'FAIL'))
+ fleet.cleanup.assert_not_called()
+ retained = next(call.args[1] for call in fleet.record.call_args_list if call.args[0] == 'retain-unknown-seed-receipt')
+ self.assertEqual(retained['request']['operation'], 'original-version-specific-grace-source-procedure')
+ self.assertTrue(retained['namespacesRetained'])
+ self.assertGreaterEqual(fleet.stop.call_count, 2)
+
+ def test_completed_source_grace_does_not_run_a_second_non_source_fault(self):
+ for verdict in ('PASS', 'NOT_OBSERVED', 'FAIL', 'incomplete'):
+ with self.subTest(verdict=verdict), tempfile.TemporaryDirectory() as temporary, ExitStack() as stack:
+ root = Path(temporary); rc = root / 'rc'; rc.write_bytes(b'cli')
+ args = types.SimpleNamespace(evidence=root / 'evidence', rc_bin=rc, binary_sha='b' * 64,
+ testing_commit='a' * 40, tool_directory=root / 'tools')
+ factory = stack.enter_context(patch.object(outcome, 'Fleet')); fleet = factory.return_value
+ fleet.reserved = ['node1']; fleet.record.side_effect = lambda label, value: value
+ fleet.cleanup.return_value = True
+ stack.enter_context(patch.dict(os.environ, GITHUB_RUN_ID='12345', GITHUB_RUN_ATTEMPT='1'))
+ for name in ('Probe', 'Recorder', 'verify_inspector', 'verify_quorum_contract', 'validate_topology',
+ 'verify_seed', 'idle_fleet', 'run_task'):
+ stack.enter_context(patch.object(outcome, name))
+ stack.enter_context(patch.object(outcome, 'seed', return_value=[]))
+ stack.enter_context(patch.object(outcome, 'validate_version_receipts', return_value={'bucketIncarnationId': INCARNATION}))
+ source = dict(originalGraceSourceProcedure=True, verdict=verdict, sourceDiagnosticComplete=verdict != 'incomplete', fixtureRescueIsProductRepair=False,
+ sameTokenRecoveryClaimed=False, globalClockChanged=False, metadataAgeRewritten=False, canonicalPassClaimed=False)
+ stack.enter_context(patch.object(outcome, 'original_grace_probe', return_value=([], {}, source)))
+ supplemental = stack.enter_context(patch.object(outcome, 'fault_plan', side_effect=AssertionError('unexpected extra fault')))
+ result = outcome.run_branch(args, 'H1', ['node1'], 'DB', 'deferred',
+ {'source_sha': outcome.CANDIDATE, 'package_sha256': 'c' * 64})
+ supplemental.assert_not_called()
+ if verdict in ('PASS', 'NOT_OBSERVED'):
+ self.assertEqual((result['verdict'], result['cleanup']), (verdict, 'PASS'))
+ self.assertEqual(result['sourceProcedure'], 'heal_status_batch.Batch.deferred')
+ fleet.cleanup.assert_called_once()
+ else:
+ self.assertEqual((result['verdict'], result['cleanup']), ('FAIL', 'FAIL'))
+ fleet.cleanup.assert_not_called()
+ self.assertFalse(any(call.args[0] == 'healthy-physical-baseline' for call in fleet.snapshot.call_args_list))
+
+ def test_manual_diagnostic_entry_cannot_certify_canonical_even_if_driver_returns_zero(self):
+ root = Path(__file__).resolve().parent
+ dry = subprocess.run(['bash', str(root / 'scripts/validation_heal/run_case.sh'), '--validation-case',
+ outcome.CASE_ID, '--dry-run'], capture_output=True, text=True)
+ self.assertEqual(dry.returncode, 0, dry.stderr)
+ shell = (root / 'scripts/validation_heal/run_case.sh').read_text()
+ verdict = shell[shell.index('verdict=FAIL reason='):shell.index('args=("${ROOT}/scripts/validation_result.py"')]
+ with tempfile.TemporaryDirectory() as temporary:
+ evidence = Path(temporary)
+ for code, all_pass, cleanup in ((0, True, 'PASS'), (1, True, 'PASS'), (1, False, 'PASS'), (0, True, 'FAIL')):
+ (evidence / 'healthy-summary.json').write_text(json.dumps(dict(diagnosticBranchesPassed=all_pass,
+ complete=False, canonicalCasePassClaimed=False)))
+ script = verdict + '\nprintf "%s" "$verdict"\n'
+ checked = subprocess.run(['bash', '-euc', script], capture_output=True, text=True,
+ env={**os.environ, 'case_id': outcome.CASE_ID, 'cleanup_status': cleanup,
+ 'case_rc': str(code), 'evidence': str(evidence)})
+ self.assertEqual(checked.returncode, 0, checked.stderr)
+ self.assertEqual(checked.stdout, 'BLOCKED' if cleanup == 'PASS' and (code == 0 or all_pass)
+ else 'INFRA_ERROR' if code == 0 else 'FAIL')
+
+
+class GraceProtocolTest(unittest.TestCase):
+ def execute(self, *, nodes=1, mode='normal'):
+ initial, authority, injected, target, directory = scene(nodes)
+ clock = types.SimpleNamespace(now=1000.0, elapsed=0.0)
+ events, records, admissions = [], {}, []
+ ack_entries = []
+ def sleep(seconds):
+ self.assertLessEqual(seconds, 1)
+ if mode != 'stalled-clock': clock.now += seconds
+ clock.elapsed += seconds; events.append(('wait', seconds))
+ timer = types.SimpleNamespace(time=lambda: clock.now, monotonic=lambda: clock.elapsed, sleep=sleep)
+ identity = GraceRuntimeTest().identity()
+ class Fleet:
+ def __init__(self): self.nodes = list(initial); self.orphan_faults = []; self.phase = 'initial'
+ def start(self): events.append(('start', clock.now))
+ def stop(self): events.append(('stop', clock.now))
+ def record(self, label, value): records[label] = copy.deepcopy(value); return value
+ def remote(self, node, action, **params):
+ events.append((action, node))
+ if action == 'orphan-observe':
+ snapshot = initial if self.phase == 'initial' else authority
+ return dict(present=True, files=grace.object_files(snapshot, BUCKET, grace.KEY)[target], nodeEpoch=clock.now)
+ if action == 'orphan-install':
+ if mode == 'miss-admission': clock.now = 1111
+ self.phase = 'fault'
+ if action == 'process-clock':
+ actual = {**identity, 'pid': 124} if mode == 'replacement-after-compensation' and len(tasks) >= 3 else identity
+ return dict(identity=actual, nodeEpoch=clock.now, readOnly=True, clockChanged=False)
+ return {}
+ def snapshot(self, label, **kwargs):
+ records[label] = self.phase
+ return copy.deepcopy(initial if self.phase == 'initial' else authority if self.phase in ('authority', 'clean') else injected)
+ fleet = Fleet()
+ class Probe:
+ def inventory(self, *args, **kwargs): return {'bucket': BUCKET, 'entries': copy.deepcopy(ack_entries)}
+ def s3(self, operation, params, body=None):
+ self_outer.assertEqual(operation, 'head_object')
+ return dict(acknowledged=True, resultUnknown=False, response=dict(VersionId=params['VersionId'],
+ ContentLength=grace.SIZE, ETag='etag', LastModified=datetime.fromtimestamp(1000, timezone.utc)))
+ self_outer = self
+ class Recorder:
+ def __init__(self, *args): self.versions = iter((OLD, CURRENT))
+ def call(self, operation, params, body=None):
+ events.append((operation, clock.now))
+ if operation == 'put_object':
+ version = next(self.versions)
+ for entry in ack_entries: entry['isLatest'] = False
+ ack_entries.append(dict(key=grace.KEY, versionId=version, isLatest=True, deleteMarker=False,
+ payload=dict(size=len(body), sha256=hashlib.sha256(body).hexdigest())))
+ return dict(VersionId=version, ETag='etag')
+ if operation == 'delete_object':
+ fleet.phase = 'authority'
+ ack_entries[:] = [entry for entry in ack_entries if entry['versionId'] != OLD]
+ return dict(VersionId=OLD, ResponseMetadata={'HTTPStatusCode': 204})
+ return {}
+ tasks = []
+ def task(*args, **kwargs):
+ tasks.append(clock.now); events.append(('task', clock.now))
+ if len(tasks) == 1: return terminal(healthy_only=True)
+ token = 'same' if mode == 'reused-token' else 'token-' + str(len(tasks))
+ admission = {'clientToken': token}; kwargs['admissions'].append(admission); admissions.append(admission)
+ if len(tasks) == 2:
+ if mode == 'late-original': clock.now = 1121
+ if mode == 'late-cleanup': clock.now = 1121; fleet.phase = 'clean'; return terminal(cleanup=True)
+ if mode == 'partial-original':
+ fleet.phase = 'clean'
+ raise ValueError('partial outcome')
+ return terminal()
+ if mode != 'false-cleanup': fleet.phase = 'clean'
+ return terminal(cleanup=True)
+ def preserve(*args): events.append(('preserve', clock.now))
+ def verify_public(*args):
+ self.assertEqual(fleet.phase, 'clean'); events.append(('public', clock.now))
+ def recover(*args):
+ events.append(('rescue', clock.now)); fleet.phase = 'authority'; fleet.orphan_faults.clear()
+ with tempfile.TemporaryDirectory() as temporary, ExitStack() as stack:
+ for name, value in {'time': timer, 'Recorder': Recorder,
+ 'run_task': task, 'idle_fleet': lambda *a: None, 'verify_proof': lambda *a: {},
+ 'map_target': lambda *a, **kw: {'dataDir': directory}}.items():
+ stack.enter_context(patch.object(grace, name, value))
+ stack.enter_context(patch.object(grace.orphan, 'verify_public', verify_public))
+ stack.enter_context(patch.object(grace.orphan, 'recover_all', recover))
+ self.events, self.fleet = events, fleet
+ result = grace.run(Probe(), fleet, dict(bucketPrefix=BUCKET, candidateBinarySha256='a' * 64,
+ endpoints={node: 'http://' + node for node in fleet.nodes}), Path(temporary), [], INCARNATION, [], preserve)
+ return result, records, tasks, events
+
+ def test_natural_expiry_new_token_and_product_cleanup_precede_get_and_rescue(self):
+ for nodes in (1, 4):
+ result, records, tasks, events = self.execute(nodes=nodes)
+ self.assertEqual(result[2]['verdict'], 'PASS')
+ self.assertFalse(result[2]['canonicalPassClaimed'])
+ self.assertEqual(result[2]['physicalFixture']['allOriginalMembers'], 4 * nodes)
+ self.assertGreaterEqual(tasks[2], 1125)
+ kinds = [event[0] for event in events]
+ self.assertLess(kinds.index('public'), kinds.index('rescue'))
+ self.assertEqual(kinds.count('preserve'), 2)
+ # No service restart across original, wait, compensation, public checks or token rechecks.
+ first, last = kinds.index('wait'), kinds.index('public')
+ self.assertFalse(set(kinds[first:last + 1]) & {'start', 'stop'})
+ self.assertEqual(records['J02-grace-product-cleaned-before-any-GET-or-rescue'], 'clean')
+ self.assertEqual(self.fleet.orphan_faults, [])
+
+ def test_missed_admission_or_terminal_window_remains_not_observed(self):
+ for mode in ('miss-admission', 'late-original', 'late-cleanup'):
+ result, _, tasks, events = self.execute(mode=mode)
+ self.assertEqual(result[2]['verdict'], 'NOT_OBSERVED')
+ self.assertEqual(len(tasks), 1 if mode == 'miss-admission' else 2)
+ self.assertNotIn('public', [event[0] for event in events])
+ self.assertEqual(self.fleet.orphan_faults, [])
+
+ def test_reused_token_false_cleanup_and_partial_truth_never_rescue_or_pass(self):
+ for mode in ('reused-token', 'false-cleanup', 'partial-original', 'stalled-clock', 'replacement-after-compensation'):
+ with self.subTest(mode=mode), self.assertRaises(ValueError): self.execute(mode=mode)
+ self.assertNotIn('rescue', [event[0] for event in self.events])
+ self.assertNotIn('public', [event[0] for event in self.events])
+ self.assertEqual(len(self.fleet.orphan_faults), 1)
+
+
+
+
+class GraceRecoveryTest(unittest.TestCase):
+ def fixture(self, root):
+ from scripts.validation_heal import outcome_recovery as recovery
+ keys = ['objects/0000.bin', 'objects/0001.bin', 'objects/0002.bin', grace.KEY]
+ original, bucket, _ = versioned_scene(keys)
+ value = original.pop('node1'); original['vm000'] = value
+ expected = []
+ for key in keys:
+ ids = (OLD, CURRENT) if key == grace.KEY else ('null',)
+ expected += [dict(key=key, versionId=version, isLatest=index == len(ids) - 1,
+ deleteMarker=False, payload=dict(size=100, sha256='d' * 64)) for index, version in enumerate(ids)]
+ for entry in value['files']:
+ if entry['path'] == bucket + '/' + key + '/xl.meta':
+ entry['metadata']['versions'] = entry['metadata']['versions'][:len(ids)]
+ for index, version in enumerate(entry['metadata']['versions']):
+ version.update(versionId=ids[index], isLatest=index == len(ids) - 1)
+ candidate = dict(source_sha=recovery.CANDIDATE, package_sha256='a' * 64)
+ run_id = 'gh-12345-1-h1'; case = outcome.CASE_ID
+ folder = root / 'artifact/run' / case
+ evidence = folder / 'evidence' / case; branch = evidence / 'DB/H1/deferred'
+ branch.mkdir(parents=True); (folder / 'cases').mkdir()
+ def save(path, data): path.write_text(json.dumps(data))
+ save(folder / 'cases' / (case + '.json'), dict(case_id=case, verdict='FAIL', cleanup_status='FAIL',
+ provenance=dict(source_commit=candidate['source_sha'], candidate_sha256='a' * 64,
+ testing_commit='b' * 40, binary_sha256='c' * 64)))
+ rows = [dict(topology='H1', fixture='DB', branch=name, verdict='FAIL', cleanup='PASS')
+ for name in ('mixed-healthy-repaired-failed', 'dry-run')]
+ rows.append(dict(topology='H1', fixture='DB', branch='deferred', verdict='FAIL', cleanup='FAIL',
+ failure=dict(type='FixtureStateMismatch', message='baseline content/version/marker differs from deterministic fixture ACK')))
+ save(evidence / 'healthy-summary.json', dict(caseId=case, results=rows, cleanup='FAIL', complete=False, canonicalCasePassClaimed=False))
+ owner = dict(runId=run_id, bucket=bucket, runtimeProfile='outcome-grace', binarySha256='c' * 64, mounts=[{}] * 4)
+ for index, (label, data) in enumerate((('binding', dict(runId=run_id, bucketPrefix=bucket,
+ harnessCommit='b' * 40, candidateCommit=candidate['source_sha'], candidatePackageSha256='a' * 64,
+ candidateBinarySha256='c' * 64)), ('failure-scene-before-fault-rescue', original),
+ ('reserved', dict(node='vm000', reserved=True, owner=owner)),
+ ('retain-unknown-seed-receipt', dict(state='DIRTY', namespacesRetained=True,
+ request=dict(operation='original-version-specific-grace-source-procedure', bucket=bucket))),
+ ('complete-ACK-manifest', dict(expected=expected[:3]))), 1):
+ save(branch / (f'{index:05d}-' + label + '.json'), data)
+ for name, operations in (('fixture', ['create_bucket'] + ['put_object'] * 3),
+ ('original-grace-fixture', ['put_bucket_versioning', 'put_object', 'put_object'])):
+ ledger = branch / name; ledger.mkdir()
+ for sequence, operation in enumerate(operations, 1):
+ params = {'Bucket': bucket}; payload = None; response = {'ResponseMetadata': {'HTTPStatusCode': 200}}
+ if operation == 'put_object':
+ entry = expected[sequence - 2 if name == 'fixture' else sequence + 1]
+ params['Key'] = entry['key']; payload = entry['payload']; response['VersionId'] = entry['versionId']
+ request = dict(sequence=sequence, runId=run_id, operation=operation, params=params)
+ if payload: request['payload'] = payload
+ path = ledger / f'{sequence:06d}-request.json'; save(path, request)
+ save(ledger / f'{sequence:06d}-ack.json', dict(sequence=sequence, runId=run_id,
+ requestFile=path.name, requestSha256=hashlib.sha256(path.read_bytes()).hexdigest(), operationCheck='PASS',
+ observation=dict(operation=operation, params=params, payload=payload, response=response,
+ acknowledged=True, clientCallInvoked=True, resultUnknown=False)))
+ return candidate, branch
+
+ def test_acknowledged_pre_replay_plan_and_poisoned_ledger_boundaries(self):
+ from scripts.validation_heal import outcome_recovery as recovery
+ for poison in (None, 'unknown', 'request-digest', 'extra-delete', 'later-grace', 'foreign-key', 'lost-root', 'another-retained-branch'):
+ with self.subTest(poison=poison), tempfile.TemporaryDirectory() as temporary:
+ root = Path(temporary); candidate, branch = self.fixture(root)
+ path = branch / 'original-grace-fixture/000003-ack.json'
+ ack = json.loads(path.read_text())
+ if poison == 'unknown': ack['observation']['resultUnknown'] = True
+ elif poison == 'request-digest': ack['requestSha256'] = 'e' * 64
+ elif poison == 'foreign-key': ack['observation']['params']['Key'] = 'foreign'
+ path.write_text(json.dumps(ack))
+ if poison == 'extra-delete': (path.parent / '000004-request.json').write_text('{}')
+ elif poison == 'later-grace': (branch / '00100-J02-grace-single-member-old-replay.json').write_text('{}')
+ elif poison == 'lost-root':
+ path = branch / '00002-failure-scene-before-fault-rescue.json'; scene = json.loads(path.read_text())
+ scene['vm000']['formats'].pop(); path.write_text(json.dumps(scene))
+ elif poison == 'another-retained-branch':
+ path = branch.parents[2] / 'healthy-summary.json'; summary = json.loads(path.read_text())
+ summary['results'][0]['cleanup'] = 'FAIL'; path.write_text(json.dumps(summary))
+ if poison:
+ with self.assertRaises(ValueError): recovery.acknowledged_grace_plan(root, 12345, candidate)
+ else:
+ plan = recovery.acknowledged_grace_plan(root, 12345, candidate)[0]
+ self.assertEqual(plan['nodes'], ['vm000'])
+ self.assertEqual(plan['owners']['vm000']['runtimeProfile'], 'outcome-grace')
+
+ def test_real_descriptor_archive_requires_complete_scene_and_retries_after_every_move(self):
+ from scripts.validation_heal import retained_scene as retained, outcome_recovery as recovery
+ import test_validation_heal_retained_scene as retained_tests
+ helper = retained_tests.RetainedSceneTests()
+ for lost_after in range(1, 6):
+ with self.subTest(lost_after=lost_after), tempfile.TemporaryDirectory() as temporary:
+ state, roots, node, request = helper.scene(temporary)
+ roots[0].mkdir(parents=True); owner = request['expectedOwner']; owner['runtimeProfile'] = 'outcome-grace'
+ for folder in (*roots, state): (folder / 'owner.json').write_text(json.dumps(owner))
+ snapshot = dict(files=[dict(path='original', sha256='d' * 64)], formats=[{}] * 4,
+ pid=0, quiescent=True, binarySha256=owner['binarySha256'])
+ node['snapshot'] = lambda value: snapshot
+ request['expectedOriginalSceneSha256'] = recovery.digest({key: snapshot[key] for key in ('files', 'formats')})
+ with helper.root_stats(), patch.object(retained, 'rename_new', side_effect=helper.local_rename):
+ baseline = retained.run(request, node)['baseline']
+ change = {**request, 'action': 'retained-scene-isolate', 'expectedBaseline': baseline}
+ count = 0
+ def move(source, target):
+ nonlocal count
+ helper.local_rename(source, target)
+ if target.is_dir():
+ count += 1
+ if count == lost_after: raise RuntimeError('lost rename ACK')
+ with patch.object(retained, 'rename_new', side_effect=move), self.assertRaises(RuntimeError): retained.run(change, node)
+ checked = retained.run(change, node)
+ self.assertEqual(checked['locations'], ['retained'] * 5)
+ self.assertEqual(checked['baseline'], baseline)
+ self.assertFalse(checked['canonicalPassClaimed'])
+ self.assertFalse(checked['backupRestorationUsed'])
+ self.assertFalse(checked['credentialContentsRead'])
+ with self.assertRaises(ValueError): retained.run({**request, 'expectedOriginalSceneSha256': 'e' * 64}, node)
+ def remote(host, action, **params):
+ return retained.run(dict(runId=request['runId'], action=action, **params), node)
+ fleet = types.SimpleNamespace(record=lambda label, value: value, remote=remote)
+ plan = dict(runId=request['runId'], before={'vm000': snapshot}, owners={'vm000': owner},
+ originalCaseSha256='d' * 64)
+ result = recovery.isolate_acknowledged_grace(fleet, plan, [dict(node='vm000',
+ retained={'state': True, 'roots': [True] * 4})])
+ self.assertEqual(result['cleanup'], 'ISOLATED')
+ self.assertFalse(result['originalBusinessFixtureRestored'])
+ self.assertFalse(result['originalVerdictRewritten'])
+
+ def test_drifted_acknowledged_scene_never_starts_archive(self):
+ from scripts.validation_heal import outcome_recovery as recovery
+ for poison in ('bytes', 'missing-root', 'live-pid'):
+ with self.subTest(poison=poison):
+ before = {'vm000': dict(files=[], formats=[], pid=0, quiescent=True, binarySha256='a' * 64)}
+ current = copy.deepcopy(before)
+ state = dict(node='vm000', state={'exists': True}, roots=[{'exists': True}] * 4, retained={})
+ if poison == 'bytes': current['vm000']['files'] = [{}]
+ elif poison == 'live-pid': current['vm000']['pid'] = 123
+ else: state['roots'] = [{'exists': False}] * 4
+ fleet = types.SimpleNamespace(record=lambda label, value: value,
+ remote=unittest.mock.Mock(), snapshot=lambda *a, **kw: current)
+ plan = dict(before=before, binarySha256='a' * 64)
+ with self.assertRaises(ValueError): recovery.isolate_acknowledged_grace(fleet, plan, [state])
+ self.assertFalse(any(call.args[1].startswith('retained-scene-') for call in fleet.remote.call_args_list))
+
+
+if __name__ == '__main__': unittest.main()
diff --git a/test_validation_heal_outcome_recovery.py b/test_validation_heal_outcome_recovery.py
new file mode 100644
index 00000000..2bb2e52e
--- /dev/null
+++ b/test_validation_heal_outcome_recovery.py
@@ -0,0 +1,276 @@
+"""Interrupted opaque probes restore only the two archived pre-task file faults."""
+import copy
+import hashlib
+import json
+from pathlib import Path
+import tempfile
+import types
+import unittest
+from unittest.mock import patch
+
+from scripts.validation_heal import fleet_node, heal_fault, interrupted_recovery, outcome_recovery, recover_environment, retained_scene
+from scripts.validation_heal.healthy import physical_content
+from scripts.validation_heal.shards import map_target
+from scripts.validation_heal.versions import metadata_target
+from test_validation_heal import versioned_scene
+import test_validation_heal_retained_scene as scene_tests
+
+
+def emit(path, value):
+ path.write_text(json.dumps(value))
+
+
+class OpaqueRecoveryTests(unittest.TestCase):
+ def fixture(self, root):
+ case = 'heal.outcome-counters-and-error-truth'
+ base = root / 'artifact/run' / case
+ folder = base / 'evidence' / case / 'DB/H1/unknown-negative'
+ folder.mkdir(parents=True); (base / 'cases').mkdir()
+ candidate = dict(source_sha=outcome_recovery.CANDIDATE, package_sha256='a' * 64)
+ provenance = dict(source_commit=candidate['source_sha'], candidate_sha256='a' * 64,
+ testing_commit='b' * 40, binary_sha256='c' * 64)
+ emit(base / 'cases' / (case + '.json'), dict(case_id=case, verdict='FAIL', cleanup_status='FAIL', provenance=provenance))
+ failure = dict(type='ValueError', message='Heal node vm000 snapshot failed: ValueError: metadata decoder failed outside an exact witnessed file fault\n')
+ branches = ('mixed-healthy-repaired-failed', 'dry-run', 'deferred', 'readonly-terminal-failure', 'unknown-negative')
+ rows = [dict(topology='H1', fixture='DB', branch=branch, verdict='FAIL',
+ cleanup='FAIL' if branch == branches[-1] else 'PASS', failure=failure) for branch in branches]
+ emit(base / 'evidence' / case / 'healthy-summary.json', dict(caseId=case, results=rows,
+ cleanup='FAIL', complete=False, canonicalCasePassClaimed=False))
+ original, bucket, expected = versioned_scene(['objects/0000.bin', 'opaque/source.bin'], sizes=(100,))
+ original['vm000'] = original.pop('node1')
+ original['vm000'].update(pid=0, quiescent=True, binarySha256='c' * 64)
+ for entry in expected:
+ entry.update(versionId='null', isLatest=True); entry['payload']['sha256'] = 'd' * 64
+ for entry in original['vm000']['files']:
+ if 'metadata' in entry:
+ entry['metadata']['versions'][0].update(versionId='null', isLatest=True)
+ entry['sha256'] = hashlib.sha256(str(entry['disk']).encode()).hexdigest()
+ run_id = 'gh-12345-1-h1'
+ owner = dict(runId=run_id, bucket=bucket, runtimeProfile='outcome-legacy', binarySha256='c' * 64, mounts=[{}] * 4)
+ labels = ['binding', 'inspector-build', 'preflight', 'integrity-fleet-attestation', 'reserved', 'inspector-upload',
+ 'started', 'deployed-topology', 'topology-proof', 'ACK-bound-full-S3', 'complete-ACK-manifest', 'trigger',
+ 'control', 'control-repeat', 'control-repeat', 'task-exit-background', 'quiescent', 'started',
+ 'original-opaque-ACK-full-S3', 'original-opaque-complete-ACK-manifest', 'trigger', 'control',
+ 'control-repeat', 'control-repeat', 'task-exit-background', 'quiescent', 'original-opaque-complete-physical',
+ *(['original-opaque-full-cohort-bitrot'] * 4), 'original-opaque-metadata-fault-plan',
+ 'original-opaque-byte-zero-bitflip', 'original-opaque-physical-injection', 'original-opaque-byte-zero-bitflip',
+ 'failure-scene-error', 'quiescent', 'retain-unknown-seed-receipt', 'branch-result']
+ data = {1: dict(runId=run_id, bucketPrefix=bucket, candidateCommit=candidate['source_sha'],
+ candidatePackageSha256='a' * 64, candidateBinarySha256='c' * 64, harnessCommit='b' * 40),
+ 5: dict(node='vm000', reserved=True, owner=owner), 19: dict(entries=expected),
+ 20: dict(expected=expected), 27: original,
+ 38: dict(state='DIRTY', namespacesRetained=True,
+ request=dict(operation='original-corrupt-metadata-source-probe', bucket=bucket))}
+ target = map_target(original, bucket, 'opaque/source.bin', 'data', 1, 'null', latest_required=False)
+ targets = [metadata_target(original, {**target, **member}) for member in target['cohort']]
+ data[32] = targets
+ first = copy.deepcopy(original)
+ for index, number in enumerate((33, 35)):
+ selected = targets[index]; fault_id = heal_fault.bounded_fault_id('J02', 'original-opaque', index)
+ receipt = dict(runId=run_id, faultId=fault_id, node='vm000', relativePath=selected['relativePath'], kind='bitflip',
+ offset=0, before={k: selected[k] for k in ('size', 'sha256')},
+ fault=dict(size=selected['size'], sha256=str(index + 4) * 64), backup=fault_id + '.original',
+ requiresQuiescentService=True, backend='logical-file-fault', witnessedAt='timestamp')
+ data[number] = receipt
+ if index == 0:
+ entry = next(e for e in first['vm000']['files'] if e['disk'] == selected['disk'] and e['path'] == selected['relativePath'])
+ entry.pop('metadata'); entry.update(receipt['fault'], metadataError=dict(code=1, message='Error: FileCorrupt\n',
+ exactWitnessedFault=True, validMetadataClaimed=False))
+ data[34] = first
+ for index, label in enumerate(labels, 1): emit(folder / f'{index:05d}-{label}.json', data.get(index, {}))
+ for name, operations in (('fixture', ('create_bucket', 'put_object')), ('original-opaque-fixture', ('put_object',))):
+ ledger = folder / name; ledger.mkdir()
+ for seq, operation in enumerate(operations, 1):
+ params = dict(Bucket=bucket); response = dict(ResponseMetadata=dict(HTTPStatusCode=200))
+ request = dict(sequence=seq, runId=run_id, operation=operation, params=params)
+ if operation == 'put_object':
+ entry = expected[0 if name == 'fixture' else 1]
+ params['Key'] = entry['key']; request['payload'] = entry['payload']; response['VersionId'] = 'null'
+ path = ledger / f'{seq:06d}-request.json'; emit(path, request)
+ emit(ledger / f'{seq:06d}-ack.json', dict(sequence=seq, runId=run_id, requestFile=path.name,
+ requestSha256=hashlib.sha256(path.read_bytes()).hexdigest(), operationCheck='PASS',
+ observation=dict(operation=operation, params=params, payload=request.get('payload'), response=response,
+ acknowledged=True, clientCallInvoked=True, resultUnknown=False)))
+ return candidate, folder
+
+ def test_plan_binds_each_disk_to_its_original_receipt_and_rejects_unknown_boundaries(self):
+ for poison in (None, 'candidate', 'unknown-ack', 'request-hash', 'extra-delete', 'extra-ledger', 'later-task',
+ 'task-directory', 'replaced-bitrot', 'fault-id', 'before-hash', 'fault-offset', 'first-witness', 'lost-format', 'prior-cleanup', 'owner'):
+ with self.subTest(poison=poison), tempfile.TemporaryDirectory() as temporary:
+ root = Path(temporary); candidate, folder = self.fixture(root)
+ def change(pattern, function):
+ path = next(folder.glob(pattern)); value = json.loads(path.read_text()); function(value); emit(path, value)
+ if poison == 'candidate': candidate['source_sha'] = 'e' * 40
+ elif poison == 'unknown-ack': change('original-opaque-fixture/*-ack.json', lambda x: x['observation'].update(resultUnknown=True))
+ elif poison == 'request-hash': change('fixture/*-ack.json', lambda x: x.update(requestSha256='f' * 64))
+ elif poison == 'extra-delete': emit(folder / 'fixture/000003-request.json', {})
+ elif poison == 'extra-ledger': emit(folder / 'fixture/unknown.json', {})
+ elif poison == 'later-task': emit(folder / '00040-trigger.json', {})
+ elif poison == 'task-directory': (folder / 'admin').mkdir()
+ elif poison == 'replaced-bitrot': next(folder.glob('00031-*')).rename(folder / '00031-trigger.json')
+ elif poison == 'fault-id': change('00035-*', lambda x: x.update(faultId=heal_fault.bounded_fault_id('J02', 'original-opaque', 0)))
+ elif poison == 'before-hash': change('00035-*', lambda x: x['before'].update(sha256='e' * 64))
+ elif poison == 'fault-offset': change('00035-*', lambda x: x.update(offset=1))
+ elif poison == 'first-witness': change('00034-*', lambda x: next(e for e in x['vm000']['files'] if 'metadataError' in e)['metadataError'].update(exactWitnessedFault=False))
+ elif poison == 'lost-format': change('00027-*', lambda x: x['vm000']['formats'].pop())
+ elif poison == 'owner': change('00005-*', lambda x: x['owner'].update(runtimeProfile='manual'))
+ elif poison == 'prior-cleanup':
+ path = folder.parents[2] / 'healthy-summary.json'; value = json.loads(path.read_text())
+ value['results'][0]['cleanup'] = 'FAIL'; emit(path, value)
+ if poison:
+ with self.assertRaises((ValueError, KeyError)): recover_environment.recovery_plan(root, 12345, candidate)
+ else:
+ plan = recover_environment.recovery_plan(root, 12345, candidate)[0]
+ self.assertEqual(plan['kind'], 'interrupted-j02-opaque')
+ self.assertEqual([f['disk'] for f in plan['faults']], [0, 1])
+ self.assertEqual(len(plan['metadataFaults']['vm000']), 2)
+ self.assertNotEqual(*[f['receipt']['fault']['sha256'] for f in plan['faults']])
+ for current in (plan['failure'], plan['before']): self.assertTrue(recover_environment.recovery_scene_matches(current, plan))
+ partial = copy.deepcopy(plan['failure']); key = plan['faults'][0]['receipt']['relativePath']
+ for entry in partial['vm000']['files']:
+ if entry['path'] == key and entry['disk'] == 0:
+ entry.clear(); entry.update(copy.deepcopy(next(e for e in plan['before']['vm000']['files'] if e['path'] == key and e['disk'] == 0)))
+ self.assertTrue(recover_environment.recovery_scene_matches(partial, plan))
+ for drift in ('control', 'unknown-meta', 'format', 'extra-file', 'lost-file'):
+ current = copy.deepcopy(partial)
+ if drift == 'control': next(e for e in current['vm000']['files'] if 'objects/0000.bin' in e['path'])['sha256'] = 'e' * 64
+ elif drift == 'unknown-meta': next(e for e in current['vm000']['files'] if 'metadataError' in e)['sha256'] = 'e' * 64
+ elif drift == 'format': current['vm000']['formats'].pop()
+ elif drift == 'extra-file': current['vm000']['files'].append(copy.deepcopy(current['vm000']['files'][0]))
+ elif drift == 'lost-file': current['vm000']['files'].pop()
+ with self.subTest(drift=drift): self.assertFalse(recover_environment.recovery_scene_matches(current, plan))
+
+ def test_restore_passes_disk_witnesses_and_exact_intent_then_archives_only_restored_bytes(self):
+ with tempfile.TemporaryDirectory() as temporary:
+ root = Path(temporary); candidate, _ = self.fixture(root)
+ plan = recover_environment.recovery_plan(root, 12345, candidate)[0]
+ current = copy.deepcopy(plan['failure']); events = []; snapshots = 0
+ def remote(node, action, **params):
+ nonlocal snapshots
+ events.append(action)
+ if action == 'owner-markers-audit': return dict(markerRecoveryRequired=True, inspectionOnly=True, mutated=False, canonicalPassClaimed=False)
+ if action == 'owner-markers-recover':
+ self.assertEqual(params['expectedAudit'], dict(markerRecoveryRequired=True, inspectionOnly=True, mutated=False, canonicalPassClaimed=False))
+ return dict(ownerMetadataRestored=True, businessBytesChanged=False, canonicalPassClaimed=False)
+ if action == 'stop': return {}
+ if action == 'snapshot':
+ snapshots += 1
+ self.assertEqual(params, dict(quiescent=True, **({'metadataFaults': plan['metadataFaults'][node]} if snapshots == 1 else {})))
+ return copy.deepcopy(current[node])
+ if action == 'recover-fault':
+ fault = next(f for f in plan['faults'] if f['faultId'] == params['faultId'])
+ self.assertEqual(params['disk'], fault['disk'])
+ self.assertEqual(params['expectedReceiptSha256'], outcome_recovery.digest({k: v for k, v in fault['receipt'].items() if k != 'witnessedAt'}))
+ entry = next(e for e in current[node]['files'] if e['disk'] == fault['disk'] and e['path'] == fault['receipt']['relativePath'])
+ entry.clear(); entry.update(copy.deepcopy(next(e for e in plan['before'][node]['files'] if e['disk'] == fault['disk'] and e['path'] == fault['receipt']['relativePath'])))
+ return dict(runId=plan['runId'], faultId=fault['faultId'], physicalRepairPassAllowed=False)
+ self.assertEqual(physical_content(current), physical_content(plan['before']))
+ return dict(baseline=dict(originalSceneSha256=params['expectedOriginalSceneSha256']), inspectionOnly=True,
+ mutated=False, activeNamespaceIsolated=True, canonicalPassClaimed=False)
+ fleet = types.SimpleNamespace(remote=remote, record=lambda label, value: value)
+ states = [dict(node='vm000', state=dict(exists=True), roots=[dict(exists=True)] * 4)]
+ result = interrupted_recovery.restore_and_isolate(fleet, plan, states, recover_environment.recovery_scene_matches)
+ self.assertEqual(result['cleanup'], 'ISOLATED'); self.assertFalse(result['canonicalPassClaimed'])
+ self.assertEqual(events[:6], ['owner-markers-audit', 'owner-markers-recover', 'stop', 'snapshot', 'recover-fault', 'recover-fault'])
+ self.assertFalse(set(events) & {'start', 'cleanup', 'journal-recover'})
+
+ def test_real_opaque_archive_retries_all_moves_and_hashes_fault_backups_without_credentials(self):
+ helper = scene_tests.RetainedSceneTests()
+ for lost_after in range(1, 6):
+ with self.subTest(lost_after=lost_after), tempfile.TemporaryDirectory() as temporary:
+ state, roots, node, request = helper.scene(temporary)
+ roots[0].mkdir(parents=True); owner = request['expectedOwner']; owner['runtimeProfile'] = 'outcome-legacy'
+ for folder in (*roots, state): emit(folder / 'owner.json', owner)
+ old_state, old_roots = retained_scene.destinations(state, roots)
+ old_parents = [path.parent for path in (*old_roots, old_state)]
+ for parent in old_parents:
+ parent.mkdir(mode=0o755, parents=True)
+ (parent / 'unrelated-scene').write_bytes(b'preserve-shared-parent')
+ preserved = [(p.stat().st_ino, p.stat().st_mode, (p / 'unrelated-scene').read_bytes()) for p in old_parents]
+ fault_id = heal_fault.bounded_fault_id('J02', 'original-opaque', 0)
+ for suffix in ('original', 'intent.json', 'applied.json', 'failed-' + 'a' * 64):
+ (roots[1] / 'quarantine' / (fault_id + '.' + suffix)).write_bytes(b'fault-evidence')
+ snapshot = dict(files=[], formats=[{}] * 4, pid=0, quiescent=True, binarySha256=owner['binarySha256'])
+ node['snapshot'] = lambda value: snapshot
+ request['expectedOriginalSceneSha256'] = outcome_recovery.digest({k: snapshot[k] for k in ('files', 'formats')})
+ opened = []; original_open = retained_scene.os.open
+ def checked_open(path, *args, **kwargs):
+ opened.append(str(path)); return original_open(path, *args, **kwargs)
+ with helper.root_stats(), patch.object(retained_scene.os, 'open', side_effect=checked_open), patch.object(retained_scene, 'rename_new', side_effect=helper.local_rename):
+ baseline = retained_scene.run(request, node)['baseline']
+ self.assertTrue(all(any(fault_id + '.' + suffix in p for p in opened) for suffix in ('original', 'intent.json', 'applied.json', 'failed-' + 'a' * 64)))
+ self.assertFalse(any('service.env' in p or '/iam/credential' in p for p in opened))
+ count = 0
+ def move(source, target):
+ nonlocal count
+ helper.local_rename(source, target)
+ if target.is_dir():
+ count += 1
+ if count == lost_after: raise RuntimeError('lost ACK')
+ change = dict(request, action='retained-scene-isolate', expectedBaseline=baseline)
+ with patch.object(retained_scene, 'rename_new', side_effect=move), self.assertRaises(RuntimeError): retained_scene.run(change, node)
+ def command(*args, **kw):
+ return types.SimpleNamespace(stdout='0' if 'MainPID' in args else 'inactive')
+ with patch.object(fleet_node, 'paths', return_value=(state, roots)), patch.object(fleet_node, 'command', side_effect=command):
+ observed = fleet_node.namespace_state(dict(runId=request['runId'], action='namespace-state'))
+ self.assertEqual(observed['retained']['roots'], [i < lost_after for i in range(4)])
+ self.assertEqual(observed['retained']['state'], lost_after == 5)
+ node['snapshot'] = lambda value: self.fail('partial archive must use durable original digest')
+ result = retained_scene.run(change, node)
+ self.assertEqual(result['locations'], ['retained'] * 5); self.assertEqual(result['baseline'], baseline)
+ self.assertFalse(result['canonicalPassClaimed']); self.assertFalse(result['credentialContentsRead'])
+ self.assertEqual(result['baseline']['archiveLayout'], 'per-run-parent')
+ self.assertEqual(len(result['privateArchiveParents']), 5)
+ self.assertTrue(all(p['exists'] and p['uid'] == 0 and p['mode'] == 0o700 for p in result['privateArchiveParents']))
+ checked = retained_scene.run(request, node)
+ self.assertEqual(checked['privateArchiveParents'], result['privateArchiveParents'])
+ self.assertEqual([(p.stat().st_ino, p.stat().st_mode, (p / 'unrelated-scene').read_bytes()) for p in old_parents], preserved)
+
+ def test_real_file_recovery_preserves_corrupt_bytes_and_refuses_later_data(self):
+ for drift in (None, 'intent', 'backup', 'target'):
+ with self.subTest(drift=drift), tempfile.TemporaryDirectory() as temporary:
+ root = Path(temporary).resolve(); (root / 'quarantine').mkdir()
+ bucket = 'rv-heal-0123456789abcdef'; relative = bucket + '/opaque/source.bin/xl.meta'
+ target = root / 'data' / relative; target.parent.mkdir(parents=True)
+ original = b'XL2 original acknowledged metadata'; target.write_bytes(original)
+ owner = dict(runId='gh-12345-1-h1', bucketPrefix=bucket)
+ fault_id = heal_fault.bounded_fault_id('J02', 'original-opaque', 1)
+ with patch.object(heal_fault, 'mount_id', return_value=1), heal_fault.directory(root) as fd:
+ receipt = heal_fault.apply_fault(fd, owner, relative, fault_id, 'bitflip', hashlib.sha256(original).hexdigest(), 0)
+ corrupt = target.read_bytes(); receipt_sha = outcome_recovery.digest({k: v for k, v in receipt.items() if k != "witnessedAt"})
+ if drift == 'intent': receipt_sha = 'f' * 64
+ elif drift == 'backup': (root / 'quarantine' / (fault_id + '.original')).write_bytes(b'foreign backup')
+ elif drift == 'target': target.write_bytes(b'later acknowledged bytes')
+ before = target.read_bytes()
+ if drift:
+ with self.assertRaises(ValueError): heal_fault.recover_fault(fd, owner, fault_id, expected_receipt_sha256=receipt_sha)
+ self.assertEqual(target.read_bytes(), before)
+ continue
+ proof = heal_fault.recover_fault(fd, owner, fault_id, expected_receipt_sha256=receipt_sha)
+ self.assertTrue(proof['restoredFromBackup']); self.assertFalse(proof['physicalRepairPassAllowed'])
+ self.assertEqual(target.read_bytes(), original)
+ preserved = root / 'quarantine' / (fault_id + '.failed-' + hashlib.sha256(corrupt).hexdigest())
+ self.assertEqual(preserved.read_bytes(), corrupt)
+ self.assertFalse(heal_fault.recover_fault(fd, owner, fault_id, expected_receipt_sha256=receipt_sha)['restoredFromBackup'])
+
+ def test_opaque_archive_refuses_shared_copies_and_namespace_inspection_rejects_both_layouts(self):
+ helper = scene_tests.RetainedSceneTests()
+ for collision in ('root', 'state'):
+ with self.subTest(collision=collision), tempfile.TemporaryDirectory() as temporary:
+ state, roots, node, request = helper.scene(temporary)
+ roots[0].mkdir(parents=True); owner = request['expectedOwner']; owner['runtimeProfile'] = 'outcome-legacy'
+ for folder in (*roots, state): emit(folder / 'owner.json', owner)
+ request['expectedOriginalSceneSha256'] = 'a' * 64
+ shared_state, shared_roots = retained_scene.destinations(state, roots)
+ private_state, private_roots = retained_scene.destinations(state, roots, per_run_parent=True)
+ shared = shared_roots[0] if collision == 'root' else shared_state
+ private = private_roots[0] if collision == 'root' else private_state
+ shared.mkdir(parents=True)
+ with helper.root_stats(), patch.object(retained_scene, 'rename_new') as move, self.assertRaisesRegex(ValueError, 'shared archive already exists'):
+ retained_scene.run(request, node)
+ move.assert_not_called(); self.assertFalse(private.parent.exists())
+ private.mkdir(parents=True)
+ def command(*args, **kw): return types.SimpleNamespace(stdout='0' if 'MainPID' in args else 'inactive')
+ with patch.object(fleet_node, 'paths', return_value=(state, roots)), patch.object(fleet_node, 'command', side_effect=command), \
+ self.assertRaisesRegex(ValueError, 'both shared and per-run'):
+ fleet_node.namespace_state(dict(runId=request['runId'], action='namespace-state'))
diff --git a/test_validation_heal_owner_recovery.py b/test_validation_heal_owner_recovery.py
new file mode 100644
index 00000000..8cc87bd7
--- /dev/null
+++ b/test_validation_heal_owner_recovery.py
@@ -0,0 +1,164 @@
+"""Owner drift never authorizes a business write or bypasses archived byte checks."""
+from contextlib import ExitStack, contextmanager
+import copy
+import hashlib
+import json
+import os
+from pathlib import Path
+import socket
+import tempfile
+import types
+import unittest
+from unittest.mock import patch
+
+from scripts.validation_heal import fleet_node, heal_fault, healthy, owner_recovery as recovery, retained_scene
+from test_validation_heal import versioned_scene
+
+
+class OwnerRecoveryTests(unittest.TestCase):
+ @contextmanager
+ def fixture(self):
+ with tempfile.TemporaryDirectory() as directory, ExitStack() as stack:
+ base = Path(directory).resolve(); run = 'gh-12345-1-h1'
+ state = base / 'state' / run; state.mkdir(parents=True, mode=0o700)
+ mounts = [base / f'disk{i}' for i in range(4)]
+ roots = [p / 'heal-validation' / run for p in mounts]
+ before, bucket, _ = versioned_scene(['objects/0000.bin', 'opaque/source.bin'], sizes=(100,))
+ before = before['node1']; before.update(pid=0, quiescent=True)
+ owner = dict(runId=run, hostname=socket.gethostname(), unit='rustfs-validation-heal-' + run + '.service',
+ runtimeProfile='outcome-legacy', bucket=bucket, binarySha256=hashlib.sha256(b'candidate').hexdigest(),
+ inspectorSha256=hashlib.sha256(b'inspector').hexdigest(), mounts=[dict(
+ target=str(p), source=str(i), fstype='xfs', uuid=str(i), **{'maj:min': str(i)}) for i, p in enumerate(mounts)])
+ before['binarySha256'] = owner['binarySha256']
+ (state / 'owner.json').write_text(json.dumps(owner)); (state / 'rustfs').write_bytes(b'candidate')
+ (state / 'heal-meta-inspect').write_bytes(b'inspector'); (state / 'service.env').write_bytes(b'credential-not-to-read')
+ markers = {}
+ for disk, root in enumerate(roots):
+ root.mkdir(parents=True, mode=0o700); (root / 'quarantine').mkdir()
+ (root / 'owner.json').write_text(json.dumps(owner))
+ info = (root / 'owner.json').stat(); markers[(info.st_dev, info.st_ino)] = (999, 988)
+ for entry in (e for e in before['files'] if e['disk'] == disk):
+ path = root / 'data' / entry['path']; path.parent.mkdir(parents=True, exist_ok=True)
+ raw = json.dumps(entry.get('metadata', {'shard': disk})).encode()
+ path.write_bytes(raw); entry.update(size=len(raw), sha256=hashlib.sha256(raw).hexdigest())
+ path = root / 'data/.rustfs.sys/format.json'; path.parent.mkdir(parents=True)
+ raw = json.dumps(before['formats'][disk]['value']).encode(); path.write_bytes(raw)
+ before['formats'][disk]['sha256'] = hashlib.sha256(raw).hexdigest()
+ stack.enter_context(patch.object(heal_fault, 'mount_id', return_value=1))
+ faults = []
+ for disk in (0, 1):
+ with heal_fault.directory(roots[disk]) as fd:
+ binding = dict(schemaVersion=1, runId=run, bucketPrefix=bucket, mountId=1,
+ device=os.fstat(fd).st_dev, leaseExpiresAt='2020-01-01T00:00:00+00:00')
+ heal_fault.new_file(fd, 'fault-owner.json', heal_fault.encode(binding))
+ relative = bucket + '/opaque/source.bin/xl.meta'
+ original = next(e for e in before['files'] if e['disk'] == disk and e['path'] == relative)
+ fault_id = heal_fault.bounded_fault_id('J02', 'original-opaque', disk)
+ receipt = heal_fault.apply_fault(fd, binding, relative, fault_id, 'bitflip', original['sha256'], 0)
+ faults.append(dict(disk=disk, faultId=fault_id, receipt=receipt))
+ original_fstat, original_stat, original_lstat = os.fstat, Path.stat, Path.lstat
+ def mapped(info):
+ row = {key: getattr(info, key) for key in dir(info) if key.startswith('st_')}
+ row['st_uid'], row['st_gid'] = markers.get((info.st_dev, info.st_ino), (0, 0))
+ return types.SimpleNamespace(**row)
+ stack.enter_context(patch.object(os, 'fstat', side_effect=lambda fd: mapped(original_fstat(fd))))
+ stack.enter_context(patch.object(Path, 'stat', lambda p, **kw: mapped(original_stat(p, **kw))))
+ stack.enter_context(patch.object(Path, 'lstat', lambda p: mapped(original_lstat(p))))
+ stack.enter_context(patch.object(recovery.pwd, 'getpwnam', return_value=types.SimpleNamespace(pw_uid=999, pw_gid=988)))
+ stack.enter_context(patch.object(retained_scene, 'mounted_paths', return_value=[]))
+ changed = []
+ def chown(fd, uid, gid):
+ info = original_fstat(fd); key = (info.st_dev, info.st_ino)
+ self.assertIn(key, markers); self.assertEqual((uid, gid), (0, 0))
+ markers[key] = (0, 0); changed.append(key)
+ stack.enter_context(patch.object(os, 'fchown', side_effect=chown))
+ node = dict(paths=lambda run_id: (state, roots), checked=fleet_node.checked, MOUNTS=mounts,
+ unit=lambda run_id: owner['unit'], socket=socket, mounts=lambda: copy.deepcopy(owner['mounts']),
+ stopped=lambda value: dict(MainPID='0', ActiveState='inactive'), digest=fleet_node.digest,
+ fault_module=lambda: heal_fault, retained_scene_backend=retained_scene)
+ node['testMarkerOwners'] = markers
+ request = dict(runId=run, action='owner-markers-audit', expectedOwner=owner, before=before, faults=faults)
+ yield state, roots, node, request, changed, chown
+
+ def test_audit_and_recovery_preserve_all_bytes_and_durable_prechange_evidence(self):
+ with self.fixture() as (state, roots, node, request, changed, chown):
+ original = {p: (p.stat().st_ino, p.read_bytes()) for folder in (state, *roots) for p in folder.rglob('*') if p.is_file()}
+ opened = []; original_open = os.open
+ def observed_open(path, *args, **kw):
+ opened.append(str(path)); return original_open(path, *args, **kw)
+ with patch.object(os, 'open', side_effect=observed_open):
+ audit = recovery.run(request, node)
+ self.assertTrue(audit['markerRecoveryRequired']); self.assertEqual(changed, [])
+ self.assertFalse((state / 'owner-marker-recovery.intent.json').exists())
+ result = recovery.run(dict(request, action='owner-markers-recover', expectedAudit=audit), node)
+ self.assertEqual(len(changed), 4); self.assertTrue(result['ownerMetadataRestored'])
+ self.assertFalse(result['businessBytesChanged']); self.assertFalse(result['canonicalPassClaimed'])
+ self.assertFalse(any('service.env' in p for p in opened))
+ intent = json.loads((state / 'owner-marker-recovery.intent.json').read_text())
+ self.assertEqual(intent['markers'], audit['markers']); self.assertEqual(intent['rootCensus'], audit['rootCensus'])
+ self.assertEqual(result['intentSha256'], recovery.sha(intent))
+ for path, saved in original.items(): self.assertEqual((path.stat().st_ino, path.read_bytes()), saved)
+ again = recovery.run(request, node)
+ self.assertFalse(again['markerRecoveryRequired']); self.assertEqual(again['priorIntent'], intent)
+
+ def test_recovery_retries_after_each_marker_change_without_rewriting_the_original_intent(self):
+ for lost_after in range(1, 5):
+ with self.subTest(lost_after=lost_after), self.fixture() as (state, roots, node, request, changed, chown):
+ audit = recovery.run(request, node)
+ def interrupted(fd, uid, gid):
+ chown(fd, uid, gid)
+ if len(changed) == lost_after: raise RuntimeError('lost remote acknowledgement')
+ with patch.object(os, 'fchown', side_effect=interrupted), self.assertRaises(RuntimeError):
+ recovery.run(dict(request, action='owner-markers-recover', expectedAudit=audit), node)
+ raw_intent = (state / 'owner-marker-recovery.intent.json').read_bytes()
+ current = recovery.run(request, node)
+ result = recovery.run(dict(request, action='owner-markers-recover', expectedAudit=current), node)
+ self.assertTrue(result['ownerMetadataRestored']); self.assertEqual(len(changed), 4)
+ self.assertEqual((state / 'owner-marker-recovery.intent.json').read_bytes(), raw_intent)
+
+ def test_drift_and_unknown_authority_refuse_before_chown(self):
+ for poison in ('owner', 'binary', 'inspector', 'empty-prior', 'format', 'business', 'missing', 'extra', 'backup', 'intent', 'applied',
+ 'foreign-backup', 'mount', 'nested-mount', 'foreign-uid', 'central-uid', 'live', 'legacy-phase', 'gate', 'symlink', 'hardlink', 'audit-race'):
+ with self.subTest(poison=poison), self.fixture() as (state, roots, node, request, changed, _):
+ audit = recovery.run(request, node)
+ target = roots[0] / 'data' / request['expectedOwner']['bucket'] / 'objects/0000.bin/xl.meta'
+ if poison == 'owner': (roots[0] / 'owner.json').write_text('{}')
+ elif poison == 'binary': (state / 'rustfs').write_bytes(b'foreign')
+ elif poison == 'inspector': (state / 'heal-meta-inspect').write_bytes(b'foreign')
+ elif poison == 'empty-prior':
+ (state / 'owner-marker-recovery.intent.json').write_text('{}')
+ with self.assertRaises(ValueError): recovery.run(request, node)
+ self.assertEqual(changed, [])
+ continue
+ elif poison == 'format': (roots[0] / 'data/.rustfs.sys/format.json').write_text('{}')
+ elif poison == 'business': target.write_bytes(b'later acknowledged data')
+ elif poison == 'missing': target.unlink()
+ elif poison == 'extra': (target.parent / 'other').write_bytes(b'foreign')
+ elif poison in ('backup', 'intent', 'applied'):
+ suffix = {'backup': 'original', 'intent': 'intent.json', 'applied': 'applied.json'}[poison]
+ next((roots[0] / 'quarantine').glob('*.' + suffix)).write_bytes(b'foreign')
+ elif poison == 'foreign-backup': (roots[2] / 'quarantine/foreign').write_bytes(b'unknown')
+ elif poison == 'mount': node['mounts'] = lambda: []
+ elif poison == 'nested-mount': retained_scene.mounted_paths.return_value = [roots[0] / 'data']
+ elif poison in ('foreign-uid', 'central-uid'):
+ path = roots[0] / 'owner.json' if poison == 'foreign-uid' else state / 'owner.json'
+ info = path.stat(); node['testMarkerOwners'][(info.st_dev, info.st_ino)] = (777, 777) if poison == 'foreign-uid' else (999, 988)
+ elif poison == 'live': node['stopped'] = lambda owner: (_ for _ in ()).throw(ValueError('live PID'))
+ elif poison == 'legacy-phase': (state / 'J02-phase.json').write_text('{}')
+ elif poison == 'gate': (state / 'drive-gate.json').write_text('{}')
+ elif poison == 'symlink': target.unlink(); target.symlink_to(state / 'service.env')
+ elif poison == 'hardlink': os.link(target, target.parent / 'alias')
+ elif poison == 'audit-race': audit['rawPhysicalSha256'] = 'f' * 64
+ with self.assertRaises((ValueError, KeyError)): recovery.run(dict(request, action='owner-markers-recover', expectedAudit=audit), node)
+ self.assertEqual(changed, [])
+
+ def test_remote_adapter_bundles_both_auditors_and_descriptor_backend(self):
+ def execute(*args, **kwargs):
+ source = kwargs['input'].rsplit('\nmain(', 1)[0]; scope = dict(__name__='__test_node__')
+ exec(compile(source, 'owner-recovery-node', 'exec'), scope)
+ self.assertTrue(callable(scope['owner_recovery_backend'].run))
+ self.assertTrue(callable(scope['retained_scene_backend'].census))
+ self.assertTrue(callable(scope['fault_module']().read_file))
+ return types.SimpleNamespace(returncode=0, stdout='{}', stderr='')
+ with tempfile.TemporaryDirectory() as directory, patch.object(healthy.subprocess, 'run', side_effect=execute):
+ healthy.Fleet(['vm000'], Path(directory), 'gh-12345-1-h1').remote('vm000', 'owner-markers-audit')
diff --git a/test_validation_heal_performance.py b/test_validation_heal_performance.py
new file mode 100644
index 00000000..42190e60
--- /dev/null
+++ b/test_validation_heal_performance.py
@@ -0,0 +1,208 @@
+import copy
+import hashlib
+import json
+from pathlib import Path
+import tempfile
+import types
+import unittest
+from unittest.mock import patch
+
+from scripts.validation_heal import performance as perf
+from scripts.validation_heal import fleet_node as node
+from scripts.validation_heal.matrix import select, AUTOMATIC_CASES
+
+
+class PerformanceTests(unittest.TestCase):
+ def metrics(self, epoch=1):
+ return {name: {"identity": {field: name + field for field in node.PROCESS_FIELDS},
+ "clockDomain": name + "bootId", "monotonicNs": epoch * 10**9,
+ "cpuTicks": epoch * 10, "clockTicksPerSecond": 100, "rssBytes": epoch * 100,
+ "fileDescriptors": 100, "io": {key: epoch * 100 for key in
+ ("rchar", "wchar", "syscr", "syscw", "read_bytes", "write_bytes")}, "readOnly": True}
+ for name in ("n0", "n1", "n2", "n3")}
+
+ def rows(self):
+ return [{"sequence": index, "operation": "get_object" if index % 2 == 0 else "put_object",
+ "latencyMs": 10, "verdict": "PASS"} for index in range(120)]
+
+ def phases(self):
+ return [{**spec, "statistics": perf.summarize(self.rows()),
+ "resources": perf.resource_delta(self.metrics(), self.metrics(2)),
+ "repair": {"physicalVerified": True, "MiBPerSecond": 1} if spec["mode"] == "B" else None,
+ "overlap": {"bothOperationsDuringRealRepair": True}, "noHeal": spec["mode"] == "A"}
+ for spec in perf.schedule()]
+
+ def test_frozen_order_contains_every_source_repetition(self):
+ self.assertEqual(select(perf.CASE_ID)[0]["variants"], list(perf.BRANCHES))
+ self.assertIn(perf.CASE_ID, AUTOMATIC_CASES)
+ self.assertEqual([phase["mode"] for phase in perf.schedule()], list("ABBAABBAABBA"))
+ self.assertGreaterEqual(perf.PROTOCOL["faultLeaseMinutes"], 120)
+ with self.assertRaises(ValueError): perf.schedule({**perf.PROTOCOL, "maxErrorRate": 1})
+
+ def test_nearest_rank_percentiles_keep_the_slowest_request(self):
+ samples = list(range(1, 101))
+ self.assertEqual(perf.percentile(samples, .95), 95)
+ self.assertEqual(perf.percentile(samples, .99), 99)
+ for bad in ([], [float("nan")], [-1], [float("inf")]):
+ with self.assertRaises(ValueError): perf.percentile(bad, .95)
+
+ def test_failed_samples_stay_in_latency_and_error_denominators(self):
+ rows = self.rows()
+ for index in (1, 3): rows[index].update(verdict="FAIL", latencyMs=30000)
+ value = perf.summarize(rows)
+ self.assertEqual(value["put"]["requests"], 60)
+ self.assertEqual(value["failedRequests"], 2)
+ self.assertEqual(value["errorRate"], 2 / 120)
+ self.assertEqual(value["put"]["p99Ms"], 30000)
+ for bad in (rows[:-1], rows + [rows[0]], rows[:-1] + [rows[0]]):
+ with self.assertRaises(ValueError): perf.summarize(bad)
+
+ def test_transport_unknown_is_durable_before_summary_and_cannot_pass(self):
+ with tempfile.TemporaryDirectory() as name:
+ root = Path(name)
+ probe = types.SimpleNamespace(s3=lambda *args: {"acknowledged": False, "resultUnknown": True,
+ "clientCallInvoked": True, "error": {"type": "Timeout"}})
+ with patch.object(perf.time, "monotonic", return_value=1), patch.object(perf.time, "monotonic_ns", return_value=10**9):
+ row = perf.request(probe, "rv-heal-abcdef-load", 0, 1, root)
+ self.assertEqual(row["verdict"], "FAIL")
+ self.assertEqual(row["requestSha256"], hashlib.sha256((root / "00000-request.json").read_bytes()).hexdigest())
+ self.assertTrue(json.loads((root / "00000-ack.json").read_text())["observation"]["resultUnknown"])
+
+ def test_missed_admission_is_a_failed_sample_without_a_late_network_write(self):
+ probe = types.SimpleNamespace(s3=lambda *args: self.fail("late request was sent"))
+ with tempfile.TemporaryDirectory() as name, patch.object(perf.time, "monotonic", return_value=33), \
+ patch.object(perf.time, "monotonic_ns", return_value=33 * 10**9):
+ value = perf.request(probe, "rv-heal-abcdef-load", 1, 1, Path(name))
+ self.assertEqual(value["verdict"], "FAIL")
+ self.assertFalse(value["observation"]["clientCallInvoked"])
+
+ def test_get_success_with_the_wrong_bytes_is_a_failure(self):
+ observed = {"acknowledged": True, "resultUnknown": False, "response": {"ResponseMetadata": {"HTTPStatusCode": 200}},
+ "payload": {"size": 65536, "sha256": "wrong"}}
+ with tempfile.TemporaryDirectory() as name, patch.object(perf.time, "monotonic", return_value=1), \
+ patch.object(perf.time, "monotonic_ns", return_value=10**9):
+ value = perf.request(types.SimpleNamespace(s3=lambda *args: observed), "rv-heal-abcdef-load", 0, 1, Path(name))
+ self.assertEqual(value["verdict"], "FAIL")
+
+ def test_resource_counters_require_all_peers_and_the_same_process_birth(self):
+ start, end = self.metrics(), self.metrics(3)
+ delta = perf.resource_delta(start, end)
+ self.assertEqual(delta["n0"]["cpuCores"], .1)
+ self.assertEqual(delta["n0"]["ioDelta"]["read_bytes"], 200)
+ for kind in ("missing", "pid", "boot", "binary", "mount", "clock", "backwards", "io", "mutable"):
+ with self.subTest(kind=kind):
+ bad = copy.deepcopy(end)
+ if kind == "missing": del bad["n1"]
+ elif kind in {"pid", "binary", "mount"}:
+ key = {"pid": "pid", "binary": "binarySha256", "mount": "mountNamespace"}[kind]
+ bad["n1"]["identity"][key] = "other"
+ elif kind == "boot": bad["n1"]["clockDomain"] = "another-boot"
+ elif kind == "clock": bad["n1"]["clockTicksPerSecond"] = 1000
+ elif kind == "backwards": bad["n1"]["monotonicNs"] = 0
+ elif kind == "io": bad["n1"]["io"]["write_bytes"] = 99
+ else: bad["n1"]["readOnly"] = False
+ with self.assertRaises(ValueError): perf.resource_delta(start, bad)
+
+ def windows(self):
+ return [{"callStartedMonotonicNs": index * 3 * 10**9,
+ "callFinishedMonotonicNs": (index * 3 + .1) * 10**9,
+ "response": {"summary": "running", "outcome": {"execution": {"state": "running"},
+ "counters": {"processed": (index + 1) * 10, "healed": index, "unchanged": 10,
+ "skipped": 0, "failed": 0, "unknown": 0, "attemptFailures": 0, "overflowed": False}}}}
+ for index in range(3)]
+
+ def bracketed(self):
+ return [{"operation": "get_object" if index % 2 == 0 else "put_object", "observation": {
+ "clientCallInvoked": True, "callStartedMonotonicNs": (index * .3 + .2) * 10**9,
+ "callFinishedMonotonicNs": (index * .3 + .3) * 10**9}} for index in range(4)]
+
+ def test_true_partial_running_progress_must_bracket_both_workload_operations(self):
+ self.assertEqual(perf.overlap(self.bracketed(), self.windows())["bracketedCalls"], 4)
+ for kind in ("finished", "zero", "stalled", "regressed", "unknown", "put-only", "outside", "not-invoked", "few"):
+ with self.subTest(kind=kind):
+ rows, windows = self.bracketed(), self.windows()
+ if kind == "finished":
+ for row in windows: row["response"]["summary"] = "finished"
+ elif kind in {"zero", "stalled"}:
+ for row in windows: row["response"]["outcome"]["counters"]["processed"] = 0 if kind == "zero" else 10
+ elif kind == "regressed": windows[-1]["response"]["outcome"]["counters"]["processed"] = 1
+ elif kind == "unknown": windows[0]["response"]["outcome"]["counters"]["unknown"] = 1
+ elif kind == "put-only":
+ for row in rows: row["operation"] = "put_object"
+ elif kind == "outside":
+ for row in rows: row["observation"]["callFinishedMonotonicNs"] = 99 * 10**9
+ elif kind == "not-invoked":
+ for row in rows: row["observation"]["clientCallInvoked"] = False
+ else: rows.pop()
+ with self.assertRaises(ValueError): perf.overlap(rows, windows)
+
+ def test_independent_performance_gates_reject_functional_success_with_slow_or_failed_calls(self):
+ self.assertEqual(len(perf.acceptance(self.phases())["pairedComparisons"]), 12)
+ for kind in ("omit", "reorder", "error", "absolute", "paired", "no-physical", "no-overlap", "cpu", "rss", "fd", "baseline-heal"):
+ with self.subTest(kind=kind):
+ phases = self.phases()
+ if kind == "omit": phases.pop()
+ elif kind == "reorder": phases[0], phases[1] = phases[1], phases[0]
+ elif kind == "error": phases[1]["statistics"]["errorRate"] = 1 / 120
+ elif kind == "absolute": phases[1]["statistics"]["get"]["p99Ms"] = 11000
+ elif kind == "paired": phases[1]["statistics"]["get"]["p99Ms"] = 100
+ elif kind == "no-physical": phases[1]["repair"]["physicalVerified"] = False
+ elif kind == "no-overlap": phases[1]["overlap"]["bothOperationsDuringRealRepair"] = False
+ elif kind in {"cpu", "rss", "fd"}:
+ key = {"cpu": "cpuCores", "rss": "maxSampledRssBytes", "fd": "maxSampledFDs"}[kind]
+ phases[1]["resources"]["n2"][key] = 10**12
+ else: phases[0]["noHeal"] = False
+ with self.assertRaises(ValueError): perf.acceptance(phases)
+
+ def test_phase_hardware_cannot_drift(self):
+ snapshot = {node: {"formats": [{"value": "disk-" + node}], "hostname": node, "binarySha256": "candidate"}
+ for node in ("n0", "n1", "n2", "n3")}
+ self.assertTrue(perf.same_hardware(snapshot, copy.deepcopy(snapshot)))
+ for key in ("formats", "hostname", "binarySha256"):
+ changed = copy.deepcopy(snapshot)
+ changed["n2"][key] = "new"
+ with self.assertRaises(ValueError): perf.same_hardware(snapshot, changed)
+
+ def test_node_metrics_reject_path_or_pid_injection_before_reading_process(self):
+ for field in ("pid", "path", "command", "expected"):
+ with patch.object(node, "process", side_effect=AssertionError("must reject before reading")):
+ with self.assertRaises(ValueError): node.process_metrics({"runId": "gh-1-1-h2", "action": "process-metrics", field: "/etc/shadow"})
+
+ def test_node_metrics_attest_resources_and_reject_a_recycled_pid(self):
+ identity = {name: name for name in node.PROCESS_FIELDS}
+ identity.update(pid=123, startTicks=900)
+ fields = ["0"] * 22
+ fields[0], fields[11], fields[12], fields[17], fields[19], fields[20], fields[21] = "S", "100", "50", "12", "900", "9000", "5"
+ proc = {"stat": "123 (rustfs worker) " + " ".join(fields),
+ "io": "\n".join(name + ": 100" for name in ("rchar", "wchar", "syscr", "syscw", "read_bytes", "write_bytes", "cancelled_write_bytes"))}
+ class Proc:
+ def __init__(self, name=""): self.name = name
+ def __truediv__(self, name): return Proc(name)
+ def read_text(self): return proc[self.name]
+ def iterdir(self): return iter(range(7))
+ with patch.object(node, "Path", return_value=Proc()), patch.object(node, "process", return_value=identity), \
+ patch.object(node, "owned", return_value=(None, None, {"runtimeProfile": "manual"})), \
+ patch.object(node.os, "sysconf", side_effect=lambda name: 100 if name == "SC_CLK_TCK" else 4096):
+ value = node.process_metrics({"runId": "gh-1-1-h2", "action": "process-metrics"})
+ self.assertEqual(value["cpuTicks"], 150)
+ self.assertEqual(value["rssBytes"], 5 * 4096)
+ self.assertEqual(value["fileDescriptors"], 7)
+ with patch.object(node, "process", side_effect=[identity, {**identity, "startTicks": 901}]):
+ with self.assertRaises(ValueError): node.process_metrics({"runId": "gh-1-1-h2", "action": "process-metrics"})
+
+ def test_exit_race_refreshes_systemd_and_never_infers_an_exit_from_proc_absence(self):
+ request = {"runId": "gh-1-1-h2", "action": "process"}
+ missing = FileNotFoundError(2, "process exited", "/proc/123/stat")
+ terminal = {"pid": 0, "service": {"ExecMainCode": "1", "ExecMainStatus": "0", "Result": "success"}}
+ with patch.object(node, "process_once", side_effect=[missing, terminal]) as reader, patch.object(node.time, "sleep"):
+ self.assertEqual(node.process(request), terminal)
+ self.assertEqual(reader.call_count, 2)
+ with patch.object(node, "process_once", side_effect=missing), patch.object(node.time, "sleep"):
+ with self.assertRaisesRegex(ValueError, "did not settle"): node.process(request)
+ wrong_path = FileNotFoundError(2, "candidate missing", "/var/lib/rustfs-auto-testing/heal/rustfs")
+ with patch.object(node, "process_once", side_effect=wrong_path) as reader:
+ with self.assertRaises(FileNotFoundError): node.process(request)
+ self.assertEqual(reader.call_count, 1)
+
+
+if __name__ == "__main__": unittest.main()
diff --git a/test_validation_heal_persistence.py b/test_validation_heal_persistence.py
new file mode 100644
index 00000000..740cdc90
--- /dev/null
+++ b/test_validation_heal_persistence.py
@@ -0,0 +1,844 @@
+"""F07 preserves actual corrupt responsibility and separates rescan success."""
+
+import base64
+import copy
+import json
+import os
+from pathlib import Path
+import tempfile
+from types import SimpleNamespace
+import unittest
+from unittest.mock import patch
+
+from scripts.validation_heal import persistence, checkpoint, journal_fault as journal
+import test_validation_heal_checkpoint as fixture_source
+from test_validation_heal_checkpoint import BUCKET, TOKEN, INCARNATION, encoded
+
+
+class PersistenceContractTests(unittest.TestCase):
+ def test_observation_fixtures_fit_existing_inventory_inode_and_volume_limits(self):
+ from scripts.validation_heal import bounded_volume
+ for fixture in ('DB', 'DV'):
+ for branch in (*persistence.IMPLEMENTED, 'journal-write-failure'):
+ count, size = persistence.checkpoint_fixture(branch, fixture)
+ data = count * (3 if fixture == 'DV' else 1)
+ entries = data + ((count + 1) // 2 if fixture == 'DV' else 0)
+ self.assertGreater(count, 4)
+ self.assertGreater(size, 1024**2)
+ self.assertLessEqual(entries, 512)
+ self.assertLess(data * size, 2 * 1024**3)
+ if branch == 'journal-write-failure':
+ # Conservatively allow per-version integrity and metadata,
+ # object directories, and 128 system/working inodes.
+ self.assertLess(3 * data + 2 * count + 128, bounded_volume.MAX_INODES)
+ self.assertLess(data * ((size + 1) // 2 + 16384) + 16 * 1024**2,
+ bounded_volume.IMAGE_BYTES)
+
+ def fixture(self, kind="truncate", *, admission_options=None, terminal_options=None):
+ observed, _, _ = fixture_source.AdminCheckpointTests().fixture()
+ frozen = checkpoint.owned_checkpoint(observed, TOKEN, BUCKET)
+ root_value = {"schema": 3, "task_id": TOKEN, "heal_type": {
+ "type": "erasure_set", "buckets": [], "set_disk_id": "pool_0_set_0", "all_buckets": True},
+ "options": {"scan_mode": 2, "recursive": True}}
+ if admission_options is not None: root_value["options"] = copy.deepcopy(admission_options)
+ owner = {"node": "node", "disk": 1, "relativePath": journal.names("root", TOKEN)[0],
+ "value": root_value, **encoded(root_value)}
+ frozen["rootAdmission"] = owner
+ receipt = {"applied": True, "originalBackupRetained": True, "productRepairClaimed": False,
+ "family": "checkpoint", "taskId": TOKEN, "disk": frozen["disk"],
+ "relativePath": frozen["checkpoint"]["relativePath"], "kind": kind,
+ "before": journal.entry(checkpoint.bytes_of(frozen["checkpoint"]))}
+ corrupt = journal.transform(checkpoint.bytes_of(frozen["checkpoint"]), receipt, {"bucket": BUCKET}, None)
+ receipt["injected"] = journal.entry(corrupt)
+ observed["node"]["records"][4].update(present=True, **journal.entry(corrupt), rawBase64=base64.b64encode(corrupt).decode())
+ observed["node"]["records"][6].update(present=True, **journal.entry(b"blocked"), rawBase64=base64.b64encode(b"blocked").decode())
+ current = {"summary": "stopped", "settings": checkpoint.OPTIONS, "detail": "Failed to deserialize checkpoint",
+ "outcome": {"execution": {"state": "aborted", "reason": "untraversable"}, "coverage": "partial",
+ "objects": [], "objectsTruncated": False, "counters": dict(processed=0, healed=0, unchanged=0,
+ skipped=0, failed=0, unknown=0, attemptFailures=0, overflowed=False)}}
+ terminal = {"schema": 1, "task_id": TOKEN, "heal_type": root_value["heal_type"],
+ "options": root_value["options"], "status": {"Failed": {"error": current["detail"]}}}
+ if terminal_options is not None: terminal["options"] = copy.deepcopy(terminal_options)
+ report = {"schema": 1, "terminal": terminal, "outcome": current["outcome"]}
+ names = journal.names("root", TOKEN)
+ roots = {"node": {"readOnly": True, "quiescent": True, "family": "root", "taskId": TOKEN,
+ "directoryEnumerationUsed": False, "records": [
+ {"disk": disk, "relativePath": name, "present": False} for disk in range(4) for name in names]}}
+ roots["node"]["records"][4].update(encoded(terminal))
+ roots["node"]["records"][5].update(encoded(report))
+ return observed, roots, frozen, receipt, current
+
+ def test_corrupt_bytes_blocked_checkpoint_and_durable_failure_remain_separate_from_repair(self):
+ for kind in ("truncate", "future-schema"):
+ with self.subTest(kind=kind):
+ proof = persistence.retained_corruption(*self.fixture(kind))
+ self.assertTrue(proof["durableOriginalFailureRetained"])
+ self.assertFalse(proof["originalTaskRepairClaimed"])
+ self.assertFalse(proof["backupRescueUsed"])
+
+ def recorded_options(self):
+ return json.loads((Path(__file__).parent / "testdata/validation/root-terminal-options.json").read_text())
+
+ def test_candidate_default_timeout_reconciles_original_corruption_and_completion(self):
+ recorded = self.recorded_options()
+ for kind in ("truncate", "future-schema"):
+ data = self.fixture(kind, admission_options=recorded["admissionOptions"],
+ terminal_options=recorded["terminalOptions"])
+ proof = persistence.retained_corruption(*data)
+ self.assertTrue(proof["durableOriginalFailureRetained"])
+ self.assertFalse(proof["originalTaskRepairClaimed"])
+ roots = self.completion_roots(data[2], data[4])
+ terminal = journal.decode_json(checkpoint.bytes_of(roots["node"]["records"][4]))
+ terminal["options"] = recorded["terminalOptions"]
+ roots["node"]["records"][4].update(encoded(terminal))
+ roots["node"]["records"][5].update(encoded({"schema": 1, "terminal": terminal, "outcome": data[4]["outcome"]}))
+ self.assertTrue(persistence.completed_original(roots, data[2], data[4])["sameOriginalTaskAndPhysicalOwner"])
+
+ def test_default_timeout_never_normalizes_other_options_or_refunds_explicit_budgets(self):
+ recorded = self.recorded_options()
+ for change in ("shorter", "longer", "fractional", "float", "bool", "missing", "unknown-duration-field",
+ "missing-admission-timeout", "explicit-budget", "consumed-budget", "scope", "nolock", "extra-option"):
+ admitted, terminal = copy.deepcopy(recorded["admissionOptions"]), copy.deepcopy(recorded["terminalOptions"])
+ if change == "shorter": terminal["timeout"]["secs"] = 299
+ elif change == "longer": terminal["timeout"]["secs"] = 301
+ elif change == "fractional": terminal["timeout"]["nanos"] = 1
+ elif change == "float": terminal["timeout"]["secs"] = 300.0
+ elif change == "bool": terminal["timeout"]["nanos"] = False
+ elif change == "missing": terminal.pop("timeout")
+ elif change == "unknown-duration-field": terminal["timeout"]["extra"] = 0
+ elif change == "missing-admission-timeout": admitted.pop("timeout")
+ elif change == "explicit-budget": admitted["timeout"] = {"secs": 120, "nanos": 0}
+ elif change == "consumed-budget": admitted["timeout"] = {"secs": 299, "nanos": 0}
+ elif change == "scope": terminal["pool_index"] = 1
+ elif change == "nolock": terminal["no_lock"] = True
+ else: terminal["extra"] = "foreign"
+ with self.subTest(change=change), self.assertRaises(ValueError):
+ persistence.retained_corruption(*self.fixture(admission_options=admitted, terminal_options=terminal))
+ for timeout in ({"secs": 120, "nanos": 0}, {"secs": 299, "nanos": 17}):
+ options = {**recorded["admissionOptions"], "timeout": timeout}
+ self.assertTrue(persistence.retained_corruption(*self.fixture(
+ admission_options=options, terminal_options=options))["durableOriginalFailureRetained"])
+ for change in ("float", "bool", "task", "scope", "options"):
+ data = self.fixture(admission_options=recorded["admissionOptions"], terminal_options=recorded["terminalOptions"])
+ row = data[1]["node"]["records"][5]
+ report = journal.decode_json(checkpoint.bytes_of(row))
+ if change == "float": report["terminal"]["options"]["timeout"]["secs"] = 300.0
+ elif change == "bool": report["terminal"]["options"]["timeout"]["nanos"] = False
+ elif change == "task": report["terminal"]["task_id"] = INCARNATION
+ elif change == "scope": report["terminal"]["heal_type"]["set_disk_id"] = "pool_0_set_1"
+ else: report["terminal"]["options"]["recursive"] = False
+ row.update(encoded(report))
+ with self.subTest(reportChange=change), self.assertRaises(ValueError): persistence.retained_corruption(*data)
+
+ def test_unreviewed_candidate_options_contract_fails_before_fleet_allocation(self):
+ with patch.object(persistence, "Fleet") as fleet, self.assertRaisesRegex(ValueError, "terminal-options contract"):
+ persistence.run_checkpoint_fault(None, "H1", ["node"], "DB", "checkpoint-truncate", {"source_sha": "f" * 40})
+ fleet.assert_not_called()
+
+ def test_missing_rewritten_moved_or_duplicated_corruption_never_passes(self):
+ for change in ("missing", "restored", "blocked", "resume", "moved", "duplicate", "partial-census"):
+ data = self.fixture(); observed, _, frozen, _, _ = data
+ rows = observed["node"]["records"]
+ if change == "missing": rows[4] = {"disk": 1, "relativePath": rows[4]["relativePath"], "present": False}
+ elif change == "restored": rows[4] = copy.deepcopy(frozen["checkpoint"])
+ elif change == "blocked": rows[6].update(encoded({"diagnostic": "invented"}))
+ elif change == "resume": rows[5].update(encoded({"completed": True}))
+ elif change == "moved": rows[0] = {**rows[4], "disk": 0}; rows[4] = {"disk": 1, "relativePath": rows[4]["relativePath"], "present": False}
+ elif change == "duplicate": observed["other"] = copy.deepcopy(observed["node"])
+ else: rows.pop()
+ with self.subTest(change=change), self.assertRaises(ValueError): persistence.retained_corruption(*data)
+
+ def test_injected_bytes_must_match_original_reviewed_transformation(self):
+ data = self.fixture(); data[3]["injected"] = journal.entry(b"unrelated corruption")
+ with self.assertRaisesRegex(ValueError, "reviewed transformation"): persistence.retained_corruption(*data)
+
+ def test_silent_success_cancelled_and_processed_object_claims_are_rejected(self):
+ for change in ("success", "cancelled", "processed", "undocumented"):
+ data = self.fixture(); current = data[-1]
+ if change == "success": current.update(summary="finished"); current["outcome"]["execution"] = {"state": "completed"}
+ elif change == "cancelled": current["outcome"]["execution"]["reason"] = "cancelled"
+ elif change == "processed": current["outcome"]["counters"]["healed"] = 1
+ else: current["detail"] = "generic failure"
+ with self.subTest(change=change), self.assertRaises(ValueError): persistence.retained_corruption(*data)
+
+ def test_original_terminal_report_identity_scope_and_failure_are_required(self):
+ for change in ("missing", "success", "new-task", "scope", "report", "owner"):
+ data = self.fixture(); roots = data[1]; rows = roots["node"]["records"]
+ value = journal.decode_json(checkpoint.bytes_of(rows[4]))
+ if change == "missing": rows[4] = {"disk": 1, "relativePath": rows[4]["relativePath"], "present": False}
+ elif change == "success": value["status"] = "Completed"; rows[4].update(encoded(value))
+ elif change == "new-task": value["task_id"] = INCARNATION; rows[4].update(encoded(value))
+ elif change == "scope": value["heal_type"] = {"type": "cluster"}; rows[4].update(encoded(value))
+ elif change == "report": rows[5].update(encoded({"schema": 1, "terminal": value, "outcome": {}}))
+ else: rows[1] = {**rows[4], "disk": 0}; rows[4] = {"disk": 1, "relativePath": rows[4]["relativePath"], "present": False}
+ with self.subTest(change=change), self.assertRaises(ValueError): persistence.retained_corruption(*data)
+
+ def rescan(self):
+ expected = [{"key": f"objects/{index:04d}.bin", "versionId": "null", "deleteMarker": False} for index in range(257)]
+ repairs = {(expected[-1]["key"], "null")}
+ current = {"summary": "finished", "settings": checkpoint.OPTIONS, "truncated": True,
+ "outcome": {"execution": {"state": "completed"}, "coverage": "complete", "objectsTruncated": True,
+ "counters": dict(processed=257, healed=1, unchanged=256, skipped=0, failed=0, unknown=0, attemptFailures=0, overflowed=False),
+ "objects": [{"identity": {"kind": "object", "bucket": BUCKET, "object": entry["key"],
+ "versionId": None, "bucketIncarnationId": INCARNATION, "poolIndex": 0, "setIndex": 0},
+ "disposition": {"state": "verified_healthy"}} for entry in expected[:128]]}}
+ return current, BUCKET, expected, repairs, INCARNATION
+
+ def test_bounded_diagnostics_need_complete_independent_manifests_and_exact_counters(self):
+ proof = persistence.rescan_outcome(*self.rescan())
+ self.assertTrue(proof["completeIndependentManifestsRequired"])
+ self.assertFalse(proof["boundedDiagnosticsAreCompleteManifest"])
+ self.assertFalse(proof["originalFailedTaskReclassified"])
+ for change in ("counter", "unknown", "duplicate", "foreign", "incarnation", "partial", "truncated"):
+ data = self.rescan(); outcome = data[0]["outcome"]
+ if change == "counter": outcome["counters"]["healed"] = True
+ elif change == "unknown": outcome["objects"][0]["disposition"]["state"] = "unknown"
+ elif change == "duplicate": outcome["objects"][-1] = copy.deepcopy(outcome["objects"][0])
+ elif change == "foreign": outcome["objects"][0]["identity"]["object"] = "foreign"
+ elif change == "incarnation": outcome["objects"][0]["identity"]["bucketIncarnationId"] = TOKEN
+ elif change == "partial": outcome["coverage"] = "partial"
+ else: outcome["objectsTruncated"] = False
+ with self.subTest(change=change), self.assertRaises(ValueError): persistence.rescan_outcome(*data)
+
+ def test_rescan_accepts_omitted_false_legacy_truncation_without_defaulting_invalid_values(self):
+ data = self.rescan()
+ del data[0]['truncated']
+ before = copy.deepcopy(data[0])
+ proof = persistence.rescan_outcome(*data)
+ self.assertTrue(proof['completeIndependentManifestsRequired'])
+ self.assertEqual(data[0], before)
+ self.assertTrue(data[0]['outcome']['objectsTruncated'])
+ for value in (None, 0, 1, 'false', []):
+ invalid = self.rescan()
+ invalid[0]['truncated'] = value
+ with self.subTest(value=value), self.assertRaisesRegex(ValueError, 'truthful coverage'):
+ persistence.rescan_outcome(*invalid)
+
+ def test_incomplete_branches_never_enter_fleet_mutation(self):
+ for branch in ("unknown", "native-persist-fence"):
+ with self.subTest(branch=branch), self.assertRaisesRegex(ValueError, "cannot mutate"):
+ persistence.run_checkpoint_fault(None, "H1", ["node"], "DB", branch, {})
+
+ def test_native_persistence_is_collected_and_failures_remain_independent(self):
+ with tempfile.TemporaryDirectory() as temp:
+ root = Path(temp)
+ native_dir = root / "native"
+ evidence = root / "evidence"
+ args = SimpleNamespace(native_directory=native_dir, evidence=evidence,
+ testing_commit="a" * 40)
+ candidate = {"source_sha": "1aa9be87d18cf941e5020f19e8622c26ba4b10ac"}
+ manifest = {"candidateCommit": candidate["source_sha"]}
+ proof = {**manifest, "complete": True, "exactTests": 6,
+ "canonicalCasePassClaimed": False, "fleetPassClaimed": False}
+ with patch.object(persistence.persistence_native, "binding", return_value=(manifest, root / "binary")), \
+ patch.object(persistence.persistence_native, "run") as run, \
+ patch.object(persistence.persistence_native, "verify", return_value=proof) as verify:
+ result = persistence.collect_native_persistence(args, candidate)
+ run.assert_called_once_with(native_dir / "native-tools", native_dir / "native-evidence",
+ evidence / "native-persistence", "a" * 40)
+ verify.assert_called_once_with(native_dir / "native-tools", native_dir / "native-evidence",
+ evidence / "native-persistence", "a" * 40)
+ self.assertEqual(result, proof)
+ self.assertEqual(json.loads((evidence / "independent-native-persistence.json").read_text()), proof)
+ branch = persistence.native_branch_result("H1", "DB", {"candidateCommit": candidate["source_sha"]}, result)
+ self.assertTrue(branch["requiredNativeCoverageComplete"])
+ self.assertFalse(branch["canonicalCasePassClaimed"])
+ self.assertFalse(branch["fleetBranchExecuted"])
+
+ failed_evidence = root / "failed"
+ failed_args = SimpleNamespace(native_directory=native_dir, evidence=failed_evidence,
+ testing_commit="a" * 40)
+ with patch.object(persistence.persistence_native, "binding", return_value=(manifest, root / "binary")), \
+ patch.object(persistence.persistence_native, "run", side_effect=RuntimeError("native failed")), \
+ patch.object(persistence.persistence_native, "verify") as verify_failed:
+ failure = persistence.collect_native_persistence(failed_args, candidate)
+ verify_failed.assert_not_called()
+ self.assertFalse(failure["complete"])
+ self.assertFalse(failure["canonicalCasePassClaimed"])
+ retained = json.loads((failed_evidence / "independent-native-persistence-failure.json").read_text())
+ self.assertEqual(retained["failure"]["message"], "native failed")
+ failed_branch = persistence.native_branch_result("H1", "DB", {"manager": True}, failure)
+ self.assertFalse(failed_branch["requiredNativeCoverageComplete"])
+ self.assertTrue(failed_branch["nativeFailureRetained"])
+ self.assertFalse(failed_branch["canonicalCasePassClaimed"])
+
+ def test_manager_and_directory_fsync_proofs_cannot_mix_candidates(self):
+ with self.assertRaisesRegex(ValueError, "mix candidates"):
+ persistence.native_branch_result("H1", "DB", {"candidateCommit": "a" * 40},
+ {"candidateCommit": "b" * 40, "complete": True})
+
+ def test_native_evidence_from_a_different_candidate_never_executes(self):
+ with tempfile.TemporaryDirectory() as temp:
+ root = Path(temp)
+ args = SimpleNamespace(native_directory=root / "native", evidence=root / "evidence",
+ testing_commit="a" * 40)
+ candidate = {"source_sha": "072b57b37559fbe626aeda8abf61617fdcbdd926"}
+ manifest = {"candidateCommit": "1aa9be87d18cf941e5020f19e8622c26ba4b10ac"}
+ with patch.object(persistence.persistence_native, "binding", return_value=(manifest, root / "binary")), \
+ patch.object(persistence.persistence_native, "run") as run, \
+ patch.object(persistence.persistence_native, "verify") as verify:
+ failure = persistence.collect_native_persistence(args, candidate)
+ run.assert_not_called()
+ verify.assert_not_called()
+ self.assertFalse(failure["complete"])
+ self.assertIn("native and fleet candidates differ", failure["failure"]["message"])
+ self.assertTrue((args.evidence / "independent-native-persistence-failure.json").is_file())
+
+ def test_native_proof_cannot_change_candidate_after_execution(self):
+ with tempfile.TemporaryDirectory() as temp:
+ root = Path(temp)
+ args = SimpleNamespace(native_directory=root / "native", evidence=root / "evidence",
+ testing_commit="a" * 40)
+ candidate = {"source_sha": "1aa9be87d18cf941e5020f19e8622c26ba4b10ac"}
+ manifest = {"candidateCommit": candidate["source_sha"]}
+ with patch.object(persistence.persistence_native, "binding", return_value=(manifest, root / "binary")), \
+ patch.object(persistence.persistence_native, "run"), \
+ patch.object(persistence.persistence_native, "verify", return_value={"complete": True, "candidateCommit": "b" * 40}):
+ failure = persistence.collect_native_persistence(args, candidate)
+ self.assertFalse(failure["complete"])
+ self.assertIn("changed the selected candidate", failure["failure"]["message"])
+ self.assertFalse((args.evidence / "independent-native-persistence.json").exists())
+
+ def test_native_branch_reuses_exact_evidence_without_claiming_fleet_or_full_case_pass(self):
+ with tempfile.TemporaryDirectory() as temp:
+ root = Path(temp); candidate = root / "candidate.json"
+ candidate.write_text(json.dumps({"source_sha": "1aa9be87d18cf941e5020f19e8622c26ba4b10ac"}))
+ args = ["persistence.py", "--case-id", persistence.CASE_ID, "--candidate", str(candidate),
+ "--evidence", str(root), "--rc-bin", str(root / "rc"), "--tool-directory", str(root / "tools"),
+ "--testing-commit", "a" * 40, "--binary-sha", "b" * 64]
+ proof = {"caseId": persistence.CASE_ID, "casePrerequisiteTests": list(range(10)),
+ "canonicalFleetBranchesExecuted": False, "canonicalCasePassClaimed": False}
+ def passed(args, topology, nodes, fixture, branch, candidate):
+ return {"topology": topology, "fixture": fixture, "branch": branch, "verdict": "PASS", "cleanup": "PASS"}
+ with patch("sys.argv", args), patch.dict(os.environ, {"GITHUB_RUN_ID": "12345"}), \
+ patch.object(persistence, "verify_manager", return_value=proof) as verify, \
+ patch.object(persistence, "run_mrf_checksum", side_effect=lambda args, topology, nodes, fixture, candidate: {
+ "topology": topology, "fixture": fixture, "branch": "mrf-checksum", "verdict": "PASS", "cleanup": "PASS"}) as mrf_mutate, \
+ patch.object(persistence, "run_checkpoint_fault", side_effect=passed) as mutate:
+ self.assertEqual(persistence.main(), 1)
+ verify.assert_called_once()
+ self.assertEqual(mutate.call_count, 16)
+ self.assertEqual(mrf_mutate.call_count, 4)
+ summary = json.loads((root / "healthy-summary.json").read_text())
+ self.assertEqual(len(summary["results"]), 24)
+ self.assertFalse(summary["complete"])
+ self.assertFalse(summary["canonicalCasePassClaimed"])
+ self.assertTrue(summary["diagnosticBranchesPassed"])
+ prerequisites = [row for row in summary["results"] if row["branch"] == "native-persist-fence"]
+ self.assertEqual(len(prerequisites), 4)
+ for row in prerequisites:
+ self.assertEqual(row["verdict"], "PREREQUISITE_PASS")
+ self.assertIsNone(row["topology"]); self.assertIsNone(row["fixture"])
+ self.assertFalse(row["fleetBranchExecuted"])
+ self.assertFalse(row["individualFsyncHooksClaimed"])
+ self.assertFalse(row["physicalPowerLossClaimed"])
+ self.assertFalse(row["requiredNativeCoverageComplete"])
+ self.assertEqual(row["nativeProof"], proof)
+ self.assertEqual(sum(row["verdict"] == "BLOCKED" for row in summary["results"]), 0)
+ def failed(*values):
+ row = passed(*values)
+ if row["branch"] == "stale-generation": row["verdict"] = "FAIL"
+ return row
+ failed_directory = root / "failed-diagnostic"; failed_directory.mkdir()
+ failed_args = args.copy(); failed_args[failed_args.index("--evidence") + 1] = str(failed_directory)
+ with patch("sys.argv", failed_args), patch.dict(os.environ, {"GITHUB_RUN_ID": "12345"}), \
+ patch.object(persistence, "verify_manager", return_value=proof), \
+ patch.object(persistence, "run_mrf_checksum", side_effect=lambda args, topology, nodes, fixture, candidate: {
+ "topology": topology, "fixture": fixture, "branch": "mrf-checksum", "verdict": "PASS", "cleanup": "PASS"}), \
+ patch.object(persistence, "run_checkpoint_fault", side_effect=failed):
+ self.assertEqual(persistence.main(), 1)
+ failed_summary = json.loads((failed_directory / "healthy-summary.json").read_text())
+ self.assertEqual(failed_summary["cleanup"], "PASS")
+ self.assertFalse(failed_summary["diagnosticBranchesPassed"])
+ self.assertFalse(failed_summary["canonicalCasePassClaimed"])
+
+ def stale(self):
+ observed, value, _ = fixture_source.AdminCheckpointTests().fixture()
+ value["admin"]["outcome"]["counters"].update(healed=0, unchanged=2)
+ observed["node"]["records"][4].update(encoded(value, signed=True))
+ early = checkpoint.owned_checkpoint(observed, TOKEN, BUCKET)
+ early["captureId"] = "F07-earlier-generation"
+ value["checkpoint_time"] += 1; value["current_object_index"] += 1
+ value["admin"]["outcome"]["counters"]["processed"] += 1
+ value["admin"]["outcome"]["counters"]["unchanged"] += 1
+ observed["node"]["records"][4].update(encoded(value, signed=True))
+ later = checkpoint.owned_checkpoint(observed, TOKEN, BUCKET)
+ later["rootAdmission"] = self.fixture()[2]["rootAdmission"]
+ receipt = {"family": "checkpoint", "taskId": TOKEN, "disk": 1,
+ "relativePath": later["checkpoint"]["relativePath"], "kind": "stale-generation",
+ "captureId": early["captureId"], "before": journal.entry(checkpoint.bytes_of(later["checkpoint"])),
+ "injected": journal.entry(checkpoint.bytes_of(early["checkpoint"])),
+ "capturedGeneration": journal.entry(checkpoint.bytes_of(early["checkpoint"])),
+ "applied": True, "originalBackupRetained": True, "productRepairClaimed": False}
+ return early, later, receipt
+
+ def completion_roots(self, frozen, current):
+ roots = self.fixture()[1]
+ terminal = {"schema": 1, "task_id": TOKEN, "heal_type": frozen["rootAdmission"]["value"]["heal_type"],
+ "options": frozen["rootAdmission"]["value"]["options"], "status": "Completed"}
+ roots["node"]["records"][4].update(encoded(terminal))
+ roots["node"]["records"][5].update(encoded({"schema": 1, "terminal": terminal, "outcome": current["outcome"]}))
+ return roots
+
+ def test_stale_valid_generation_is_exact_original_progress_without_cas_or_rejection_claim(self):
+ proof = persistence.stale_receipt(*self.stale())
+ self.assertTrue(proof["exactEarlierProductBytesInstalled"])
+ self.assertFalse(proof["oldCheckpointManagerCasClaimed"])
+ self.assertFalse(proof["staleRecordRejectionClaimed"])
+ for change in ("same-progress", "owner", "capture", "injected", "backup", "already-healed"):
+ early, later, receipt = self.stale()
+ if change == "same-progress": later = copy.deepcopy(early)
+ elif change == "owner": later["disk"] = 2
+ elif change == "capture": receipt["captureId"] = "F07-other"
+ elif change == "injected": receipt["injected"] = receipt["before"]
+ elif change == "backup": receipt["originalBackupRetained"] = False
+ else: early["value"]["admin"]["outcome"]["counters"].update(healed=1, unchanged=1)
+ with self.subTest(change=change), self.assertRaises(ValueError): persistence.stale_receipt(early, later, receipt)
+
+ def test_original_completion_needs_exact_admission_owner_scope_and_report(self):
+ _, frozen, _ = self.stale(); current = self.rescan()[0]
+ proof = persistence.completed_original(self.completion_roots(frozen, current), frozen, current)
+ self.assertTrue(proof["sameOriginalTaskAndPhysicalOwner"])
+ for change in ("runnable", "missing", "moved", "scope", "different-task", "different-report"):
+ roots = self.completion_roots(frozen, current); rows = roots["node"]["records"]
+ if change == "runnable": rows[3].update(encoded(frozen["rootAdmission"]["value"]))
+ elif change == "missing": rows[4] = {key: rows[4][key] for key in ("disk", "relativePath")}; rows[4]["present"] = False
+ elif change == "moved": rows[1] = {**rows[4], "disk": 0}; rows[4] = {key: rows[4][key] for key in ("disk", "relativePath")}; rows[4]["present"] = False
+ elif change == "different-report": rows[5].update(encoded({"schema": 1, "terminal": {}, "outcome": current["outcome"]}))
+ else:
+ value = journal.decode_json(checkpoint.bytes_of(rows[4]))
+ if change == "scope": value["heal_type"] = {"type": "cluster"}
+ else: value["task_id"] = INCARNATION
+ rows[4].update(encoded(value))
+ with self.subTest(change=change), self.assertRaises(ValueError): persistence.completed_original(roots, frozen, current)
+
+ def test_rejected_stale_generation_retains_original_bytes_and_failed_responsibility(self):
+ early, frozen, receipt = self.stale()
+ observed, roots, _, _, current = self.fixture()
+ observed["node"]["records"][4] = copy.deepcopy(early["checkpoint"])
+ proof = persistence.retained_corruption(observed, roots, frozen, receipt, current, earlier=early)
+ self.assertTrue(proof["durableOriginalFailureRetained"])
+ self.assertFalse(proof["originalTaskRepairClaimed"])
+ self.assertFalse(proof["backupRescueUsed"])
+ with self.assertRaises(ValueError): persistence.retained_corruption(observed, roots, frozen, receipt, current)
+
+ def write_failure(self):
+ observed, _, frozen, _, _ = self.fixture()
+ value = frozen["value"]
+ value["admin"]["outcome"]["counters"].update(healed=0, unchanged=2)
+ frozen["checkpoint"].update(encoded(value, signed=True))
+ observed["node"]["records"][4] = copy.deepcopy(frozen["checkpoint"])
+ observed["node"]["records"][6] = {"disk": 1, "relativePath": journal.names("checkpoint", TOKEN)[2], "present": False}
+ detail = "Task execution failed: Failed to save checkpoint: No space left on device (os error 28)"
+ current = {"summary": "stopped", "settings": checkpoint.OPTIONS, "detail": detail,
+ "outcome": {**copy.deepcopy(value["admin"]["outcome"]), "execution": {"state": "aborted", "reason": "untraversable"}}}
+ root_observed, root_value = fixture_source.AdminCheckpointTests().root_fixture()
+ frozen["rootAdmission"] = checkpoint.owned_root_admission(root_observed, TOKEN)
+ events = {"node": [{"target": "rustfs::heal::task", "message": "Heal task failed", "task_id": TOKEN, "error": detail}]}
+ return observed, root_observed, frozen, current, events
+
+ def test_actual_write_error_keeps_original_healthy_generation_and_admitted_owner(self):
+ proof = persistence.retained_write_failure(*self.write_failure())
+ self.assertTrue(proof["lastDurableProgressRetained"])
+ self.assertTrue(proof["originalAdmissionRetained"])
+ self.assertFalse(proof["failedWriteAcknowledged"])
+ self.assertFalse(proof["originalFailureReclassified"])
+ for change in ("success", "wrong-error", "invented-progress", "missing-checkpoint", "rewrite-resume", "missing-owner", "wrong-task-event"):
+ observed, roots, frozen, current, events = self.write_failure()
+ if change == "success": current["summary"] = "finished"
+ elif change == "wrong-error": current["detail"] = "Failed to save checkpoint: Permission denied"
+ elif change == "invented-progress": current["outcome"]["counters"]["processed"] += 1
+ elif change == "missing-checkpoint": observed["node"]["records"][4]["present"] = False
+ elif change == "rewrite-resume": observed["node"]["records"][5].update(encoded({"completed": True}))
+ elif change == "missing-owner": roots["node"]["records"][3]["present"] = False
+ else: events["node"][0]["task_id"] = INCARNATION
+ with self.subTest(change=change), self.assertRaises(ValueError): persistence.retained_write_failure(observed, roots, frozen, current, events)
+
+ def test_known_write_failure_can_have_explicit_failed_terminal_without_silent_success(self):
+ observed, roots, frozen, current, events = self.write_failure()
+ roots["node"]["records"][3] = {"disk": 1, "relativePath": journal.names("root", TOKEN)[0], "present": False}
+ terminal = {"schema": 1, "task_id": TOKEN, "heal_type": frozen["rootAdmission"]["value"]["heal_type"],
+ "options": frozen["rootAdmission"]["value"]["options"], "status": {"Failed": {"error": current["detail"]}}}
+ roots["node"]["records"][4].update(encoded(terminal))
+ roots["node"]["records"][5].update(encoded({"schema": 1, "terminal": terminal, "outcome": current["outcome"]}))
+ proof = persistence.retained_write_failure(observed, roots, frozen, current, events)
+ self.assertTrue(proof["originalResponsibilityRetained"])
+ self.assertFalse(proof["originalAdmissionRetained"])
+ terminal["status"] = "Completed"; roots["node"]["records"][4].update(encoded(terminal))
+ with self.assertRaises(ValueError): persistence.retained_write_failure(observed, roots, frozen, current, events)
+
+ def test_checkpoint_write_failure_terminal_keeps_the_source_default_and_exact_scope(self):
+ recorded = self.recorded_options()
+ for change in (None, "timeout", "scope", "report"):
+ observed, roots, frozen, current, events = self.write_failure()
+ admission = frozen["rootAdmission"]["value"]
+ admission["options"] = copy.deepcopy(recorded["admissionOptions"])
+ frozen["rootAdmission"].update(**encoded(admission))
+ roots["node"]["records"][3] = {"disk": 1, "relativePath": journal.names("root", TOKEN)[0], "present": False}
+ terminal = {"schema": 1, "task_id": TOKEN, "heal_type": admission["heal_type"],
+ "options": copy.deepcopy(recorded["terminalOptions"]), "status": {"Failed": {"error": current["detail"]}}}
+ if change == "timeout": terminal["options"]["timeout"]["secs"] = 301
+ elif change == "scope": terminal["options"]["set_index"] = 1
+ report = {"schema": 1, "terminal": copy.deepcopy(terminal), "outcome": current["outcome"]}
+ if change == "report": report["terminal"]["options"]["timeout"]["secs"] = 299
+ roots["node"]["records"][4].update(encoded(terminal))
+ roots["node"]["records"][5].update(encoded(report))
+ if change is None:
+ self.assertTrue(persistence.retained_write_failure(observed, roots, frozen, current, events)["originalResponsibilityRetained"])
+ else:
+ with self.subTest(change=change), self.assertRaises(ValueError):
+ persistence.retained_write_failure(observed, roots, frozen, current, events)
+
+ def test_write_failure_can_consume_root_timeout_without_refunding_or_changing_intent(self):
+ for change in (None, "refunded", "removed", "scope", "retry", "owner"):
+ observed, roots, frozen, current, events = self.write_failure()
+ before = copy.deepcopy(frozen["rootAdmission"]["value"])
+ before["options"]["timeout"] = {"secs": 120, "nanos": 0}
+ frozen["rootAdmission"].update(value=before, **encoded(before))
+ after = copy.deepcopy(before)
+ after["options"]["timeout"] = {"secs": 90, "nanos": 0}
+ if change == "refunded": after["options"]["timeout"]["secs"] = 121
+ elif change == "removed": after["options"].pop("timeout")
+ elif change == "scope": after["options"]["pool_index"] = 1
+ elif change == "retry": after["retry_attempts"] = 1
+ roots["node"]["records"][3].update(encoded(after))
+ if change == "owner":
+ roots["node"]["records"][0].update(encoded(after))
+ roots["node"]["records"][3] = {"disk": 1, "relativePath": journal.names("root", TOKEN)[0], "present": False}
+ if change is None:
+ self.assertTrue(persistence.retained_write_failure(observed, roots, frozen, current, events)["originalResponsibilityRetained"])
+ else:
+ with self.subTest(change=change), self.assertRaises(ValueError):
+ persistence.retained_write_failure(observed, roots, frozen, current, events)
+
+ def test_write_failure_live_census_requires_proven_frozen_processes(self):
+ for change in (None, "serviceFrozen", "stable", "processBound", "independentReads"):
+ observed, roots, frozen, current, events = self.write_failure()
+ for census in (observed["node"], roots["node"]):
+ census.update(quiescent=False, stable=True, serviceFrozen=True, processBound=True, independentReads=2)
+ if change is None:
+ persistence.retained_write_failure(observed, roots, frozen, current, events)
+ else:
+ roots["node"][change] = 1 if change == "independentReads" else False
+ with self.subTest(change=change), self.assertRaises(ValueError):
+ persistence.retained_write_failure(observed, roots, frozen, current, events)
+
+ def test_write_refusal_release_rescan_and_real_original_retry_have_distinct_evidence(self):
+ for change in (None, "wrong-mount-witness", "release-failure", "late-export-failure", "pending-owner"):
+ observed, roots, frozen, failed, events = self.write_failure()
+ expected = [{"key": f"objects/{i:04d}.bin", "versionId": "null", "deleteMarker": False} for i in range(3)]
+ def finished(heal):
+ return {"summary": "finished", "settings": checkpoint.OPTIONS, "truncated": False,
+ "outcome": {"execution": {"state": "completed"}, "coverage": "complete", "objectsTruncated": False,
+ "counters": dict(processed=3, healed=heal, unchanged=3-heal, skipped=0, failed=0, unknown=0, attemptFailures=0, overflowed=False),
+ "objects": [{"identity": {"kind": "object", "bucket": BUCKET, "object": entry["key"], "versionId": None,
+ "bucketIncarnationId": INCARNATION, "poolIndex": 0, "setIndex": 0},
+ "disposition": {"state": "repaired" if heal and i == 2 else "verified_healthy"}} for i, entry in enumerate(expected)]}}
+ rescanned, final = finished(1), finished(0)
+ final_roots = self.completion_roots(frozen, final)
+ identity = {field: field for field in persistence.PROCESS_FIELDS}; identity["pid"] = 50
+ target = {"node": "node", "disk": 1, "relativePath": "bound/part.1", "key": expected[-1]["key"], "versionId": "null"}
+ repaired = {"node": {"files": [{"disk": 1, "path": "bound/part.1", "sha256": "a" * 64}], "formats": []}}
+ issued = []
+ class WriteFleet:
+ nodes, frozen_processes, faults = ["node"], {}, [("node", {"faultId": "F07-shard-0"})]
+ paused = False
+ def start(self): issued.append("start")
+ def stop(self): issued.append("stop")
+ def record(self, label, value):
+ issued.append(label)
+ if change == "late-export-failure" and label == "complete-original-journal-write-reconciliation": raise OSError("export failed")
+ return value
+ def snapshot(self, label, **kwargs):
+ if label == "original-journal-write-failure-physical": return {"node": {"files": [], "formats": []}}
+ return repaired
+ def remote(self, node, action, **params):
+ issued.append(action)
+ if action == "bounded-volumes-fill": return {"quiescent": True, "hostSharedMountRemounted": False,
+ "candidateProcessWitnessRequired": True, "changes": [{"actualErrno": 28, "freeBlocks": 0}] * 4}
+ if action == "bounded-volumes-witness": return {"sameOriginalProcess": True, "hostSharedMountRemounted": False,
+ "disks": [{"writeErrno": 28, "sameServiceUidAndMountNamespace": change != "wrong-mount-witness"}] * 4}
+ if action == "bounded-volumes-release": return {"sameOriginalProcess": True, "hostSharedMountRemounted": False,
+ "disks": [{"writeErrno": 28 if change == "release-failure" else 0, "freeBlocks": 1}] * 4}
+ if action == "process": return {**identity, "state": "T" if self.paused else "S"}
+ if action == "process-signal": self.paused = params["signal"] == "SIGSTOP"; return {}
+ raise AssertionError(action)
+ fleet = WriteFleet()
+ active = [(checkpoint.PATH, TOKEN)]
+ def records(fleet, token, label, **kwargs):
+ if label == "retained-write-admission-after-reads": return final_roots
+ return roots if kwargs.get("family") == "root" else observed
+ def run_task(*args, **kwargs): kwargs["admissions"].append({"clientToken": INCARNATION}); return rescanned
+ def recover_pending(*args):
+ issued.append("recover-pending-original-before-new-task")
+ if change == "pending-owner": raise ValueError("original durable scope remains owned")
+ return None
+ window = SimpleNamespace(events=events, capture=lambda: events)
+ with patch.object(persistence, "JournalWindow", return_value=window), \
+ patch.object(persistence, "recover_write_before_rescan", create=True, side_effect=recover_pending), \
+ patch.object(persistence, "recovery_status", side_effect=[failed, failed, final]), \
+ patch.object(persistence.checkpoints, "records", side_effect=records), \
+ patch.object(persistence, "validate_repaired_physical"), patch.object(persistence, "verify_proof"), \
+ patch.object(persistence, "idle_fleet"), patch.object(persistence, "run_task", side_effect=run_task), \
+ patch.object(persistence, "status", return_value=final), \
+ patch.object(persistence, "Probe", return_value=SimpleNamespace(inventory=lambda *a, **kw: {})), \
+ patch.object(persistence, "verify_seed"), patch.object(persistence, "compare_inventories"):
+ args = (fleet, None, {"endpoints": {"node1": "http://owned"}}, expected, {}, repaired, [target], frozen, active)
+ if change is None:
+ result = persistence.journal_write_failure(*args)
+ self.assertTrue(result["originalWriteFailure"]["lastDurableProgressRetained"])
+ self.assertTrue(result["originalAfterRestart"]["realSameTaskReplayAfterSeparateRescan"])
+ self.assertFalse(result["originalAfterRestart"]["separateRescanIsOriginalTaskCompletion"])
+ self.assertFalse(result["canonicalCasePassClaimed"])
+ self.assertFalse(fleet.faults)
+ else:
+ with self.assertRaises((ValueError, OSError)): persistence.journal_write_failure(*args)
+ self.assertTrue(fleet.faults)
+ if change == "wrong-mount-witness": self.assertNotIn("bounded-volumes-release", issued)
+ if change == "pending-owner":
+ self.assertIn("recover-pending-original-before-new-task", issued)
+ self.assertNotIn("journal-rescan-complete-physical-before-any-GET", issued)
+
+ def test_pending_write_owner_recovers_before_new_rescan_without_reclassifying_first_failure(self):
+ cases = (None, "failed-terminal", "still-owned", "wrong-terminal", "streaming-error", "rescan-heals",
+ "same-token", "peer-disagrees", "read-repair", "changed-terminal", "export-error",
+ "pre-recovery-drift", "recovery-timeout", "late-recovery-completion")
+ for change in cases:
+ with self.subTest(change=change):
+ observed, failed_roots, frozen, failed, events = self.write_failure()
+ current, _, expected, _, _ = self.rescan()
+ final_roots = self.completion_roots(frozen, current)
+ failed_terminal = copy.deepcopy(failed_roots)
+ failed_terminal["node"]["records"][3] = {"disk": 1, "relativePath": journal.names("root", TOKEN)[0], "present": False}
+ terminal = {"schema": 1, "task_id": TOKEN, "heal_type": frozen["rootAdmission"]["value"]["heal_type"],
+ "options": frozen["rootAdmission"]["value"]["options"], "status": {"Failed": {"error": failed["detail"]}}}
+ failed_terminal["node"]["records"][4].update(encoded(terminal))
+ failed_terminal["node"]["records"][5].update(encoded({"schema": 1, "terminal": terminal, "outcome": failed["outcome"]}))
+ first_failed = copy.deepcopy(failed)
+ failure_scene = {"node": {"files": [], "formats": []}}
+ healed = {"node": {"files": [{"disk": 1, "path": "bound/part.1", "sha256": "a" * 64}], "formats": []}}
+ issued, exports = [], {}
+ class ReplayFleet:
+ faults = [("node", {"faultId": "F07-shard-0"})]
+ def start(self): issued.append("start")
+ def stop(self): issued.append("stop")
+ def record(self, label, value):
+ issued.append(label)
+ if change == "export-error" and label == "complete-original-journal-write-reconciliation": raise OSError("lost reconciliation export")
+ exports[label] = copy.deepcopy(value); return value
+ def snapshot(self, label, **kwargs):
+ issued.append(label)
+ if label == "write-failure-before-original-recovery" and change == "pre-recovery-drift": return copy.deepcopy(healed)
+ if label in ("write-failure-before-original-recovery", "write-original-failed-recovery-physical"): return copy.deepcopy(failure_scene)
+ if label == "write-replay-after-rescan-and-reads" and change == "read-repair": return copy.deepcopy(failure_scene)
+ return copy.deepcopy(healed)
+ fleet = ReplayFleet()
+ def records(fleet, token, label, **kwargs):
+ self.assertEqual(token, TOKEN)
+ if kwargs.get("family") != "root": return copy.deepcopy(observed)
+ if label == "write-failure-root-before-recovery": return copy.deepcopy(failed_roots)
+ if change == "still-owned": return copy.deepcopy(failed_roots)
+ if change == "failed-terminal": return copy.deepcopy(failed_terminal)
+ result = copy.deepcopy(final_roots)
+ if change == "wrong-terminal" or change == "changed-terminal" and label == "retained-write-original-completion":
+ report = journal.decode_json(checkpoint.bytes_of(result["node"]["records"][5]))
+ if change == "wrong-terminal": report["outcome"]["counters"]["healed"] = 0
+ else: report["unrelated"] = "changed after rescan"
+ result["node"]["records"][5].update(encoded(report))
+ return result
+ def run_task(*args, **kwargs):
+ self.assertIn("write-original-replay-complete-physical-proof", issued)
+ self.assertIn("write-original-replay-full-shard-integrity", issued)
+ kwargs["admissions"].append({"clientToken": TOKEN if change == "same-token" else INCARNATION})
+ issued.append("independent-new-rescan")
+ rescan = copy.deepcopy(current)
+ if change != "rescan-heals": rescan["outcome"]["counters"].update(healed=0, unchanged=257)
+ return rescan
+ def verify(*args):
+ issued.append("write-original-replay-full-shard-integrity")
+ if change == "streaming-error": raise ValueError("actual cohort failed streaming proof")
+ proof = persistence.retained_write_failure(observed, failed_roots, frozen, failed, events)
+ target = {"node": "node", "disk": 1, "relativePath": "bound/part.1", "key": expected[-1]["key"], "versionId": "null"}
+ active = []
+ with patch.object(persistence.checkpoints, "records", side_effect=records), \
+ patch.object(persistence, "validate_repaired_physical"), patch.object(persistence, "verify_proof", side_effect=verify), \
+ patch.object(persistence, "recovery_status", return_value={"summary": "running"} if change == "recovery-timeout" else failed if change in ("failed-terminal", "still-owned") else current), \
+ patch.object(persistence, "run_task", side_effect=run_task), \
+ patch.object(persistence, "status", return_value=failed if change == "peer-disagrees" else current), \
+ patch.object(persistence, "Probe", return_value=SimpleNamespace(inventory=lambda *a, **kw: {})), \
+ patch.object(persistence, "verify_seed"), patch.object(persistence, "compare_inventories"), \
+ patch.object(persistence.time, "monotonic", return_value=0,
+ side_effect=[0, .01, 181] if change in ("recovery-timeout", "late-recovery-completion") else None):
+ args = (fleet, None, {"endpoints": {"node1": "http://owned"}}, expected, {}, healed, [target],
+ frozen, failed, proof, failure_scene, [target], events, active)
+ if change in (None, "failed-terminal"):
+ result = persistence.recover_write_before_rescan(*args)
+ if change is None:
+ self.assertTrue(result["originalAfterRestart"]["realSameTaskReplayBeforeSeparateRescan"])
+ self.assertFalse(result["originalAfterRestart"]["separateRescanIsOriginalTaskCompletion"])
+ self.assertFalse(result["canonicalCasePassClaimed"])
+ self.assertFalse(fleet.faults)
+ self.assertEqual(exports["complete-original-journal-write-reconciliation"]["firstFailedAttempt"], first_failed)
+ else:
+ self.assertIsNone(result); self.assertTrue(fleet.faults)
+ self.assertNotIn("independent-new-rescan", issued)
+ else:
+ with self.assertRaises((ValueError, OSError)): persistence.recover_write_before_rescan(*args)
+ self.assertTrue(fleet.faults)
+ if change in ("still-owned", "wrong-terminal", "streaming-error"):
+ self.assertNotIn("independent-new-rescan", issued)
+ self.assertEqual(failed, first_failed)
+ self.assertEqual(exports["original-write-failure-before-recovery"]["firstFailedAttempt"], first_failed)
+
+ def test_successful_replay_cannot_discard_rescue_before_manifest_terminal_and_evidence_agree(self):
+ for change in (None, "read-repair", "late-archive-failure", "rescan-relabels-original"):
+ early, frozen, receipt = self.stale(); current, _, expected, _, _ = self.rescan()
+ roots = self.completion_roots(frozen, current)
+ repaired = {"node": {"files": [{"disk": 1, "path": "bound/part.1", "sha256": "a" * 64}], "formats": []}}
+ events, active = [], [(checkpoint.PATH, TOKEN)]
+ class ReplayFleet:
+ journal_faults, faults = [("node", {"faultId": "F07-stale-generation"})], [("node", {"disk": 1})]
+ def start(self): pass
+ def stop(self): pass
+ def record(self, label, value):
+ events.append(label)
+ if change == "late-archive-failure" and label == "complete-original-generation-reconciliation": raise OSError("evidence write failed")
+ return value
+ def snapshot(self, label, **kwargs):
+ scene = copy.deepcopy(repaired)
+ if change == "read-repair" and label == "physical-after-rescan-and-business-reads": scene["node"]["files"] = []
+ return scene
+ fleet = ReplayFleet()
+ rescan = copy.deepcopy(current); rescan["outcome"]["counters"].update(healed=0, unchanged=257)
+ def run_task(*args, **kwargs):
+ kwargs["admissions"].append({"clientToken": TOKEN if change == "rescan-relabels-original" else INCARNATION})
+ return rescan
+ target = {"node": "node", "disk": 1, "relativePath": "bound/part.1",
+ "key": expected[-1]["key"], "versionId": "null"}
+ binding = {"endpoints": {"node1": "http://owned"}}
+ with patch.object(persistence, "rescan_outcome", wraps=persistence.rescan_outcome), \
+ patch.object(persistence.checkpoints, "records", return_value=roots), patch.object(persistence, "idle_fleet"), \
+ patch.object(persistence, "validate_repaired_physical"), patch.object(persistence, "verify_proof"), \
+ patch.object(persistence, "run_task", side_effect=run_task), \
+ patch.object(persistence, "recovery_status", return_value=current), patch.object(persistence, "status", return_value=current), \
+ patch.object(persistence, "Probe", return_value=SimpleNamespace(inventory=lambda *a, **kw: {})), \
+ patch.object(persistence, "verify_seed"), patch.object(persistence, "compare_inventories"):
+ args = (fleet, None, binding, expected, {}, repaired, [target], frozen, early, receipt,
+ {"node": {"files": [], "formats": []}}, current, active)
+ if change is None:
+ result = persistence.successful_stale_replay(*args)
+ self.assertFalse(result["canonicalCasePassClaimed"])
+ self.assertFalse(result["staleRecordRejectionClaimed"])
+ self.assertIn("complete-original-generation-reconciliation", events)
+ self.assertFalse(fleet.journal_faults); self.assertFalse(fleet.faults)
+ else:
+ with self.assertRaises((ValueError, OSError)): persistence.successful_stale_replay(*args)
+ self.assertTrue(fleet.journal_faults); self.assertTrue(fleet.faults)
+
+ def interrupted_injection(self, cleanup, mutation=None):
+ frozen = self.write_failure()[2] if mutation == "fill" else self.fixture()[2]
+ snapshots = {"node": {"files": [], "formats": []}}
+ captured = {}
+
+ class InterruptedFleet:
+ def __init__(self, *args, **kwargs):
+ self.faults, self.journal_faults = [], []
+ self.nodes = ["node"]
+ captured["fleet"] = self
+
+ def record(self, label, value): return value
+ def deploy(self, *args, **kwargs): pass
+ def start(self): pass
+ def stop(self): pass
+ def snapshot(self, *args, **kwargs): return snapshots
+
+ def remote(self, node, action, **params):
+ if action == "journal-apply":
+ if mutation == "runtime": return {}
+ # The node may have committed the fault before the ACK was lost.
+ raise OSError("journal injection acknowledgement lost")
+ if action == "process": return {"settings": {"RUSTFS_HEAL_TASK_TIMEOUT_SECS": "299"}}
+ if action == "bounded-volumes-fill": raise OSError("private journal fill acknowledgement lost")
+ return {}
+
+ def cleanup(self, *, preserve_namespaces=False):
+ captured["cleanupFaults"] = copy.deepcopy(self.faults)
+ captured["cleanupJournals"] = copy.deepcopy(self.journal_faults)
+ captured["preserveNamespaces"] = preserve_namespaces
+ return cleanup
+
+ class FixtureProbe:
+ def __init__(self, *args): pass
+ def inventory(self, *args, **kwargs): return {}
+
+ target = {"node": "node", "disk": 1, "relativePath": "bound/part.1", "sha256": "0" * 64}
+ expected = [{"key": f"objects/{index:04d}.bin", "versionId": "null", "deleteMarker": False}
+ for index in range(257)]
+ with tempfile.TemporaryDirectory() as temporary:
+ directory = Path(temporary)
+ rc = directory / "rc"; rc.write_bytes(b"test-rc")
+ args = SimpleNamespace(evidence=directory, tool_directory=directory,
+ testing_commit="0" * 40, binary_sha="0" * 64, rc_bin=rc)
+ with patch.dict(persistence.os.environ, {"GITHUB_RUN_ID": "1"}), \
+ patch.object(persistence, "Fleet", InterruptedFleet), patch.object(persistence, "Probe", FixtureProbe), \
+ patch.object(persistence, "verify_inspector", return_value={}), \
+ patch.object(persistence, "validate_topology", return_value={}), \
+ patch.object(persistence, "seed", return_value=expected,
+ side_effect=OSError("seed acknowledgement lost") if mutation == "seed" else None), \
+ patch.object(persistence, "verify_seed", return_value={}), \
+ patch.object(persistence, "idle_fleet"), patch.object(persistence, "map_target", return_value=target), \
+ patch.object(persistence, "verify_proof"), patch.object(persistence, "validate_fault"), \
+ patch.object(persistence.checkpoints, "admit", return_value=TOKEN,
+ side_effect=OSError("admission acknowledgement lost") if mutation == "admission" else None), \
+ patch.object(persistence.checkpoints, "freeze", return_value=(frozen, {})), \
+ patch.object(persistence, "JournalWindow"), \
+ patch.object(persistence.checkpoints, "stop_for_fault", return_value=frozen):
+ result = persistence.run_checkpoint_fault(args, "H1", ["node"], "DB",
+ "journal-write-failure" if mutation == "fill" else "checkpoint-truncate",
+ {"source_sha": "1aa9be87d18cf941e5020f19e8622c26ba4b10ac", "package_sha256": "0" * 64})
+ return result, captured
+
+ def test_lost_injection_ack_keeps_original_journal_and_shard_rescue_registered(self):
+ result, captured = self.interrupted_injection(True)
+ self.assertEqual(result["verdict"], "FAIL")
+ self.assertEqual(result["cleanup"], "PASS")
+ self.assertEqual(result["failure"]["message"], "journal injection acknowledgement lost")
+ self.assertEqual(len(captured["cleanupFaults"]), 4)
+ self.assertEqual(len(captured["cleanupJournals"]), 1)
+ self.assertEqual(captured["cleanupJournals"][0][1]["taskId"], TOKEN)
+ self.assertNotIn("separateRescan", result)
+
+ def test_restart_runtime_change_preserves_rescue_and_refuses_terminal_normalization(self):
+ with patch.object(persistence, "recovery_status") as queried:
+ result, captured = self.interrupted_injection(True, "runtime")
+ self.assertEqual((result["verdict"], result["cleanup"]), ("FAIL", "PASS"))
+ self.assertIn("runtime timeout witness", result["failure"]["message"])
+ self.assertEqual(len(captured["cleanupJournals"]), 1)
+ self.assertEqual(len(captured["cleanupFaults"]), 4)
+ queried.assert_not_called()
+
+ def test_failed_cleanup_preserves_dirty_result_after_lost_injection_ack(self):
+ result, captured = self.interrupted_injection(False)
+ self.assertEqual((result["verdict"], result["cleanup"]), ("FAIL", "FAIL"))
+ self.assertEqual(len(captured["cleanupJournals"]), 1)
+ self.assertNotIn("completePhysicalAndS3ManifestsVerified", result)
+
+ def test_unknown_fixture_or_original_admission_preserves_namespace_even_if_cleanup_reports_success(self):
+ for mutation in ("seed", "admission"):
+ with self.subTest(mutation=mutation):
+ result, captured = self.interrupted_injection(True, mutation)
+ self.assertEqual((result["verdict"], result["cleanup"]), ("FAIL", "FAIL"))
+ self.assertTrue(captured["preserveNamespaces"])
+ self.assertIn("unresolvedMutation", result)
+ self.assertNotIn("separateRescan", result)
+
+ def test_lost_private_fill_ack_keeps_shard_rescue_and_failure_before_any_positive_rescan(self):
+ result, captured = self.interrupted_injection(False, "fill")
+ self.assertEqual((result["verdict"], result["cleanup"]), ("FAIL", "FAIL"))
+ self.assertEqual(result["failure"]["message"], "private journal fill acknowledgement lost")
+ self.assertEqual(len(captured["cleanupFaults"]), 4)
+ self.assertFalse(captured["cleanupJournals"])
+ self.assertNotIn("separateRescan", result)
+
+
+if __name__ == "__main__": unittest.main()
diff --git a/test_validation_heal_persistence_mrf.py b/test_validation_heal_persistence_mrf.py
new file mode 100644
index 00000000..f84c32d0
--- /dev/null
+++ b/test_validation_heal_persistence_mrf.py
@@ -0,0 +1,441 @@
+"""Original committed MRF corruption must remain distinct from safe rescan."""
+
+import base64
+import copy
+from contextlib import ExitStack
+import hashlib
+import json
+import os
+from pathlib import Path
+import stat
+import tempfile
+from types import SimpleNamespace
+import unittest
+import struct
+import zlib
+from unittest.mock import patch
+
+from scripts.validation_heal import journal_fault as journal, persistence_mrf as suite, drive_gate, fleet_node, heal_fault
+import test_validation_heal_mrf as fixtures
+
+
+class MrfCorruptionTests(unittest.TestCase):
+ def bucket_gate_fixture(self, root):
+ state, disk = root / "state", root / "disk"
+ state.mkdir()
+ data = disk / "data"; data.mkdir(parents=True, mode=0o700)
+ system = data / ".rustfs.sys"; system.mkdir(mode=0o700)
+ (system / "format.json").write_bytes(b"format-control")
+ bucket = data / "rv-heal-0123456789abcdef"
+ bucket.mkdir(mode=0o755)
+ obj = bucket / "objects/0000.bin"; obj.mkdir(parents=True)
+ (obj / "xl.meta").write_bytes(b"original-fixture-metadata")
+ (state / "owner.json").write_text(json.dumps({"runtimeProfile": "mrf-crash", "bucket": bucket.name}))
+ return state, disk, bucket, system
+
+ def rooted_lstat(self):
+ original = Path.lstat
+ def observe(path):
+ value = original(path)
+ return SimpleNamespace(st_mode=value.st_mode, st_uid=0, st_nlink=value.st_nlink,
+ st_size=value.st_size, st_dev=value.st_dev, st_ino=value.st_ino)
+ return patch.object(Path, "lstat", observe)
+
+ def test_bucket_gate_denies_only_owned_business_root_and_restores_original_permissions(self):
+ with tempfile.TemporaryDirectory() as temporary:
+ state, disk, bucket, system = self.bucket_gate_fixture(Path(temporary))
+ original = {path: (path.stat().st_mode, path.stat().st_ino) for path in (disk / "data", bucket, system)}
+ with self.rooted_lstat():
+ armed = drive_gate.apply_gate(state, [disk], 0, "permission-denied", scope="bucket")
+ self.assertEqual((armed["scope"], armed["dataPath"]), ("bucket", str(bucket)))
+ drive_gate.activate_gate(state, [disk])
+ self.assertEqual(stat.S_IMODE(bucket.stat().st_mode), 0)
+ self.assertEqual((system.stat().st_mode, system.stat().st_ino), original[system])
+ self.assertEqual((disk / "data").stat().st_mode, original[disk / "data"][0])
+ self.assertEqual((system / "format.json").read_bytes(), b"format-control")
+ self.assertTrue(drive_gate.release_gate(state, [disk])["originalPermissionsRestored"])
+ self.assertEqual((bucket.stat().st_mode, bucket.stat().st_ino), original[bucket])
+ self.assertEqual((bucket / "objects/0000.bin/xl.meta").read_bytes(), b"original-fixture-metadata")
+
+ def test_bucket_gate_refuses_foreign_runtime_path_links_and_changed_scope(self):
+ for change in ("runtime", "bucket", "scope", "mode", "linked-bucket", "linked-objects", "linked-object", "hardlinked-meta"):
+ with self.subTest(change=change), tempfile.TemporaryDirectory() as temporary:
+ state, disk, bucket, _ = self.bucket_gate_fixture(Path(temporary))
+ owner = json.loads((state / "owner.json").read_text())
+ if change == "runtime": owner["runtimeProfile"] = "incarnation-mrf"
+ elif change == "bucket": owner["bucket"] = "../foreign"
+ elif change == "hardlinked-meta": os.link(bucket / "objects/0000.bin/xl.meta", bucket / "linked-meta")
+ elif change.startswith("linked-"):
+ folder = {"linked-bucket": bucket, "linked-objects": bucket / "objects", "linked-object": bucket / "objects/0000.bin"}[change]
+ held = folder.with_name(folder.name + "-held"); folder.rename(held); folder.symlink_to(held, target_is_directory=True)
+ (state / "owner.json").write_text(json.dumps(owner))
+ with self.rooted_lstat(), self.assertRaises(ValueError):
+ drive_gate.apply_gate(state, [disk], 0, "read-only" if change == "mode" else "permission-denied",
+ scope="foreign" if change == "scope" else "bucket")
+ self.assertFalse((state / "drive-gate.json").exists())
+
+ def test_bucket_gate_marker_cannot_redirect_activation_or_cleanup(self):
+ with tempfile.TemporaryDirectory() as temporary:
+ state, disk, bucket, system = self.bucket_gate_fixture(Path(temporary))
+ with self.rooted_lstat():
+ drive_gate.apply_gate(state, [disk], 0, "permission-denied", scope="bucket")
+ marker = state / "drive-gate.json"; value = json.loads(marker.read_text())
+ value["dataPath"] = str(system); marker.write_text(json.dumps(value)); marker.chmod(0o600)
+ with self.assertRaises(ValueError): drive_gate.activate_gate(state, [disk])
+ with self.assertRaises(ValueError): drive_gate.release_gate(state, [disk])
+ self.assertEqual(stat.S_IMODE(bucket.stat().st_mode), 0o755)
+ self.assertEqual(stat.S_IMODE(system.stat().st_mode), 0o700)
+
+ def test_bucket_probe_keeps_same_candidate_namespace_and_checks_system_read_and_write(self):
+ with tempfile.TemporaryDirectory() as temporary:
+ state, disk, bucket, _ = self.bucket_gate_fixture(Path(temporary))
+ for failure in (None, "system-read", "system-write", "system-identity"):
+ denied = SimpleNamespace(returncode=0, stdout=json.dumps({"uid": 0, "gid": 0, "errno": 13}))
+ control = {"uid": 0, "gid": 0, "errno": 0}
+ write, read = copy.deepcopy(control), copy.deepcopy(control)
+ if failure == "system-read": read["errno"] = 13
+ elif failure == "system-write": write["errno"] = 13
+ elif failure == "system-identity": write["uid"] = 1000
+ outputs = [denied, denied, *(SimpleNamespace(returncode=0, stdout=json.dumps(x)) for x in (write, read))]
+ value = {"mode": "permission-denied", "scope": "bucket", "phase": "active", "dataPath": str(bucket), "disk": 0}
+ with self.rooted_lstat(), patch.object(drive_gate, "checked_gate", return_value=value), \
+ patch.object(drive_gate, "process_identity", return_value={"uid": 0, "gid": 0, "capEff": 0}), \
+ patch.object(Path, "readlink", side_effect=[Path("mnt:[candidate]"), Path("mnt:[host]")]), \
+ patch.object(drive_gate.subprocess, "run", side_effect=outputs) as invoke:
+ if failure is None:
+ proof = drive_gate.probe_gate(state, [disk], 123)
+ self.assertTrue(proof["systemMetadataAccessible"])
+ self.assertTrue(proof["systemMetadataControl"]["sameServiceIdentity"])
+ else:
+ with self.subTest(failure=failure), self.assertRaises(ValueError): drive_gate.probe_gate(state, [disk], 123)
+ for args in invoke.call_args_list:
+ self.assertIn("--mount=/proc/123/ns/mnt", args.args[0])
+ self.assertIn("--bounding-set=-dac_override,-dac_read_search", args.args[0])
+
+ def test_mrf_fixture_refuses_disk_wide_gate_or_failed_system_control(self):
+ valid = {"scope": "bucket", "disk": 0, "errno": 13, "readProbe": {"errno": 13}, "dacBypassRemoved": True,
+ "systemMetadataAccessible": True, "systemMetadataControl": {"sameServiceIdentity": True,
+ "read": {"errno": 0}, "write": {"errno": 0}}}
+ suite.validate_member_denial(valid, 0)
+ for change in ("scope", "disk", "read", "write", "control", "identity", "accessible"):
+ value = copy.deepcopy(valid)
+ if change == "scope": value["scope"] = "disk"
+ elif change == "disk": value["disk"] = 1
+ elif change in {"read", "write"}: value["systemMetadataControl"][change]["errno"] = 13
+ elif change == "control": value.pop("systemMetadataControl")
+ elif change == "identity": value["systemMetadataControl"]["sameServiceIdentity"] = False
+ else: value["systemMetadataAccessible"] = False
+ with self.subTest(change=change), self.assertRaises(ValueError): suite.validate_member_denial(value, 0)
+
+ def fixture(self):
+ original = {"node": fixtures.DurableMrfTests().observed()}
+ current, receipts = copy.deepcopy(original), []
+ for row in current["node"]["records"]:
+ if not row["present"] or ".heal-mrf-snapshot." not in row["relativePath"]: continue
+ raw = base64.b64decode(row["rawBase64"])
+ corrupted = raw[:-1] + bytes([raw[-1] ^ 1])
+ receipts.append({"node": "node", "disk": row["disk"], "relativePath": row["relativePath"],
+ "family": "mrf", "taskId": None, "kind": "checksum", "applied": True,
+ "originalBackupRetained": True, "productRepairClaimed": False,
+ "before": journal.entry(raw), "injected": journal.entry(corrupted)})
+ row.update(journal.entry(corrupted), rawBase64=base64.b64encode(corrupted).decode())
+ return original, current, receipts, "rv-heal-0123456789abcdef"
+
+ def add_older_slot(self, observed, *, foreign=False, empty=False):
+ fixture = fixtures.DurableMrfTests()
+ raw = fixture.journal()
+ if foreign:
+ body = raw[:-4].replace(b"rv-heal-0123456789abcdef", b"rv-heal-fedcba9876543210")
+ raw = body + struct.pack("OperationAbortedheal request not admitted: admission=dropped, reason=overlapping_paths"}
+ self.assertEqual(queue.rejection(value, admission="dropped", reason="overlapping_paths", code="OperationAborted", http=409)["reason"], "overlapping_paths")
+
+ def test_queue_requires_all_peers_and_consistent_source_priority_counts(self):
+ self.assertEqual(queue.queue_snapshot(self.status(), 1)["queueLength"], 2)
+ for name in ["coverage", "global-count", "source-count", "priority-count", "capacity", "bool-count", "admission"]:
+ value = self.status()
+ if name == "coverage": value["coverage"]["unknown"] = 1
+ elif name == "global-count": value["healQueueLength"] = 1
+ elif name == "source-count": value["healOperations"]["queuedBySource"]["admin"] = 1
+ elif name == "priority-count": value["healOperations"]["activeByPriority"]["high"] = 0
+ elif name == "capacity": value["healOperations"]["queueLength"] = 3
+ elif name == "bool-count": value["healOperations"]["activeTasks"] = True
+ else: del value["healOperations"]["admission"]["full"]
+ with self.subTest(name=name), self.assertRaises(ValueError): queue.queue_snapshot(value, 1)
+
+ def test_complete_scope_counters_and_dispositions_are_independent_of_idle(self):
+ value, expected, missing = self.terminal()
+ self.assertEqual(queue.terminal(value, "owned", expected, missing)["canonicalCounters"]["healed"], 1)
+ for name in ["coverage", "execution", "unknown", "attemptFailures", "wrong-bucket", "wrong-version", "wrong-set", "incarnation", "duplicate", "false-repair", "truncated"]:
+ value, expected, missing = self.terminal()
+ outcome = value["outcome"]
+ if name == "coverage": outcome["coverage"] = "partial"
+ elif name == "execution": outcome["execution"]["state"] = "completed_with_errors"
+ elif name in {"unknown", "attemptFailures"}: outcome["counters"][name] = 1
+ elif name == "wrong-bucket": outcome["objects"][0]["identity"]["bucket"] = "foreign"
+ elif name == "wrong-version": outcome["objects"][0]["identity"]["versionId"] = "another-version"
+ elif name == "wrong-set": outcome["objects"][0]["identity"]["setIndex"] = 1
+ elif name == "incarnation": outcome["objects"][0]["identity"]["bucketIncarnationId"] = "different"
+ elif name == "duplicate": outcome["objects"][1] = copy.deepcopy(outcome["objects"][0])
+ elif name == "false-repair": outcome["objects"][0]["disposition"]["state"] = "repaired"
+ else: outcome["objectsTruncated"] = True
+ with self.subTest(name=name), self.assertRaises(ValueError): queue.terminal(value, "owned", expected, missing)
+
+ def test_terminal_maps_only_reviewed_null_version_forms_to_exact_sdk_identity(self):
+ for version in (None, "", "null", "00000000-0000-0000-0000-000000000000"):
+ value, expected, missing = self.terminal()
+ for row in value['outcome']['objects']: row['identity']['versionId'] = version
+ with self.subTest(version=version):
+ self.assertEqual(queue.terminal(value, 'owned', expected, missing)['diagnosticObjects'], 2)
+ for version in (False, True, 0, [], {}, 'another-version', '01234567-89ab-4cde-8fab-0123456789ab'):
+ value, expected, missing = self.terminal()
+ value['outcome']['objects'][0]['identity']['versionId'] = version
+ with self.subTest(version=version), self.assertRaises(ValueError):
+ queue.terminal(value, 'owned', expected, missing)
+
+ def test_real_orchestrator_finishes_durable_gate_before_finite_pressure_admission(self):
+ for system_accessible in (True, False):
+ with self.subTest(system_accessible=system_accessible), tempfile.TemporaryDirectory() as tmp:
+ calls = []
+
+ class Fleet:
+ def __init__(self, nodes, directory, run_id, *args, **kwargs):
+ self.nodes, self.run_id = nodes, run_id
+ self.faults, self.drive_gates, self.reserved = [], set(), True
+
+ def record(self, label, value):
+ if label == 'binding': self.binding = value
+ return value
+
+ def remote(self, node, action, **kwargs):
+ calls.append(action)
+ if action == 'drive-gate-probe':
+ control = {name: {'uid': 0, 'gid': 0, 'errno': 0} for name in ('read', 'write')}
+ return {'scope': 'durable-bucket', 'disk': 0,
+ 'dataPath': f'/data/rustfs1/heal-validation/{self.run_id}/data/{self.binding["bucketPrefix"]}-durable',
+ 'systemMetadataAccessible': system_accessible,
+ 'systemMetadataControl': {**control, 'sameServiceIdentity': True},
+ 'sameServiceIdentity': True, 'privateMountNamespace': True, 'dacBypassRemoved': True,
+ 'name': 'EACCES', 'errno': 13, 'readProbe': {'errno': 13}}
+ return {}
+
+ def deploy(self, *args): pass
+ def snapshot(self, *args, **kwargs): return {}
+ def start(self): pass
+ def stop(self): pass
+ def cleanup(self): return True
+
+ class Pressure:
+ def __init__(self, *args): pass
+ def admit(self, path):
+ calls.append('pressure-admission')
+ raise ValueError('end test at first pressure admission')
+
+ root = Path(tmp)
+ (root / 'evidence').mkdir()
+ (root / 'candidate.json').write_text(json.dumps({
+ 'source_sha': queue.REFERENCE['candidateCommit'], 'package_sha256': 'c' * 64}))
+ (root / 'rc').write_bytes(b'unit-test-only')
+ args = types.SimpleNamespace(evidence=root / 'evidence', candidate=root / 'candidate.json',
+ rc_bin=root / 'rc', tool_directory=root, testing_commit='a' * 40, binary_sha='b' * 64)
+ target = {'node': 'vm000', 'disk': 0, 'relativePath': 'owned/objects/0/generation/part.1',
+ 'sha256': 'a' * 64, 'key': 'objects/0', 'versionId': 'null'}
+ with ExitStack() as patches:
+ replacements = {'Fleet': Fleet, 'Pressure': Pressure,
+ 'Probe': lambda *args: types.SimpleNamespace(inventory=lambda *args, **kwargs: {}),
+ 'Recorder': lambda *args: None, 'seed': lambda *args: [{'key': 'objects/0', 'versionId': 'null'}],
+ 'verify_inspector': lambda *args: {}, 'verify_reference': lambda *args: {},
+ 'verify_seed': lambda *args: {}, 'validate_topology': lambda *args: {},
+ 'map_cohorts': lambda *args, **kwargs: [target], 'verify_part_manifest': lambda *args, **kwargs: {},
+ 'validate_missing_batch': lambda *args: {}, 'physical_content': lambda *args: 'unchanged',
+ 'read_checkpoints': lambda *args, **kwargs: {'vm000': {'intents': []}},
+ 'JournalWindow': lambda *args, **kwargs: None}
+ for name, value in replacements.items(): patches.enter_context(patch.object(queue, name, value))
+ patches.enter_context(patch.dict(queue.os.environ, {'GITHUB_RUN_ID': '321', 'GITHUB_RUN_ATTEMPT': '1'}))
+ result = queue.run_topology(args, 'H1', ['vm000'])
+ self.assertEqual(result['verdict'], 'FAIL')
+ for action in ('drive-gate-arm-live', 'drive-gate-activate', 'drive-gate-probe'):
+ self.assertIn(action, calls)
+ if system_accessible:
+ self.assertLess(calls.index('drive-gate-probe'), calls.index('pressure-admission'))
+ self.assertEqual(result['failure']['message'], 'end test at first pressure admission')
+ else:
+ self.assertNotIn('pressure-admission', calls)
+
+ def test_running_window_requires_partial_actual_processing(self):
+ value = {"summary": "running", "outcome": {"execution": {"state": "running"}, "counters": {"processed": 5}}}
+ self.assertTrue(queue.active_window(value, 10))
+ for count in [0, 10, True, None]:
+ value["outcome"]["counters"]["processed"] = count
+ self.assertFalse(queue.active_window(value, 10))
+
+ def test_queue_failure_keeps_exact_durable_owner_and_generation(self):
+ records, obligations, bindings = self.durable()
+ self.assertTrue(queue.retained(records, obligations, bindings))
+ for name in ["absent", "owner", "sequence", "missing", "duplicate", "digest", "responsibility", "incarnation", "held", "operator", "version"]:
+ records, obligations, bindings = self.durable()
+ current = records["node"]; item = current["intents"][0]
+ if name == "absent": current["present"] = False
+ elif name == "owner": current["owner"] = "different"
+ elif name == "sequence": current["sequence"] = 7
+ elif name == "missing": current["intents"] = []
+ elif name == "duplicate": current["intents"].append(copy.deepcopy(item))
+ elif name == "digest": item["intentDigest"] = "changed"
+ elif name == "responsibility": item["lifecycle"]["responsibility_id"] = "new"
+ elif name == "incarnation": item["lifecycle"]["source_bucket_incarnation_id"] = "new"
+ elif name == "held": item["lifecycle"]["state"] = {"state": "held"}
+ elif name == "operator": item["lifecycle"]["last_operator_acceptance"] = {"actor": "operator"}
+ else: item["versionId"] = "other-version"
+ with self.subTest(name=name), self.assertRaises(ValueError): queue.retained(records, obligations, bindings)
+
+ def test_mrf_full_is_real_source_priority_capacity_and_time(self):
+ after = datetime(2026, 10, 11, tzinfo=timezone.utc)
+ row = {"target": "rustfs::heal::manager", "event": "heal_queue_admission", "source": "mrf", "priority": "Normal",
+ "result": "rejected_full", "queue_capacity": 2, "queue_len": 2, "request_id": "actual-request", "timestamp": "2026-10-11T01:00:00Z"}
+ self.assertEqual(len(queue.mrf_full_events({"node": [row]}, ["node"], after)), 1)
+ for changes in [{"source": "admin"}, {"priority": "High"}, {"result": "accepted"}, {"queue_len": 1},
+ {"queue_capacity": 99}, {"request_id": ""}, {"timestamp": "2026-10-10T23:59:59Z"}]:
+ self.assertEqual(queue.mrf_full_events({"node": [{**row, **changes}]}, ["node"], after), [])
+ self.assertEqual(queue.mrf_full_events({"foreign": [row]}, ["node"], after), [])
+
+ def test_resource_ceiling_and_process_birth_are_preregistered(self):
+ baseline = {"node": {"clockDomain": "boot", "monotonicNs": 1, "identity": {"pid": 12, "startTicks": 100}}}
+ sample = {"node": {**baseline["node"], "monotonicNs": 2, "readOnly": True, "rssBytes": 123, "fileDescriptors": 30, "threads": 20}}
+ self.assertTrue(queue.resources(sample, baseline)["withinFrozenLimits"])
+ for name in ["pid", "clock", "time", "rssBytes", "fileDescriptors", "threads", "missing-node"]:
+ current = copy.deepcopy(sample)
+ if name == "pid": current["node"]["identity"]["pid"] = 13
+ elif name == "clock": current["node"]["clockDomain"] = "another-boot"
+ elif name == "time": current["node"]["monotonicNs"] = 1
+ elif name == "missing-node": current = {}
+ else: current["node"][name] = queue.RESOURCE_BOUNDS[name] + 1
+ with self.subTest(name=name), self.assertRaises(ValueError): queue.resources(current, baseline)
+
+ def test_request_budget_counts_failed_and_merged_requests(self):
+ calls = []
+ probe = types.SimpleNamespace(admin=lambda *args: calls.append(args) or {"httpStatus": 503})
+ fleet = types.SimpleNamespace(nodes=[], record=lambda label, value: value)
+ pressure = queue.Pressure(probe, fleet, {}, [])
+ for _ in range(queue.MAX_ADMIN_REQUESTS): pressure.call("/owned")
+ with self.assertRaises(ValueError): pressure.call("/owned")
+ self.assertEqual(len(calls), queue.MAX_ADMIN_REQUESTS)
+
+ def test_multi_bucket_physical_subset_keeps_every_format_and_control(self):
+ snapshot = {"node": {"formats": ["actual-format"], "files": [{"path": "a/objects/0/xl.meta"}, {"path": "b/objects/0/part.1"}]}}
+ a = queue.original_subset(snapshot, "a")
+ self.assertEqual(a["node"]["formats"], snapshot["node"]["formats"])
+ self.assertEqual(a["node"]["files"], [snapshot["node"]["files"][0]])
+ self.assertEqual(queue.original_subset(snapshot, None, excluded="b"), a)
+ self.assertEqual(len(snapshot["node"]["files"]), 2)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test_validation_heal_quorum_candidate.py b/test_validation_heal_quorum_candidate.py
new file mode 100644
index 00000000..963734fa
--- /dev/null
+++ b/test_validation_heal_quorum_candidate.py
@@ -0,0 +1,123 @@
+"""Candidate selection must bind the real executors before any fleet mutation."""
+
+from contextlib import ExitStack
+import copy
+import json
+from pathlib import Path
+import tempfile
+import types
+import unittest
+from unittest.mock import patch
+
+from scripts.validation_heal import network, quorum_boundary, write_quorum
+
+
+ORIGINAL = '072b57b37559fbe626aeda8abf61617fdcbdd926'
+FIXED = '1c7fc818dd858247c45b39542abd8291b1e6e31b'
+DIAGNOSTIC = '85c706cac09d53d0649487b8f1865f18beb53225'
+RPC_FIXED = '62ff96774fa4139d45f14d70b70519aae8070ac0'
+IDENTITY_FIXED = '1a3f038c90704f9ad9693eddcf7d861c9530f13c'
+
+
+class RecordedSourceContract(Exception):
+ def __init__(self, records):
+ self.records = copy.deepcopy(records)
+ super().__init__("source contract exported")
+
+
+class QuorumCandidateTests(unittest.TestCase):
+ def execute_until_contract(self, module, source, *, allow_inspector=True):
+ records = {}
+ def record(label, value):
+ records[label] = value
+ if label in {'quorum-source-contract', 'network-source-contract', 'storage-boundary-source-contract'}:
+ raise RecordedSourceContract(records)
+ return value
+ fleet = types.SimpleNamespace(record=record)
+ with tempfile.TemporaryDirectory() as temporary, ExitStack() as stack:
+ root = Path(temporary)
+ candidate = root / 'candidate.json'
+ candidate.write_text(json.dumps({'source_sha': source, 'package_sha256': 'a' * 64}))
+ rc = root / 'rc'
+ rc.write_bytes(b'client')
+ args = types.SimpleNamespace(evidence=root / 'evidence', candidate=candidate,
+ rc_bin=rc, tool_directory=root, testing_commit='b' * 40, binary_sha='c' * 64)
+ stack.enter_context(patch.dict('os.environ', {'GITHUB_RUN_ID': '123', 'GITHUB_RUN_ATTEMPT': '1'}))
+ stack.enter_context(patch.object(module, 'Fleet', return_value=fleet))
+ stack.enter_context(patch.object(module, 'verify_inspector', return_value={},
+ side_effect=None if allow_inspector else AssertionError('unreviewed candidate reached inspector')))
+ stack.enter_context(patch.object(module, 'verify_quorum_contract', return_value={
+ 'recognized': True, 'writeQuorumRule': 'data-plus-one-if-equal-parity'}))
+ # Reaching the contract is the last allowed action; no deploy method
+ # exists on this fleet and no Probe is constructed by these tests.
+ if module is quorum_boundary:
+ module.run_topology(args, 'H1', ['vm000'])
+ elif module is write_quorum:
+ module.run_branch(args, 'H1', ['vm000'], write_quorum.BRANCHES[0])
+ else:
+ module.run_branch(args, ['vm000', 'vm001', 'vm002', 'vm003'], network.BRANCHES[0])
+ return records
+
+ def test_actual_executors_record_the_selected_candidate_before_deploy(self):
+ for module in (quorum_boundary, network):
+ for source in (ORIGINAL, FIXED, DIAGNOSTIC, RPC_FIXED):
+ with self.subTest(executor=module.CASE_ID, source=source):
+ with self.assertRaises(RecordedSourceContract) as completed:
+ self.execute_until_contract(module, source)
+ captured = completed.exception.records
+ reference = captured['quorum-source-contract']['locks'] if module is quorum_boundary else captured['network-source-contract']
+ self.assertEqual(reference['candidateCommit'], source)
+ if module is network:
+ self.assertEqual(reference['lock']['candidateCommit'], source)
+ self.assertEqual(reference['storage']['candidateCommit'], source)
+ self.assertEqual(captured['binding']['candidateCommit'], source)
+
+ def test_unknown_and_malformed_sources_never_reach_inspector_or_deploy(self):
+ for module in (quorum_boundary, network, write_quorum):
+ for source in ('0' * 40, FIXED[:7], FIXED.upper(), RPC_FIXED[:7], RPC_FIXED.upper(),
+ RPC_FIXED[:-1] + '1', None, [], True):
+ with self.subTest(executor=module.CASE_ID, source=source):
+ with self.assertRaises(ValueError):
+ self.execute_until_contract(module, source, allow_inspector=False)
+
+ def test_write_quorum_binds_both_source_reviews_before_inspector_or_deploy(self):
+ for source in (ORIGINAL, IDENTITY_FIXED):
+ with self.subTest(source=source), self.assertRaises(RecordedSourceContract) as completed:
+ self.execute_until_contract(write_quorum, source)
+ captured = completed.exception.records
+ self.assertEqual(captured['lock-policy-source-contract']['candidateCommit'], source)
+ self.assertEqual(captured['storage-boundary-source-contract']['candidateCommit'], source)
+ for source in (FIXED, DIAGNOSTIC, RPC_FIXED):
+ with self.subTest(source=source), self.assertRaisesRegex(ValueError, 'storage boundary'):
+ self.execute_until_contract(write_quorum, source, allow_inspector=False)
+
+ def test_network_storage_review_is_separate_from_lock_review(self):
+ source = 'f' * 40
+ with patch.dict(quorum_boundary.LOCK_REFERENCES, {source: {
+ **quorum_boundary.LOCK_REFERENCE, 'candidateCommit': source}}):
+ with self.assertRaises(ValueError):
+ network.source_reference(source)
+
+ def test_reference_exports_cannot_mutate_original_or_other_candidate(self):
+ original = copy.deepcopy(quorum_boundary.LOCK_REFERENCE)
+ fixed = quorum_boundary.lock_reference(FIXED)
+ fixed['sourceFiles'].clear()
+ fixed['candidateCommit'] = ORIGINAL
+ reference = network.source_reference(FIXED)
+ reference['storage']['sourceFiles'].clear()
+ reference['lock']['sourceFiles'].clear()
+ self.assertTrue(quorum_boundary.lock_reference(FIXED)['sourceFiles'])
+ self.assertTrue(network.source_reference(FIXED)['storage']['sourceFiles'])
+ self.assertEqual(quorum_boundary.LOCK_REFERENCE, original)
+ self.assertEqual(write_quorum.LOCK_REFERENCE['candidateCommit'], ORIGINAL)
+ self.assertEqual(write_quorum.STORAGE_REFERENCE['candidateCommit'], ORIGINAL)
+ selected = write_quorum.source_reference(IDENTITY_FIXED)
+ selected['lock']['sourceFiles'].clear()
+ selected['storage']['sourceFiles'].clear()
+ self.assertTrue(write_quorum.source_reference(IDENTITY_FIXED)['lock']['sourceFiles'])
+ self.assertTrue(write_quorum.source_reference(IDENTITY_FIXED)['storage']['sourceFiles'])
+ self.assertEqual(write_quorum.STORAGE_REFERENCE['candidateCommit'], ORIGINAL)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/test_validation_heal_race.py b/test_validation_heal_race.py
new file mode 100644
index 00000000..d948de58
--- /dev/null
+++ b/test_validation_heal_race.py
@@ -0,0 +1,264 @@
+import copy
+import hashlib
+import json
+from pathlib import Path
+import unittest
+from types import SimpleNamespace
+from unittest.mock import Mock, patch
+import tempfile
+import threading
+
+from scripts.validation_heal import race
+from scripts.validation_heal.matrix import AUTOMATIC_CASES, select
+from scripts.validation_heal.heal_workload import RecordedCallError
+
+
+class UnversionedReaderTests(unittest.TestCase):
+ bucket = 'rv-heal-0123456789abcdef'
+
+ def model(self):
+ body = race.payload('original', 31)
+ baseline = {'entries': [{'key': race.TARGET_KEY, 'versionId': 'null', 'isLatest': True,
+ 'deleteMarker': False, 'payload': {'size': len(body), 'sha256': hashlib.sha256(body).hexdigest()},
+ 'properties': {'ETag': '"original"', 'ContentLength': len(body),
+ 'ContentType': 'application/octet-stream', 'Metadata': {'nonce': 'original'}}}]}
+ plan = race.operation_plan(self.bucket, 'overwrite-vs-read-repair', baseline['entries'])
+ for step in plan: step['branch'] = 'overwrite-vs-read-repair'
+ responses = [{'ETag': f'"new-{index}"'} for index in range(3)]
+ return baseline, plan, responses
+
+ def ack(self, entry):
+ return {'operationCheck': 'PASS', 'observation': {'operation': 'get_object',
+ 'params': {'Bucket': self.bucket, 'Key': race.TARGET_KEY}, 'acknowledged': True,
+ 'resultUnknown': False, 'payload': copy.deepcopy(entry['payload']),
+ 'response': {**copy.deepcopy(entry['properties']), 'ResponseMetadata': {'HTTPStatusCode': 200}}}}
+
+ def test_reads_accept_only_complete_original_or_ack_known_generations(self):
+ baseline, plan, responses = self.model()
+ reads = [self.ack(baseline['entries'][0])]
+ for step, response in zip(plan, responses, strict=True):
+ body = race.payload(step['nonce'], 31)
+ reads.append(self.ack({'payload': {'size': len(body), 'sha256': hashlib.sha256(body).hexdigest()},
+ 'properties': {'ETag': response['ETag'], 'ContentLength': len(body),
+ 'ContentType': step['params']['ContentType'], 'Metadata': step['params']['Metadata']}}))
+ with patch.object(race, 'SIZE', 31):
+ self.assertTrue(race.verify_unversioned_reads(reads, baseline, plan, responses))
+ self.assertFalse(race.verify_unversioned_reads([], baseline, plan, responses))
+ for mutation in ('body', 'etag', 'metadata', 'length', 'version', 'unknown', 'foreign', 'refused'):
+ with self.subTest(mutation=mutation):
+ changed = copy.deepcopy(reads); observed = changed[0]['observation']
+ if mutation == 'body': observed['payload']['sha256'] = '0' * 64
+ elif mutation == 'etag': observed['response']['ETag'] = responses[1]['ETag']
+ elif mutation == 'metadata': observed['response']['Metadata'] = plan[1]['params']['Metadata']
+ elif mutation == 'length': observed['response']['ContentLength'] += 1
+ elif mutation == 'version': observed['response']['VersionId'] = 'foreign-version'
+ elif mutation == 'unknown': observed['resultUnknown'] = True
+ elif mutation == 'foreign': observed['params']['Bucket'] += '-other'
+ else: changed[0]['operationCheck'] = 'FAIL'
+ with self.assertRaises(ValueError): race.verify_unversioned_reads(changed, baseline, plan, responses)
+
+ def test_real_recorder_retains_read_requests_and_acks_without_retrying_unknowns(self):
+ for unknown in (False, True):
+ with self.subTest(unknown=unknown), tempfile.TemporaryDirectory() as directory:
+ baseline, _, _ = self.model(); done = threading.Event(); calls = []
+ def s3(operation, params, body):
+ calls.append((operation, params)); done.set()
+ observed = self.ack(baseline['entries'][0])['observation']
+ if unknown: observed.update(acknowledged=False, resultUnknown=True, response=None)
+ return observed
+ probe = SimpleNamespace(binding={'runId': 'gh-12345-1-h1', 'bucketPrefix': self.bucket,
+ 'maxInventoryBytes': 512 * 1024**2}, s3=s3)
+ recorder = race.Recorder(probe, Path(directory) / 'read-repair-ACKs')
+ params = {'Bucket': self.bucket, 'Key': race.TARGET_KEY}
+ if unknown:
+ with self.assertRaises(RecordedCallError): race.read_unversioned(recorder, params, done)
+ else:
+ self.assertEqual(len(race.read_unversioned(recorder, params, done)), 1)
+ self.assertEqual(calls, [('get_object', params)])
+ request = recorder.output / '000001-request.json'; ack = json.loads((recorder.output / '000001-ack.json').read_text())
+ self.assertEqual(ack['requestSha256'], hashlib.sha256(request.read_bytes()).hexdigest())
+ self.assertEqual(ack['operationCheck'], 'FAIL' if unknown else 'PASS')
+
+ def test_controller_requires_a_successful_reader_and_its_own_original_token_window(self):
+ for present, reader_window, writer_window in ((True, True, True), (False, True, True),
+ (True, False, True), (True, True, False)):
+ with self.subTest(present=present, reader_window=reader_window, writer_window=writer_window), tempfile.TemporaryDirectory() as directory:
+ baseline, plan, responses = self.model()
+ output = Path(directory) / 'foreground-ACKs'; output.mkdir()
+ writer = SimpleNamespace(output=output, mutations=SimpleNamespace(poison=Mock()))
+ ready, sampled = threading.Event(), threading.Event(); records = {}
+ def foreground(*args, **kwargs):
+ for index in range(3):
+ (output / f'{index+1:06d}-ack.json').write_text(json.dumps({'operationCheck': 'PASS',
+ 'sequence': index + 1, 'observation': {'operation': 'put_object'}}))
+ self.assertTrue(ready.wait(2)); self.assertTrue(sampled.wait(2))
+ return responses, {}
+ def read(*args):
+ ready.set(); self.assertTrue(sampled.wait(2))
+ return [self.ack(baseline['entries'][0])] if present else []
+ def observe(*args): sampled.set(); return {'originalToken': True}
+ def overlap(ack, *args):
+ return writer_window and ack.get('sequence') == 2 if ack['observation']['operation'] == 'put_object' else reader_window
+ probe = SimpleNamespace(binding={'runId': 'gh-12345-1-h1', 'bucketPrefix': self.bucket})
+ fleet = SimpleNamespace(record=lambda label, value: records.setdefault(label, value))
+ with patch.object(race, 'SIZE', 31), patch.object(race, 'foreground', side_effect=foreground), \
+ patch.object(race, 'read_unversioned', side_effect=read), patch.object(race, 'observe', side_effect=observe), \
+ patch.object(race, 'sample_overlap', side_effect=overlap):
+ _, accepted = race.race(writer, plan, probe, fleet, '/original-path', 'original-token', baseline=baseline)
+ self.assertEqual(accepted, present and reader_window and writer_window)
+ self.assertEqual(records['G01-read-repair-coverage']['complete'], present)
+ self.assertEqual(records['public-call-overlap-proof']['requiredCallOverlapObserved'],
+ present and reader_window and writer_window)
+ self.assertFalse(records['public-call-overlap-proof']['everyMutationOverlapped'])
+
+ def test_missing_baseline_or_existing_reader_journal_refuses_before_any_put(self):
+ for stale in (False, True):
+ with self.subTest(stale=stale), tempfile.TemporaryDirectory() as directory:
+ baseline, plan, _ = self.model(); output = Path(directory) / 'foreground-ACKs'; output.mkdir()
+ if stale: (output.parent / 'read-repair-ACKs').mkdir()
+ writer = SimpleNamespace(output=output, mutations=SimpleNamespace(poison=Mock()))
+ probe = SimpleNamespace(binding={'runId': 'gh-12345-1-h1', 'bucketPrefix': self.bucket})
+ with patch.object(race, 'foreground') as foreground, self.assertRaises((ValueError, FileExistsError)):
+ race.race(writer, plan, probe, SimpleNamespace(), '/original-path', 'original-token',
+ baseline=baseline if stale else None)
+ foreground.assert_not_called()
+
+ def test_status_observation_continues_until_the_inflight_reader_drains(self):
+ with tempfile.TemporaryDirectory() as directory:
+ baseline, plan, responses = self.model(); output = Path(directory) / 'foreground-ACKs'; output.mkdir()
+ writer = SimpleNamespace(output=output, mutations=SimpleNamespace(poison=Mock()))
+ ready, observed_after_drain = threading.Event(), threading.Event(); shared = {}; samples = []
+ def foreground(*args, **kwargs):
+ for index in range(3):
+ (output / f'{index+1:06d}-ack.json').write_text(json.dumps({'operationCheck': 'PASS',
+ 'sequence': index + 1, 'observation': {'operation': 'put_object'}}))
+ self.assertTrue(ready.wait(2)); return responses, {}
+ def read(recorder, params, done):
+ shared['done'] = done; ready.set()
+ self.assertTrue(done.wait(2)); self.assertTrue(observed_after_drain.wait(2))
+ return [self.ack(baseline['entries'][0])]
+ def observe(*args):
+ after = shared.get('done') is not None and shared['done'].is_set()
+ samples.append(after)
+ if samples.count(True) >= 2: observed_after_drain.set()
+ return {'afterWriterDrained': after}
+ def overlap(ack, sample, *args):
+ return ack.get('sequence') == 2 if ack['observation']['operation'] == 'put_object' else sample['afterWriterDrained']
+ probe = SimpleNamespace(binding={'runId': 'gh-12345-1-h1', 'bucketPrefix': self.bucket})
+ fleet = SimpleNamespace(record=lambda label, value: value)
+ with patch.object(race, 'SIZE', 31), patch.object(race, 'foreground', side_effect=foreground), \
+ patch.object(race, 'read_unversioned', side_effect=read), patch.object(race, 'observe', side_effect=observe), \
+ patch.object(race, 'sample_overlap', side_effect=overlap):
+ _, accepted = race.race(writer, plan, probe, fleet, '/original-path', 'original-token', baseline=baseline)
+ self.assertTrue(accepted)
+ self.assertGreaterEqual(samples.count(True), 2)
+
+
+class RaceTests(unittest.TestCase):
+ def test_source_case_branch_and_topology_contracts(self):
+ for case_id, branches in race.CASE_BRANCHES.items():
+ recipe = select(case_id)[0]
+ self.assertEqual(recipe["variants"], list(branches))
+ self.assertIn(case_id, AUTOMATIC_CASES)
+ self.assertEqual(recipe["contract"]["probes"]["topology"], "H1,H2")
+
+ def test_g01_plan_keeps_three_ack_ordered_overwrites(self):
+ expected = [{"key": race.TARGET_KEY, "versionId": "null", "isLatest": True}]
+ plan = race.operation_plan("rv-heal-0123456789abcdef", "overwrite-vs-read-repair", expected)
+ self.assertEqual([step["params"]["Metadata"]["nonce"] for step in plan], ["0", "1", "2"])
+ self.assertEqual(plan[-1]["nonce"], "rv-heal-0123456789abcdef/objects/0000.bin/0")
+
+ def test_g03_selects_exact_old_or_current_version(self):
+ expected = [{"key": race.TARGET_KEY, "versionId": value, "isLatest": value == "v2"}
+ for value in ("v0", "v1", "v2")]
+ old = race.operation_plan("rv-heal-0123456789abcdef", "delete-old-version-vs-deep", expected)
+ current = race.operation_plan("rv-heal-0123456789abcdef", "delete-current-version-vs-deep", expected)
+ self.assertEqual(old[0]["params"]["VersionId"], "v0")
+ self.assertEqual(current[0]["params"]["VersionId"], "v2")
+
+ def test_g02_plan_creates_three_acknowledged_versions(self):
+ expected = [{"key": race.TARGET_KEY, "versionId": value, "isLatest": value == "v2"}
+ for value in ("v0", "v1", "v2")]
+ plan = race.operation_plan("rv-heal-0123456789abcdef", "read-all-versions-during-heal", expected)
+ self.assertEqual(len(plan), 3)
+ self.assertEqual([step['referenceKey'] for step in plan], [f'objects/{i:04d}.bin' for i in range(3)])
+ self.assertTrue(all('readVersionId' not in step for step in plan))
+ self.assertEqual([step["params"]["Metadata"]["nonce"] for step in plan], ["0", "1", "2"])
+
+ def test_g06_keeps_copy_metadata_and_tag_operations_distinct(self):
+ expected = [{"key": race.TARGET_KEY, "versionId": "null", "isLatest": True},
+ {"key": race.REFERENCE_KEY, "versionId": "null", "isLatest": True}]
+ self.assertEqual(race.operation_plan("rv-heal-0123456789abcdef", "copy-source", expected)[0]["operation"], "copy_object")
+ self.assertEqual(race.operation_plan("rv-heal-0123456789abcdef", "replace-metadata", expected)[0]["operation"], "copy_object")
+ tags = race.operation_plan("rv-heal-0123456789abcdef", "update-tags", expected)
+ self.assertEqual(len(tags), 16)
+ self.assertEqual(tags[-1]["operation"], "put_object_tagging")
+ self.assertEqual(race.target_key("copy-source"), race.G06_TARGET_KEY)
+ self.assertEqual(race.reference_key("copy-source"), race.G06_REFERENCE_KEY)
+
+ def test_g05_cannot_use_the_superseded_unversioned_fixture_or_generic_plan(self):
+ recorder = Mock()
+ for branch in race.G05_BRANCHES:
+ with self.subTest(branch=branch), self.assertRaises(ValueError):
+ race.seed_fixture(recorder, 'rv-heal-0123456789abcdef', branch)
+ with self.subTest(branch=branch), self.assertRaises(ValueError):
+ race.operation_plan('rv-heal-0123456789abcdef', branch, [])
+ recorder.call.assert_not_called()
+ for operation in ('create_multipart_upload', 'upload_part', 'complete_multipart_upload', 'abort_multipart_upload'):
+ with self.subTest(operation=operation), self.assertRaises(ValueError):
+ race.foreground(recorder, [{'operation': operation}])
+ recorder.call.assert_not_called()
+
+ def test_g05_controller_delegates_all_public_branches_and_refuses_old_source_before_bootstrap(self):
+ case = race.CASE_IDS[4]
+ with tempfile.TemporaryDirectory() as d:
+ candidate = Path(d) / 'candidate.json'
+ args = SimpleNamespace(case_id=case, candidate=candidate)
+ with patch('scripts.validation_heal.mpu_race_runner.run_branch', return_value={'owned': True}) as execute, \
+ patch.object(race, 'Fleet', side_effect=AssertionError('generic fleet must not run')):
+ candidate.write_text(json.dumps({'source_sha': race.REFERENCE['candidateCommit']}))
+ with self.assertRaisesRegex(ValueError, 'exact-source multipart or marker'):
+ race.run_branch(args, 'H1', ['vm000'], 'upload-continue')
+ execute.assert_not_called()
+ candidate.write_text(json.dumps({'source_sha': race.COPY_CANDIDATE}))
+ for branch in race.G05_BRANCHES:
+ self.assertEqual(race.run_branch(args, 'H1', ['vm000'], branch), {'owned': True})
+ execute.assert_called_with(args, 'H1', ['vm000'], branch)
+
+ def test_g04_uses_complete_frozen_marker_controller_and_rejects_legacy_before_bootstrap(self):
+ with tempfile.TemporaryDirectory() as d:
+ candidate=Path(d)/'candidate.json';args=SimpleNamespace(case_id=race.CASE_IDS[3],candidate=candidate)
+ with patch('scripts.validation_heal.version_race_runner.run_branch',return_value={'owned':True}) as execute, \
+ patch.object(race,'Fleet',side_effect=AssertionError('generic fleet must not run')):
+ candidate.write_text(json.dumps({'source_sha':race.REFERENCE['candidateCommit']}))
+ with self.assertRaisesRegex(ValueError,'exact-source'):race.run_branch(args,'H1',['vm000'],'create-marker')
+ execute.assert_not_called()
+ candidate.write_text(json.dumps({'source_sha':race.MARKER_CANDIDATE}))
+ for branch in race.G04_BRANCHES:
+ self.assertEqual(race.run_branch(args,'H1',['vm000'],branch),{'owned':True})
+ execute.assert_called_with(args,'H1',['vm000'],branch)
+
+ def test_native_branch_remains_in_denominator_for_each_case(self):
+ for case_id, branches in race.CASE_BRANCHES.items():
+ native = race.native_blocked({"source_sha": race.REFERENCE["candidateCommit"]}, "H1", case_id)
+ rows = [{"topology": topology, "branch": branch, "verdict": "PASS", "cleanup": "PASS"}
+ for topology in ("H1", "H2") for branch in branches[:2]]
+ self.assertFalse(race.complete_results([*rows, native], case_id))
+ self.assertEqual(native["branch"], branches[-1])
+
+ def test_dispatch_and_source_contract_are_present(self):
+ dispatch = Path("scripts/validation_heal/run_case.sh").read_text()
+ top_dispatch = Path("scripts/run_validation_case.sh").read_text()
+ for case_id, code in (("heal.race-unversioned-overwrite", "G01"), ("heal.race-versioned-put", "G02"),
+ ("heal.race-version-delete", "G03"), ("heal.race-markers-and-batch-delete", "G04"),
+ ("heal.race-multipart-complete-abort", "G05"),
+ ("heal.race-copy-and-metadata-update", "G06")):
+ self.assertIn(case_id, dispatch)
+ self.assertIn(case_id, top_dispatch)
+ catalog = json.loads(Path("validation-cases.json").read_text())
+ source = {case["id"]: case for module in catalog["modules"] for case in module["cases"]}[case_id]
+ self.assertEqual(source["probes"]["designCode"], code)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test_validation_heal_race_io.py b/test_validation_heal_race_io.py
new file mode 100644
index 00000000..e18d3bfa
--- /dev/null
+++ b/test_validation_heal_race_io.py
@@ -0,0 +1,282 @@
+"""Owned cgroup control boundaries; mock kernel evidence is not live proof."""
+import copy
+from contextlib import ExitStack
+from pathlib import Path
+import tempfile
+from types import SimpleNamespace
+import unittest
+from unittest.mock import Mock, patch
+
+from scripts.validation_heal import fleet_node, nolock, race, race_io
+from scripts.validation_heal.healthy import Fleet
+import test_validation_heal as physical_fixtures
+
+
+class RaceIoTests(unittest.TestCase):
+ def test_actual_mapper_selects_fourth_physical_drive_independent_of_coding_role(self):
+ scene = physical_fixtures.HealShardOracleTest().snapshot()
+ entry = next(v for v in scene.values())['files'][0]
+ bucket, key = entry['metadata']['bucket'], entry['metadata']['key']
+ mapped = race.map_fixture(scene,bucket,[{'key':key,'versionId':'null','deleteMarker':False}])
+ target = mapped[key,'null']
+ self.assertEqual((target['node'],target['disk']),('node1',3))
+ self.assertEqual(target['role'],'parity')
+ self.assertEqual(len(target['cohort']),4)
+
+ def fixture(self):
+ process = {k: k for k in fleet_node.PROCESS_FIELDS}
+ process.update(pid=1234, startTicks=17, unit='owned.service', binarySha256='candidate')
+ mounts = [{'target': f'/data/rustfs{i+1}', 'maj:min': f'8:{i+1}', 'uuid': str(i)} for i in range(4)]
+ owner = {'runtimeProfile':'manual', 'unit':'owned.service', 'binarySha256':'candidate', 'mounts':mounts}
+ kernel = {'raw':'', 'writes':[], 'wrongReadback':False, 'membership':'0::/system.slice/owned.service\n',
+ 'group':'/system.slice/owned.service\n'}
+ def command(*args, **kwargs):
+ if args[:2] == ('systemctl', 'show'): raw = kernel['group']
+ elif args[:2] == ('cat', '/proc/1234/cgroup'): raw = kernel['membership']
+ elif args[:1] == ('cat',):
+ self.assertEqual(args[1], '/sys/fs/cgroup/system.slice/owned.service/io.max')
+ raw = kernel['raw']
+ elif args[:2] == ('systemctl', 'set-property'):
+ self.assertEqual(args[2:5], ('--runtime', 'owned.service', args[4]))
+ kernel['writes'].append(args)
+ enabled = args[4] == 'IOWriteBandwidthMax=/data/rustfs4 65536'
+ self.assertIn(args[4], ('IOWriteBandwidthMax=/data/rustfs4 65536', 'IOWriteBandwidthMax='))
+ kernel['raw'] = '8:4 rbps=max wbps=' + ('65536' if enabled and not kernel['wrongReadback'] else 'max') + ' riops=max wiops=max\n'
+ raw = ''
+ else: raise AssertionError(args)
+ return SimpleNamespace(stdout=raw, stderr='', returncode=0)
+ node = {'owned':lambda run:(Path('/state'), tuple(Path(f'/root{i}') for i in range(4)), copy.deepcopy(owner)),
+ 'MOUNTS':fleet_node.MOUNTS, 'PROCESS_FIELDS':fleet_node.PROCESS_FIELDS,
+ 'process':lambda request:copy.deepcopy(process), 'mounts':lambda:copy.deepcopy(mounts), 'command':command}
+ request = {'runId':'gh-12345-1-h1', 'action':'race-io-cap', 'caseId':race.CASE_IDS[0],
+ 'branch':'overwrite-vs-deep', 'enabled':True, 'expectedProcess':copy.deepcopy(process)}
+ return request, node, kernel, process, owner
+
+ def test_enable_and_release_require_exact_kernel_fourth_device_and_original_process(self):
+ request, node, kernel, process, owner = self.fixture()
+ enabled = race_io.run(request, node)
+ self.assertTrue(enabled['actualKernelLimitVerified'])
+ self.assertEqual((enabled['disk'], enabled['device'], enabled['writeBytesPerSecond']), (3,'8:4',65536))
+ self.assertEqual(enabled['process'], process)
+ self.assertFalse(enabled['publicOverlapProved'] or enabled['canonicalCasePassClaimed'])
+ released = race_io.run({**request, 'enabled':False}, node)
+ self.assertIsNone(released['writeBytesPerSecond'])
+ self.assertEqual(len(kernel['writes']), 2)
+ self.assertEqual(owner['mounts'][3]['target'], '/data/rustfs4')
+
+ def test_unregistered_g05_bad_identity_membership_or_foreign_limit_refuse_before_write(self):
+ for fault in ('g05','unknown-branch','boolean','extra','birth','runtime','membership','group','foreign','unbound-release'):
+ with self.subTest(fault=fault):
+ request, node, kernel, process, owner = self.fixture()
+ if fault == 'g05': request.update(caseId=race.CASE_IDS[4], branch='complete')
+ elif fault == 'unknown-branch': request['branch'] = 'unknown'
+ elif fault == 'boolean': request['enabled'] = 1
+ elif fault == 'extra': request['device'] = '/etc/passwd'
+ elif fault == 'birth': process['startTicks'] += 1
+ elif fault == 'runtime': owner['runtimeProfile'] = 'root-restart'
+ elif fault == 'membership': kernel['membership'] = '0::/system.slice/another.service\n'
+ elif fault == 'group': kernel['group'] = '/system.slice/owned.service/../../foreign.service\n'
+ elif fault == 'foreign': kernel['raw'] = '8:1 rbps=max wbps=65536 riops=max wiops=max\n'
+ else: request['enabled'] = False
+ with self.assertRaises(ValueError): race_io.run(request, node)
+ self.assertFalse(kernel['writes'])
+
+ def test_wrong_kernel_readback_cannot_claim_effective_cap(self):
+ request, node, kernel, process, _ = self.fixture()
+ kernel['wrongReadback'] = True
+ with self.assertRaisesRegex(ValueError, 'kernel cgroup'): race_io.run(request, node)
+ self.assertEqual(len(kernel['writes']), 1)
+
+ def test_process_or_unit_cgroup_move_during_control_cannot_claim_effective_cap(self):
+ for move in ('process','unit'):
+ with self.subTest(move=move):
+ request, node, kernel, _, _ = self.fixture()
+ original = node['command']
+ def command(*args, **kwargs):
+ value = original(*args,**kwargs)
+ if args[:2] == ('systemctl','set-property'):
+ if move == 'process': kernel['membership'] = '0::/system.slice/other.service\n'
+ else: kernel['group'] = '/system.slice/other.service\n'
+ return value
+ node['command'] = command
+ with self.assertRaisesRegex(ValueError,'cgroup'): race_io.run(request,node)
+ self.assertEqual(len(kernel['writes']),1)
+
+ def test_process_birth_change_during_control_cannot_claim_effective_cap(self):
+ request, node, kernel, process, _ = self.fixture()
+ original = node['process']; calls = 0
+ def changing(request):
+ nonlocal calls
+ calls += 1
+ if calls == 3: process['startTicks'] += 1
+ return original(request)
+ node['process'] = changing
+ with self.assertRaisesRegex(ValueError, 'process birth'): race_io.run(request, node)
+ self.assertEqual(len(kernel['writes']), 1)
+
+ def test_kernel_parser_rejects_incomplete_duplicate_or_unbounded_fields(self):
+ for raw in ('8:4 wbps=65536', '8:4 rbps=max wbps=0 riops=max wiops=max',
+ '8:4 rbps=max wbps=65536 riops=max rbps=max',
+ '8:4 rbps=max wbps=max riops=max wiops=max\n'*2, 'x'*65537):
+ with self.subTest(raw=raw[:80]), self.assertRaises(ValueError): race_io.limits(raw)
+
+ def observation(self):
+ request, node, kernel, process, owner = self.fixture()
+ request.pop('enabled')
+ request.update(action='race-io-observe', caseId=race.CASE_IDS[4], branch='complete')
+ original = node['command']
+ kernel['ancestors'] = {'/sys/fs/cgroup/system.slice/io.max': '', '/sys/fs/cgroup/io.max': ''}
+ kernel['reads'] = []
+ def command(*args, **kwargs):
+ if args[0] == 'cat' and args[1].endswith('/io.max'):
+ kernel['reads'].append(args[1])
+ if args[1] in kernel['ancestors']:
+ return SimpleNamespace(stdout=kernel['ancestors'][args[1]], stderr='', returncode=0)
+ return original(*args, **kwargs)
+ node['command'] = command
+ return request, node, kernel, process, owner
+
+ def test_g05_observes_owned_and_inherited_io_limits_without_writes(self):
+ request, node, kernel, _, _ = self.observation()
+ for branch in ('upload-continue', 'complete', 'abort'):
+ result = race_io.run({**request, 'branch': branch}, node)
+ self.assertTrue(result['inspectionOnly'] and result['actualKernelNoLimitVerifiedAtSamples'])
+ self.assertFalse(result['mutated'] or result['continuousNoLimitProved'] or result['ioCapRequested'])
+ self.assertEqual(len(result['beforeIoScopes']), 3)
+ self.assertEqual(result['beforeIoScopes'], result['afterIoScopes'])
+ self.assertFalse(kernel['writes'])
+ self.assertEqual(len(kernel['reads']), 18)
+
+ def test_g05_refuses_each_ancestor_cap_and_cannot_release_a_foreign_limit(self):
+ for where in ('owned', '/sys/fs/cgroup/system.slice/io.max', '/sys/fs/cgroup/io.max'):
+ for limit in ('rbps', 'wbps', 'riops', 'wiops'):
+ with self.subTest(where=where, limit=limit):
+ request, node, kernel, _, _ = self.observation()
+ raw = '8:4 ' + ' '.join(f'{k}={65536 if k == limit else "max"}'
+ for k in ('rbps', 'wbps', 'riops', 'wiops')) + '\n'
+ if where == 'owned': kernel['raw'] = raw
+ else: kernel['ancestors'][where] = raw
+ with self.assertRaisesRegex(ValueError, 'inherited kernel IO cap'):
+ race_io.run(request, node)
+ self.assertFalse(kernel['writes'])
+
+ def test_g05_observation_rejects_scope_changes_and_mutation_parameters(self):
+ for fault in ('case', 'branch', 'enabled', 'process', 'group', 'runtime', 'mount', 'late-cap', 'late-birth', 'late-group'):
+ with self.subTest(fault=fault):
+ request, node, kernel, process, owner = self.observation()
+ if fault == 'case': request['caseId'] = race.CASE_IDS[0]
+ elif fault == 'branch': request['branch'] = 'copy-source'
+ elif fault == 'enabled': request['enabled'] = False
+ elif fault == 'process': process['startTicks'] += 1
+ elif fault == 'group': kernel['membership'] = '0::/system.slice/other.service\n'
+ elif fault == 'runtime': owner['runtimeProfile'] = 'lock-quorum'
+ elif fault == 'mount':
+ mounts = copy.deepcopy(owner['mounts'])
+ mounts[0]['maj:min'] = '8:99'
+ node['mounts'] = lambda: mounts
+ else:
+ original = node['command']
+ def change(*args, **kwargs):
+ result = original(*args, **kwargs)
+ if len(kernel['reads']) == 3:
+ if fault == 'late-cap': kernel['ancestors']['/sys/fs/cgroup/io.max'] = '8:4 rbps=max wbps=7 riops=max wiops=max\n'
+ elif fault == 'late-birth': process['startTicks'] += 1
+ else: kernel['membership'] = '0::/system.slice/foreign.service\n'
+ return result
+ node['command'] = change
+ with self.assertRaises(ValueError): race_io.run(request, node)
+ self.assertFalse(kernel['writes'])
+
+ def test_missing_unenabled_controller_is_accepted_only_for_an_existing_owned_group(self):
+ for group_exists in (True, False):
+ with self.subTest(group_exists=group_exists):
+ request, node, kernel, _, _ = self.fixture()
+ original = node['command']; first = True
+ def command(*args, **kwargs):
+ nonlocal first
+ if args == ('cat','/sys/fs/cgroup/system.slice/owned.service/io.max') and first:
+ first = False
+ return SimpleNamespace(returncode=1,stdout='',stderr='cat: io.max: No such file or directory')
+ if args == ('test','-d','/sys/fs/cgroup/system.slice/owned.service'):
+ return SimpleNamespace(returncode=0 if group_exists else 1,stdout='',stderr='')
+ return original(*args,**kwargs)
+ node['command'] = command
+ if group_exists:
+ self.assertTrue(race_io.run(request,node)['actualKernelLimitVerified'])
+ else:
+ with self.assertRaisesRegex(ValueError,'limits could not be read'): race_io.run(request,node)
+ self.assertFalse(kernel['writes'])
+
+ def test_real_remote_adapter_embeds_the_reviewed_node_module(self):
+ def execute(*args, **kwargs):
+ source = kwargs['input'].rsplit('\nmain(',1)[0]
+ scope = {'__name__':'__race_io_test__'}
+ exec(compile(source,'race-io-node','exec'),scope)
+ self.assertEqual(scope['race_io_backend'].RATE,'65536')
+ self.assertTrue(callable(scope['race_io_backend'].run))
+ return SimpleNamespace(returncode=0,stdout='{}',stderr='')
+ for action in ('race-io-cap','race-io-observe'):
+ with self.subTest(action=action),tempfile.TemporaryDirectory() as tmp, \
+ patch('scripts.validation_heal.healthy.subprocess.run',side_effect=execute):
+ Fleet(['vm000'],Path(tmp),'gh-12345-1-h1').remote('vm000',action)
+
+ def test_controller_records_intent_before_single_call_and_never_retries_a_lost_result(self):
+ fleet = Mock(); fleet.nodes = ['vm000','vm001']; trace = []
+ def record(label,value): trace.append(label); return value
+ def remote(host,action,**params):
+ self.assertEqual(host,'vm001')
+ if action == 'process': return {'pid':1234,'birth':'original'}
+ self.assertEqual(action,'race-io-cap')
+ self.assertEqual(params['expectedProcess'],{'pid':1234,'birth':'original'})
+ self.assertEqual(trace,['race-io-process','race-io-cap-intent'])
+ raise OSError('lost response; caller must stop its owned service')
+ fleet.record.side_effect = record; fleet.remote.side_effect = remote
+ with self.assertRaises(OSError):
+ race_io.set_fleet(fleet,race.CASE_IDS[0],'overwrite-vs-deep',True,nodes=['vm001'])
+ self.assertEqual(fleet.remote.call_count,2)
+ self.assertEqual(trace,['race-io-process','race-io-cap-intent'])
+ for nodes in ([],['foreign'],['vm001','vm001']):
+ fleet.remote.reset_mock()
+ with self.subTest(nodes=nodes), self.assertRaises(ValueError):
+ race_io.set_fleet(fleet,race.CASE_IDS[0],'overwrite-vs-deep',True,nodes=nodes)
+ fleet.remote.assert_not_called()
+
+ def test_real_runner_arms_before_admin_and_releases_after_foreground_before_terminal(self):
+ class ReachedTerminalBoundary(BaseException): pass
+ for driver, case_id, branch in [(race,race.CASE_IDS[0],'overwrite-vs-deep'),
+ (nolock,nolock.CASE_ID,'overwrite-nolock')]:
+ with self.subTest(case=case_id), tempfile.TemporaryDirectory() as tmp, ExitStack() as stack:
+ root = Path(tmp); candidate = root/'candidate.json'
+ candidate.write_text('{"source_sha":"072b57b37559fbe626aeda8abf61617fdcbdd926","package_sha256":"digest"}')
+ rc = root/'rc'; rc.write_bytes(b'client')
+ selected = race.target_key(branch) if driver is race else nolock.TARGET_KEY
+ expected = [{'key':key,'versionId':'null','isLatest':True} for key in ('control/a','control/b',selected)]
+ mapped = {(r['key'],'null'):{**r,'disk':0,'node':'vm000','relativePath':r['key'],'sha256':'original'} for r in expected}
+ trace = []
+ fleet = Mock(); fleet.nodes = ['vm000']
+ fleet.record.side_effect = lambda label,value:value
+ fleet.snapshot.return_value = {'vm000':{'files':[],'formats':[]}}
+ fleet.remote.return_value = {}
+ fleet.cleanup.return_value = True
+ probe = Mock(); probe.admin.side_effect = lambda *a:(trace.append('admin') or {'httpStatus':200,'response':{'clientToken':'token'}})
+ probe.inventory.return_value = {'entries':expected}
+ def cap(f,c,b,enabled,*,nodes):
+ self.assertIs(f,fleet); self.assertEqual(nodes,['vm000']); trace.append('cap-on' if enabled else 'cap-off')
+ def foreground(*args, **kwargs): trace.append('foreground'); return [],False
+ def terminal(*args): trace.append('terminal'); raise ReachedTerminalBoundary()
+ values = {'Fleet':Mock(return_value=fleet),'Probe':Mock(return_value=probe),'verify_inspector':Mock(return_value={}),
+ 'validate_topology':Mock(),'seed_fixture':Mock(return_value=expected),'verify_seed':Mock(),
+ 'operation_plan':Mock(return_value=[{}]),'map_fixture':Mock(return_value=mapped),'verify_part_manifest':Mock(),
+ 'validate_missing_batch':Mock(),'wait_running':Mock(),'race':Mock(side_effect=foreground),
+ 'final_inventory':Mock(return_value={'entries':expected}),
+ 'wait_terminal':Mock(side_effect=terminal),'set_race_io_cap':Mock(side_effect=cap),'OBJECTS':3}
+ if driver is race: values['verify_endpoint_resolution'] = Mock()
+ for name,value in values.items(): stack.enter_context(patch.object(driver,name,value))
+ stack.enter_context(patch.dict(race.os.environ,{'GITHUB_RUN_ID':'12345','GITHUB_RUN_ATTEMPT':'1'}))
+ args = SimpleNamespace(evidence=root/'evidence',candidate=candidate,rc_bin=rc,tool_directory=root,
+ testing_commit='helper',binary_sha='binary',case_id=case_id)
+ with self.assertRaises(ReachedTerminalBoundary): driver.run_branch(args,'H1',['vm000'],branch)
+ self.assertEqual(trace[:trace.index('terminal')+1],['cap-on','admin','foreground','cap-off','terminal'])
+
+
+if __name__ == '__main__': unittest.main()
diff --git a/test_validation_heal_race_native.py b/test_validation_heal_race_native.py
new file mode 100644
index 00000000..4abeedde
--- /dev/null
+++ b/test_validation_heal_race_native.py
@@ -0,0 +1,348 @@
+"""Negative admission checks; synthetic files never constitute native evidence."""
+
+import copy
+import hashlib
+import json
+from pathlib import Path
+import subprocess
+import sys
+import tempfile
+import unittest
+from unittest.mock import patch
+
+from scripts.validation_heal import race_native as suite
+from scripts.validation_heal import race_native_evidence as consumer
+from scripts.validation_heal import race
+
+
+SOURCE = '6afdec9fe1b0b88007f2e5b445c19bf73d51d3ca'
+HELPER = 'a' * 40
+CASE = 'heal.race-unversioned-overwrite'
+CANDIDATE = {'source_sha': SOURCE, 'package_sha256': 'b' * 64}
+
+
+class RaceNativeTests(unittest.TestCase):
+ def test_new_source_g01_requires_exact_bundle_and_other_race_cases_remain_closed(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ root = Path(tmp)
+ self.fixture(root)
+ proof = race.native_preflight(CANDIDATE, HELPER, CASE, root)
+ self.assertTrue(proof['requiredNativeSubsetPassed'])
+ with self.assertRaises(ValueError): race.native_preflight(CANDIDATE, HELPER, CASE, '')
+ for case in race.CASE_IDS[1:]:
+ with self.subTest(case=case), self.assertRaises(ValueError):
+ race.native_preflight(CANDIDATE, HELPER, case, root)
+ self.assertIsNone(race.native_preflight({'source_sha': race.REFERENCE['candidateCommit']}, HELPER, CASE, ''))
+ self.assertFalse(race.complete_results([], CASE))
+ shell = Path('scripts/validation_heal/run_case.sh').read_text()
+ self.assertLess(shell.index('${driver}" preflight'), shell.index('(bootstrap)'))
+ self.assertIn('[ "${driver}" = nolock.py ]', shell)
+
+ def test_source_specific_settings_keep_exact_types_selectors_and_lock_policy(self):
+ reference = race.source_contract(CANDIDATE, CASE)
+ value = {'httpStatus': 200, 'response': {'summary': 'running', 'settings': copy.deepcopy(reference['effective'])}}
+ self.assertEqual(race.settings(value, reference), reference['effective'])
+ for key, changed in [('nolock', True), ('pool', 0), ('set', 0), ('recursive', 1), ('readRepair', True)]:
+ rejected = copy.deepcopy(value)
+ rejected['response']['settings'][key] = changed
+ with self.subTest(key=key), self.assertRaises(ValueError): race.settings(rejected, reference)
+
+ def test_a_registered_historical_owner_subset_cannot_certify_the_full_version_put_case(self):
+ case = 'heal.race-versioned-put'
+ with tempfile.TemporaryDirectory() as tmp:
+ root = Path(tmp)
+ self.fixture(root)
+ subset = consumer.verify(root, CANDIDATE, HELPER, case)
+ self.assertEqual(subset['requiredRegressionLabels'], ['versioned-read-repair-lock-owner'])
+ self.assertTrue(subset['requiredNativeSubsetPassed'])
+ with patch.dict(race.NATIVE_RUNTIME, {SOURCE: {'reference': race.NATIVE_REFERENCE, 'cases': (case,)}}):
+ with self.assertRaisesRegex(ValueError, 'case-specific native commit group is incomplete'):
+ race.native_preflight(CANDIDATE, HELPER, case, root)
+
+ def test_runtime_case_opt_in_does_not_change_the_exact_source_or_enable_other_native_groups(self):
+ wrong_source = copy.deepcopy(race.NATIVE_REFERENCE)
+ wrong_source['candidateCommit'] = 'e' * 40
+ for reference, case in [(wrong_source, CASE)]:
+ with self.subTest(case=case), patch.dict(race.NATIVE_RUNTIME, {SOURCE: {'reference': reference, 'cases': (case,)}}):
+ with self.assertRaisesRegex(ValueError, 'race candidate/case contract is not reviewed'):
+ race.native_preflight(CANDIDATE, HELPER, case, '/never-used')
+ case = 'heal.race-multipart-complete-abort'
+ with tempfile.TemporaryDirectory() as tmp:
+ root = Path(tmp); self.fixture(root)
+ with patch.dict(race.NATIVE_RUNTIME, {SOURCE: {'reference': race.NATIVE_REFERENCE, 'cases': (case,)}}):
+ with self.assertRaises(ValueError):
+ race.native_preflight(CANDIDATE, HELPER, case, root)
+
+ def test_version_source_admits_only_three_complete_native_groups_without_executing_artifacts(self):
+ source = race.VERSION_CANDIDATE
+ candidate = {**CANDIDATE, 'source_sha': source}
+ with tempfile.TemporaryDirectory() as tmp:
+ root = Path(tmp)
+ self.fixture(root, source=source)
+ with patch.object(suite.subprocess, 'run', side_effect=AssertionError('must not execute native artifacts')):
+ for case in race.CASE_IDS[:3]:
+ with self.subTest(case=case):
+ proof = race.native_preflight(candidate, HELPER, case, root)
+ self.assertEqual(proof['candidateCommit'], source)
+ self.assertEqual(proof['requiredRegressionLabels'], race.FULL_NATIVE_GROUPS[case])
+ self.assertTrue(proof['requiredNativeSubsetPassed'])
+ self.assertFalse(proof['wholeCasePassClaimed'])
+ self.assertFalse(proof['canonicalCasePassClaimed'])
+ for case in race.CASE_IDS[3:]:
+ with self.subTest(case=case), self.assertRaises(ValueError):
+ race.native_preflight(candidate, HELPER, case, root)
+
+ def test_marker_source_requires_the_complete_four_mutation_group_and_keeps_later_cases_closed(self):
+ source = race.MARKER_CANDIDATE
+ candidate = {**CANDIDATE, 'source_sha': source}
+ case = 'heal.race-markers-and-batch-delete'
+ contract = suite.contract_for(source)
+ label = 'marker-and-batch-delete-final-rename'
+ self.assertEqual(contract['cases'][case], [label])
+ self.assertEqual(len(contract['regressions'][label]['milestones']), 24)
+ with tempfile.TemporaryDirectory() as tmp:
+ root = Path(tmp)
+ self.fixture(root, source=source)
+ with patch.object(suite.subprocess, 'run', side_effect=AssertionError('must not execute artifacts')):
+ for selected in race.CASE_IDS[:4]:
+ proof = race.native_preflight(candidate, HELPER, selected, root)
+ self.assertEqual(proof['requiredRegressionLabels'], race.FULL_NATIVE_GROUPS[selected])
+ self.assertTrue(proof['requiredNativeSubsetPassed'])
+ self.assertFalse(proof['wholeCasePassClaimed'])
+ self.assertFalse(proof['canonicalCasePassClaimed'])
+ for selected in race.CASE_IDS[4:]:
+ with self.subTest(case=selected), self.assertRaises(ValueError):
+ race.native_preflight(candidate, HELPER, selected, root)
+ summary = root / 'native-evidence/race-native-results.json'
+ value = json.loads(summary.read_text())
+ value['results'] = [row for row in value['results'] if row['label'] != label]
+ self.write(summary, value)
+ with self.assertRaisesRegex(ValueError, 'omit or duplicate'):
+ race.native_preflight(candidate, HELPER, case, root)
+
+ def test_multipart_native_source_collects_all_ten_events_without_enabling_incomplete_runtime(self):
+ source = '09fb813a003e7964bd1b893efb7446a3c41bd3bf'
+ candidate = {**CANDIDATE, 'source_sha': source}
+ case = 'heal.race-multipart-complete-abort'
+ label = 'multipart-continue-complete-abort-final-rename'
+ contract = suite.contract_for(source)
+ self.assertEqual(contract['cases'][case], [label])
+ self.assertEqual(len(contract['regressions'][label]['milestones']), 10)
+ with tempfile.TemporaryDirectory() as tmp:
+ root = Path(tmp)
+ self.fixture(root, source=source)
+ with patch.object(suite.subprocess, 'run', side_effect=AssertionError('must not execute artifacts')):
+ proof = consumer.verify(root, candidate, HELPER, case)
+ self.assertTrue(proof['requiredNativeSubsetPassed'])
+ self.assertFalse(proof['wholeCasePassClaimed'])
+ self.assertFalse(proof['canonicalCasePassClaimed'])
+ with self.assertRaisesRegex(ValueError, 'race candidate/case contract is not reviewed'):
+ race.native_preflight(candidate, HELPER, case, root)
+ summary = root / 'native-evidence/race-native-results.json'
+ value = json.loads(summary.read_text())
+ row = next(row for row in value['results'] if row['label'] == label)
+ row['milestones'].pop()
+ self.write(summary, value)
+ with self.assertRaisesRegex(ValueError, 'invented or omitted'):
+ consumer.verify(root, candidate, HELPER, case)
+
+ def test_default_owner_copy_and_mpu_source_keep_complete_native_groups_separate(self):
+ source = '757b751482bb970bc321e92059690a7698554358'
+ candidate = {**CANDIDATE, 'source_sha': source}
+ contract = suite.contract_for(source)
+ self.assertEqual(len(contract['regressions']), 10)
+ self.assertEqual(sum(len(v['milestones']) for v in contract['regressions'].values()), 104)
+ self.assertIn('crates/ecstore/src/bucket/metadata_sys.rs', contract['sourceFiles'])
+ self.assertIn('crates/ecstore/src/set_disk/mod.rs', contract['sourceFiles'])
+ with tempfile.TemporaryDirectory() as tmp:
+ root = Path(tmp)
+ self.fixture(root, source=source)
+ with patch.object(suite.subprocess, 'run', side_effect=AssertionError('must not execute artifacts')):
+ for case, label, count in [
+ ('heal.admin-nolock-safety', 'default-heal-exclusive-owner', 16),
+ ('heal.race-copy-and-metadata-update', 'copy-and-metadata-final-rename', 24),
+ ('heal.race-multipart-complete-abort', 'multipart-continue-complete-abort-final-rename', 10),
+ ]:
+ with self.subTest(case=case):
+ self.assertEqual(contract['cases'][case], [label])
+ self.assertEqual(len(contract['regressions'][label]['milestones']), count)
+ proof = consumer.verify(root, candidate, HELPER, case)
+ self.assertEqual(proof['requiredRegressionLabels'], [label])
+ self.assertTrue(proof['requiredNativeSubsetPassed'])
+ self.assertFalse(proof['wholeCasePassClaimed'])
+ self.assertFalse(proof['canonicalCasePassClaimed'])
+ for case in race.CASE_IDS:
+ with self.subTest(case=case):
+ if case in {'heal.race-copy-and-metadata-update', 'heal.race-multipart-complete-abort'}:
+ proof = race.native_preflight(candidate, HELPER, case, root)
+ self.assertEqual(proof['requiredRegressionLabels'], race.FULL_NATIVE_GROUPS[case])
+ else:
+ with self.assertRaisesRegex(ValueError, 'case contract is not reviewed'):
+ race.native_preflight(candidate, HELPER, case, root)
+
+ def test_default_owner_or_copy_cannot_omit_reverse_or_invent_native_events(self):
+ source = '757b751482bb970bc321e92059690a7698554358'
+ candidate = {**CANDIDATE, 'source_sha': source}
+ for case, label in [('heal.admin-nolock-safety', 'default-heal-exclusive-owner'),
+ ('heal.race-copy-and-metadata-update', 'copy-and-metadata-final-rename')]:
+ for mutation in ('omit', 'reverse', 'invent'):
+ with self.subTest(case=case, mutation=mutation), tempfile.TemporaryDirectory() as tmp:
+ root = Path(tmp)
+ self.fixture(root, source=source)
+ summary = root / 'native-evidence/race-native-results.json'
+ value = json.loads(summary.read_text())
+ row = next(row for row in value['results'] if row['label'] == label)
+ if mutation == 'omit': row['milestones'].pop()
+ elif mutation == 'reverse': row['milestones'].reverse()
+ else: row['milestones'].append(row['milestones'][0] + '-invented')
+ self.write(summary, value)
+ with self.assertRaisesRegex(ValueError, 'invented or omitted'):
+ consumer.verify(root, candidate, HELPER, case)
+
+ def fixture(self, root, *, source=SOURCE):
+ tools, evidence = root / 'native-tools', root / 'native-evidence'
+ tools.mkdir()
+ evidence.mkdir()
+ contract = suite.contract_for(source)
+ binary = tools / suite.BINARY
+ binary.write_bytes(b'synthetic nonexecutable artifact; must never run')
+ manifest = {'schemaVersion': 1, 'candidateCommit': source, 'artifactKind': suite.KIND,
+ 'candidateSourceTree': contract['sourceTree'], 'candidateCargoLockSha256': contract['cargoLockSha256'],
+ 'sourceFiles': contract['sourceFiles'], 'regressions': contract['regressions'], 'cases': contract['cases'],
+ 'buildEnvironment': suite.BUILD_ENV, 'buildCommand': suite.BUILD_COMMAND,
+ 'runtimeEnvironment': suite.RUNTIME_ENV, 'optimized': True, 'sourceUnmodified': True,
+ 'binaryName': suite.BINARY, 'binarySha256': suite.sha(binary),
+ 'collectorSha256': suite.sha(Path(suite.__file__)),
+ 'selectedContractSha256': hashlib.sha256(suite.canonical(contract)).hexdigest(),
+ 'canonicalCasePassClaimed': False, 'fleetExecutionClaimed': False, 'testingCommit': HELPER,
+ 'driverCommit': 'c' * 40, 'registrySourceSha256': suite.sha(suite.CONTRACTS),
+ 'runId': '123', 'runAttempt': '2'}
+ self.write(tools / 'race-native.json', manifest)
+ rows = []
+ for label, spec in contract['regressions'].items():
+ output = f"running 1 test\ntest {spec['test']} ... ok\n"
+ output += '\n'.join(spec['milestones']) + '\n'
+ output += 'test result: ok. 1 passed; 0 failed; 0 ignored; 99 filtered out; finished in 0.1s\n'
+ log = evidence / (label + '.log')
+ log.write_text(output)
+ rows.append({'label': label, 'test': spec['test'], 'nativeRegressionPassed': True,
+ 'exitCode': 0, 'runtimeEnvironment': suite.RUNTIME_ENV,
+ 'command': ['/synthetic/' + suite.BINARY, spec['test'], '--exact', '--show-output', '--test-threads=1'],
+ 'milestones': spec['milestones'], 'originalLogSha256': suite.sha(log)})
+ self.write(evidence / 'race-native-results.json', {'schemaVersion': 1, 'candidateCommit': source,
+ 'candidateSourceTree': manifest['candidateSourceTree'], 'binarySha256': manifest['binarySha256'],
+ 'nativeManifestSha256': suite.sha(tools / 'race-native.json'), 'results': rows,
+ 'requiredNativeSubsetsComplete': True, 'canonicalCasePassClaimed': False, 'fleetExecutionClaimed': False})
+ self.write(root / 'tool-run.json', {'id': 123, 'run_attempt': 2, 'status': 'completed',
+ 'conclusion': 'success', 'event': 'workflow_dispatch', 'repository': {'full_name': 'rustfs/rustfs'},
+ 'head_sha': 'c' * 40, 'path': '.github/workflows/rustfs-functional-chain.yml'})
+
+ @staticmethod
+ def write(path, value):
+ path.write_text(json.dumps(value) + '\n')
+
+ def test_read_only_admission_selects_only_reviewed_subset_and_never_claims_case_pass(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ root = Path(tmp)
+ self.fixture(root)
+ with patch.object(suite.subprocess, 'run', side_effect=AssertionError('must not execute')):
+ proof = consumer.verify(root, CANDIDATE, HELPER, CASE)
+ self.assertEqual(len(proof['observedRegressions']), 2)
+ self.assertTrue(proof['requiredNativeSubsetPassed'])
+ self.assertFalse(proof['wholeCasePassClaimed'])
+ self.assertFalse(proof['canonicalCasePassClaimed'])
+ self.assertFalse(proof['fleetExecutionClaimed'])
+
+ def test_wrong_api_source_attempt_driver_helper_or_package_is_rejected(self):
+ mutations = [('id', 124), ('run_attempt', 1), ('run_attempt', True), ('head_sha', 'd' * 40),
+ ('status', 'in_progress'), ('conclusion', 'failure'), ('event', 'push'),
+ ('path', '.github/workflows/other.yml'), ('repository', {'full_name': 'other/rustfs'})]
+ for key, value in mutations:
+ with self.subTest(key=key, value=value), tempfile.TemporaryDirectory() as tmp:
+ root = Path(tmp)
+ self.fixture(root)
+ api = json.loads((root / 'tool-run.json').read_text())
+ api[key] = value
+ self.write(root / 'tool-run.json', api)
+ with self.assertRaises(ValueError): consumer.verify(root, CANDIDATE, HELPER, CASE)
+ with tempfile.TemporaryDirectory() as tmp:
+ root = Path(tmp)
+ self.fixture(root)
+ for candidate, helper, case in [({**CANDIDATE, 'source_sha': 'd' * 40}, HELPER, CASE),
+ ({**CANDIDATE, 'package_sha256': 'invalid'}, HELPER, CASE),
+ (CANDIDATE, 'd' * 40, CASE), (CANDIDATE, HELPER, 'heal.race-version-delete')]:
+ with self.subTest(candidate=candidate, helper=helper, case=case), self.assertRaises(ValueError):
+ consumer.verify(root, candidate, helper, case)
+
+ def test_incomplete_duplicate_false_scope_wrong_command_and_log_tampering_rejected(self):
+ for kind in ('missing', 'duplicate', 'case-pass', 'fleet-pass', 'scope', 'exit', 'bool-exit',
+ 'command', 'test', 'environment', 'milestones', 'log', 'binary', 'manifest', 'symlink'):
+ with self.subTest(kind=kind), tempfile.TemporaryDirectory() as tmp:
+ root = Path(tmp)
+ self.fixture(root)
+ path = root / 'native-evidence/race-native-results.json'
+ value = json.loads(path.read_text())
+ row = value['results'][0]
+ if kind == 'missing': value['results'].pop()
+ elif kind == 'duplicate': value['results'][-1] = copy.deepcopy(row)
+ elif kind == 'case-pass': value['canonicalCasePassClaimed'] = True
+ elif kind == 'fleet-pass': value['fleetExecutionClaimed'] = True
+ elif kind == 'scope': value['requiredNativeSubsetsComplete'] = False
+ elif kind == 'exit': row['exitCode'] = 1
+ elif kind == 'bool-exit': row['exitCode'] = False
+ elif kind == 'command': row['command'].remove('--exact')
+ elif kind == 'test': row['test'] = 'set_disk::other'
+ elif kind == 'environment': row['runtimeEnvironment'] = {}
+ elif kind == 'milestones': row['milestones'].reverse()
+ elif kind == 'log': (root / 'native-evidence' / (row['label'] + '.log')).write_text('changed')
+ elif kind == 'binary': (root / 'native-tools' / suite.BINARY).write_bytes(b'changed')
+ elif kind == 'manifest': value['nativeManifestSha256'] = '0' * 64
+ else:
+ log = root / 'native-evidence' / (row['label'] + '.log')
+ moved = log.with_suffix('.original')
+ log.rename(moved)
+ log.symlink_to(moved)
+ self.write(path, value)
+ with self.assertRaises(ValueError): consumer.verify(root, CANDIDATE, HELPER, CASE)
+
+ def test_exact_output_parser_rejects_zero_multiple_ignored_and_wrong_tests(self):
+ name = 'set_disk::ops::test'
+ valid = f'running 1 test\ntest {name} ... ok\ntest result: ok. 1 passed; 0 failed; 0 ignored;\n'
+ suite.exact_result(valid, name, 0)
+ for output, code in [(valid, 1), (valid, False), (valid.replace('running 1 test', 'running 0 tests'), 0),
+ (valid.replace('0 ignored', '1 ignored'), 0), (valid + valid, 0), (valid.replace(name, name + '_other'), 0)]:
+ with self.subTest(output=output, code=code), self.assertRaises(ValueError):
+ suite.exact_result(output, name, code)
+
+ def test_ordered_events_cannot_be_missing_duplicated_reordered_or_invented(self):
+ spec = suite.contract_for(SOURCE)['regressions']['committed-overwrite-cleanup']
+ events = spec['milestones']
+ self.assertEqual(suite.milestones('\n'.join(events), spec), events)
+ for rows in [events[:-1], list(reversed(events)), events + events[:1], events + ['native_g01: invented']]:
+ with self.subTest(rows=rows), self.assertRaises(ValueError): suite.milestones('\n'.join(rows), spec)
+
+ def test_registry_additions_preserve_selected_contract_but_edits_invalidate_artifact(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ root = Path(tmp)
+ self.fixture(root)
+ registry = json.loads(suite.CONTRACTS.read_text())
+ registry['candidates']['e' * 40] = copy.deepcopy(registry['candidates'][SOURCE])
+ p = root / 'registry.json'
+ self.write(p, registry)
+ with patch.object(suite, 'CONTRACTS', p): consumer.verify(root, CANDIDATE, HELPER, CASE)
+ registry['candidates'][SOURCE]['sourceTree'] = 'f' * 40
+ self.write(p, registry)
+ with patch.object(suite, 'CONTRACTS', p), self.assertRaises(ValueError):
+ consumer.verify(root, CANDIDATE, HELPER, CASE)
+
+ def test_supports_unknown_and_other_case_are_distinct_from_malformed_input(self):
+ for commit, case, code in [(SOURCE, CASE, 0), (SOURCE, 'heal.race-version-delete', 2),
+ ('e' * 40, CASE, 2), ('main', CASE, 1)]:
+ with self.subTest(commit=commit, case=case):
+ result = subprocess.run([sys.executable, '-B', suite.__file__, 'supports', '--commit', commit,
+ '--case-id', case], capture_output=True, text=True)
+ self.assertEqual(result.returncode, code, result.stderr)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/test_validation_heal_race_physical.py b/test_validation_heal_race_physical.py
new file mode 100644
index 00000000..dd6e7179
--- /dev/null
+++ b/test_validation_heal_race_physical.py
@@ -0,0 +1,127 @@
+"""Exercise the real G04 physical transition oracle with decoded disk scenes."""
+import copy
+from contextlib import ExitStack
+from pathlib import Path
+import re
+import tempfile
+from types import SimpleNamespace
+import unittest
+from unittest.mock import Mock, patch
+import uuid
+
+import test_validation_heal as physical_fixtures
+from scripts.validation_heal import race
+
+
+class MarkerPhysicalTests(unittest.TestCase):
+ bucket = "rv-heal-0123456789abcdef"
+
+ def scene(self, branch):
+ before = physical_fixtures.HealShardOracleTest().snapshot()
+ count = 1 if branch == "unversioned-delete" else 3
+ expected = [{"key": race.TARGET_KEY, "versionId": "null" if count == 1 else str(uuid.UUID(int=i + 1)),
+ "isLatest": i == count - 1, "deleteMarker": False} for i in range(count)]
+ for value in before.values():
+ original = value["files"]
+ value["files"] = []
+ for meta, part in zip(original[::3], original[1::3], strict=True):
+ meta = copy.deepcopy(meta)
+ meta["path"] = f"{self.bucket}/{race.TARGET_KEY}/xl.meta"
+ metadata = meta["metadata"]
+ metadata["key"] = race.TARGET_KEY
+ template = metadata["versions"][0]
+ metadata["versions"] = []
+ for i, row in enumerate(expected):
+ version = copy.deepcopy(template)
+ version.update(versionId=None if count == 1 else row["versionId"],
+ dataDir=str(uuid.UUID(int=100 + i)), isLatest=row["isLatest"])
+ metadata["versions"].append(version)
+ value["files"].append({**part, "path": f"{self.bucket}/{race.TARGET_KEY}/{version['dataDir']}/part.1",
+ "sha256": part["sha256"] + f"-version-{i}"})
+ value["files"].append(meta)
+ marker = {"key": race.TARGET_KEY, "versionId": str(uuid.UUID(int=99)),
+ "isLatest": True, "deleteMarker": True}
+ if branch == "delete-marker":
+ for row in expected: row["isLatest"] = False
+ expected.append(marker)
+ for value in before.values():
+ for f in value["files"]:
+ if not f["path"].endswith("xl.meta"): continue
+ for v in f["metadata"]["versions"]: v["isLatest"] = False
+ f["metadata"]["versions"].append({"versionId": marker["versionId"], "deleted": True, "isLatest": True})
+ mapped = race.map_fixture(before, self.bucket, expected)
+ final = copy.deepcopy(expected)
+ if branch == "create-marker":
+ for row in final: row["isLatest"] = False
+ final.append(marker)
+ elif branch == "delete-marker":
+ final.pop()
+ final[-1]["isLatest"] = True
+ elif branch == "batch-version-delete": final = final[-1:]
+ else: final = []
+ after = copy.deepcopy(before)
+ retained = {r["versionId"] for r in final if not r["deleteMarker"]}
+ keep_dirs = {v["dataDir"] for v in mapped.values() if v["versionId"] in retained}
+ for value in after.values():
+ if not final:
+ value["files"] = []
+ continue
+ value["files"] = [f for f in value["files"] if f["path"].endswith("xl.meta")
+ or f["path"].rsplit("/", 2)[1] in keep_dirs]
+ for f in value["files"]:
+ if not f["path"].endswith("xl.meta"): continue
+ old = {v["versionId"] or "null": v for v in f["metadata"]["versions"]}
+ f["metadata"]["versions"] = [
+ {**old[r["versionId"]], "isLatest": r["isLatest"]} if not r["deleteMarker"] else
+ {"versionId": r["versionId"], "isLatest": r["isLatest"], "deleted": True}
+ for r in final]
+ return before, after, mapped, {"entries": final}
+
+
+
+class RaceFaultAdmissionTests(unittest.TestCase):
+ def test_real_runner_uses_each_registered_case_code_before_first_fault(self):
+ class FaultBoundaryReached(BaseException):
+ pass
+ for case_id, branches in race.CASE_BRANCHES.items():
+ # G05's dedicated controller checks all 257 fourth-drive G05 fault
+ # IDs in MpuRunnerTests.test_complete_controller_proves_physical_before_final_get_and_only_then_cleans.
+ if case_id in ('heal.race-multipart-complete-abort','heal.race-markers-and-batch-delete'):
+ continue
+ with self.subTest(case_id=case_id), tempfile.TemporaryDirectory() as folder, ExitStack() as stack:
+ root = Path(folder)
+ candidate = root / "candidate.json"
+ candidate.write_text('{"source_sha":"072b57b37559fbe626aeda8abf61617fdcbdd926","package_sha256":"digest"}')
+ rc = root / "rc"
+ rc.write_bytes(b"fixture-client")
+ branch = branches[0]
+ target_key = race.target_key(branch)
+ expected = [{"key": key, "versionId": "null", "isLatest": True}
+ for key in ("control/a", "control/b", target_key)]
+ mapped = {(row["key"], "null"): {**row, "disk": 0, "node": "vm000", "relativePath": row["key"],
+ "sha256": "original"} for row in expected}
+ fleet = Mock()
+ fleet.record.side_effect = lambda label, value: value
+ fleet.snapshot.return_value = {"vm000": {"files": []}}
+ fleet.cleanup.return_value = True
+ def remote(node, action, **params):
+ self.assertEqual(action, "apply-fault")
+ self.assertTrue(re.fullmatch(r"G0[1-6]-[0-9a-f]{32}", params["faultId"]))
+ self.assertTrue(params["faultId"].startswith(race.select(case_id)[0]["code"] + "-"))
+ raise FaultBoundaryReached()
+ fleet.remote.side_effect = remote
+ for name, value in {"Fleet": Mock(return_value=fleet), "verify_inspector": Mock(return_value={}),
+ "validate_topology": Mock(return_value={}), "Probe": Mock(),
+ "KeepAliveStatusProbe": Mock(), "verify_endpoint_resolution": Mock(),
+ "seed_fixture": Mock(return_value=expected), "verify_seed": Mock(),
+ "operation_plan": Mock(return_value=[]), "map_fixture": Mock(return_value=mapped),
+ "verify_part_manifest": Mock(), "OBJECTS": 3}.items():
+ stack.enter_context(patch.object(race, name, value))
+ stack.enter_context(patch.dict(race.os.environ, {"GITHUB_RUN_ID": "12345", "GITHUB_RUN_ATTEMPT": "1"}))
+ args = SimpleNamespace(evidence=root / "evidence", candidate=candidate, rc_bin=rc,
+ tool_directory=root, testing_commit="helper", binary_sha="binary", case_id=case_id)
+ with self.assertRaises(FaultBoundaryReached): race.run_branch(args, "H1", ["vm000"], branch)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test_validation_heal_race_retention.py b/test_validation_heal_race_retention.py
new file mode 100644
index 00000000..e397ba3f
--- /dev/null
+++ b/test_validation_heal_race_retention.py
@@ -0,0 +1,611 @@
+"""Stopped G01 isolation preserves ACK uncertainty, bytes and credentials."""
+
+import copy
+import hashlib
+import json
+import os
+from pathlib import Path
+import shutil
+import tempfile
+from types import SimpleNamespace
+import unittest
+from unittest.mock import patch
+
+from scripts.validation_heal import race_retention as controller, retained_scene as retained, recover_environment as recovery
+from scripts.validation_heal import interrupted_race
+import test_validation_heal_retained_scene as retained_tests
+
+
+class InterruptedCopyTests(unittest.TestCase):
+ def journal(self, directory, mutation=None):
+ folder = Path(directory) / 'foreground-ACKs'; folder.mkdir()
+ params = {'Bucket': 'rv-heal-0123456789abcdef', 'Key': 'destination',
+ 'CopySource': {'Bucket': 'rv-heal-0123456789abcdef', 'Key': 'source'}}
+ request = {'schemaVersion': 2, 'runId': 'gh-12345-1-h1', 'sequence': 1,
+ 'operation': 'copy_object', 'params': params}
+ raw = json.dumps(request).encode()
+ ack = {'schemaVersion': 2, 'runId': request['runId'], 'sequence': 1,
+ 'requestFile': '000001-request.json', 'requestSha256': hashlib.sha256(raw).hexdigest(),
+ 'operationCheck': 'PASS', 'observation': {'operation': 'copy_object', 'params': params,
+ 'acknowledged': True, 'resultUnknown': False, 'clientCallInvoked': True,
+ 'response': {'CopyObjectResult': {'ETag': '"committed"'}}}}
+ if mutation == 'unknown': ack['observation']['resultUnknown'] = True
+ elif mutation == 'refusal': ack['observation']['acknowledged'] = False
+ elif mutation == 'request-hash': ack['requestSha256'] = '0' * 64
+ elif mutation == 'foreign-run': ack['runId'] = 'gh-99999-1-h1'
+ elif mutation == 'foreign-operation': ack['observation']['operation'] = 'delete_object'
+ (folder / '000001-request.json').write_bytes(raw)
+ if mutation != 'missing-ack': (folder / '000001-ack.json').write_text(json.dumps(ack))
+ return folder, request
+
+ def test_only_exact_ack_can_be_replayed_without_a_client(self):
+ with tempfile.TemporaryDirectory() as directory:
+ folder, request = self.journal(directory)
+ reader = interrupted_race.Replay(folder, request['runId'], request['params']['Bucket'])
+ response = reader.call(request['operation'], request['params'])
+ self.assertEqual(response, {'CopyObjectResult': {'ETag': '"committed"'}})
+ self.assertEqual(len(reader.finish(1)), 2)
+ (folder / '000002-request.json').write_text('{}')
+ with self.assertRaises(ValueError): reader.finish(1)
+
+ def test_unknown_refused_changed_or_missing_ack_never_becomes_authority(self):
+ for mutation in ('unknown', 'refusal', 'request-hash', 'foreign-run', 'foreign-operation', 'missing-ack'):
+ with self.subTest(mutation=mutation), tempfile.TemporaryDirectory() as directory:
+ folder, request = self.journal(directory, mutation)
+ reader = interrupted_race.Replay(folder, request['runId'], request['params']['Bucket'])
+ with self.assertRaises((ValueError, FileNotFoundError)):
+ reader.call(request['operation'], request['params'])
+
+ def test_foreign_owner_refuses_before_stop_and_scene_drift_refuses_before_move(self):
+ for mutation in ('foreign-owner', 'scene-drift'):
+ with self.subTest(mutation=mutation), tempfile.TemporaryDirectory() as directory:
+ helper, state, roots, node, request, scene = RaceRetentionTests().scene(directory)
+ owner = request['expectedOwner']
+ isolation = {'kind': 'unresolved-g06-race', 'runId': owner['runId'],
+ 'nodes': ['vm000', 'vm001', 'vm002', 'vm003'], 'bucket': owner['bucket'],
+ 'candidateCommit': '1' * 40, 'harnessCommit': '2' * 40,
+ 'binarySha256': owner['binarySha256'], 'ackFilesSha256': '3' * 64,
+ 'owners': {}, 'businessSceneSha256': {}}
+ for i, name in enumerate(isolation['nodes']):
+ isolation['owners'][name] = {**owner, 'hostname': 'host-' + str(i)}
+ isolation['businessSceneSha256'][name] = request['expectedOriginalSceneSha256']
+ calls = []
+ def remote(name, action, **params):
+ calls.append(action)
+ if action == 'namespace-state':
+ marker = {'exists': True, 'ownerEvidence': {'uid': 0,
+ 'canonicalSha256': controller.digest(isolation['owners'][name])}}
+ if mutation == 'foreign-owner' and name == 'vm003': marker['ownerEvidence']['canonicalSha256'] = '0' * 64
+ return {'state': copy.deepcopy(marker), 'roots': [copy.deepcopy(marker) for _ in range(4)]}
+ if action == 'stop': return {'unit': owner['unit'], 'quiescent': True}
+ self.assertEqual(action, 'retained-scene-audit')
+ raise ValueError('current business scene differs')
+ fleet = SimpleNamespace(nodes=isolation['nodes'], run_id=owner['runId'],
+ remote=remote, record=lambda label, value: value)
+ plan = {'kind': 'interrupted-g06-after-authority', 'isolation': isolation,
+ 'originalCaseSha256': '4' * 64}
+ with self.assertRaises(ValueError): interrupted_race.isolate(fleet, plan)
+ self.assertNotIn('retained-scene-isolate', calls)
+ self.assertNotIn('recover-fault', calls)
+ if mutation == 'foreign-owner': self.assertNotIn('stop', calls)
+
+
+class RaceRetentionTests(unittest.TestCase):
+ def scene(self, directory):
+ helper = retained_tests.RetainedSceneTests()
+ state, roots, node, request = helper.scene(directory)
+ shutil.copytree(roots[1], roots[0])
+ owner = request['expectedOwner']; owner['runtimeProfile'] = 'manual'
+ for folder in (*roots, state): (folder / 'owner.json').write_text(json.dumps(owner))
+ for root in roots:
+ for suffix in ('original', 'intent.json', 'applied.json'):
+ (root / 'quarantine' / ('G01-object-0256.' + suffix)).write_bytes(('old shard ' + suffix).encode())
+ def snapshot(value):
+ self.assertEqual(value, {'runId': request['runId'], 'action': 'snapshot', 'quiescent': True})
+ files = []
+ for disk, root in enumerate(roots):
+ for path in sorted((root / 'data' / owner['bucket']).rglob('*')):
+ if path.is_file(): files.append({'disk': disk, 'path': path.relative_to(root / 'data').as_posix(),
+ 'size': path.stat().st_size, 'sha256': hashlib.sha256(path.read_bytes()).hexdigest()})
+ return {'hostname': owner['hostname'], 'quiescent': True, 'pid': 0, 'unit': owner['unit'],
+ 'binarySha256': owner['binarySha256'], 'files': files,
+ 'formats': [{'disk': disk, 'original': True} for disk in range(4)]}
+ node['snapshot'] = snapshot
+ scene = snapshot({'runId': request['runId'], 'action': 'snapshot', 'quiescent': True})
+ request['expectedOriginalSceneSha256'] = retained.sha({key: scene[key] for key in ('files', 'formats')})
+ return helper, state, roots, node, request, scene
+
+ def test_all_five_interrupted_moves_preserve_real_bytes_inodes_and_unread_credentials(self):
+ for interrupt_after in range(1, 6):
+ with self.subTest(interrupt_after=interrupt_after), tempfile.TemporaryDirectory() as directory:
+ helper, state, roots, node, request, _ = self.scene(directory)
+ original = {path: (path.stat().st_ino, path.read_bytes()) for folder in (*roots, state)
+ for path in folder.rglob('*') if path.is_file()}
+ opened, real_open = [], os.open
+ def tracked(path, *args, **kwargs):
+ opened.append(str(path)); return real_open(path, *args, **kwargs)
+ with helper.root_stats(), patch.object(os, 'open', side_effect=tracked):
+ before = retained.run(request, node)
+ change = {**request, 'action': 'retained-scene-isolate', 'expectedBaseline': before['baseline']}
+ count = 0
+ def interrupted(source, target):
+ nonlocal count
+ helper.local_rename(source, target)
+ if target.is_dir():
+ count += 1
+ if count == interrupt_after: raise RuntimeError('SSH response lost')
+ with patch.object(retained, 'rename_new', side_effect=interrupted), self.assertRaises(RuntimeError):
+ retained.run(change, node)
+ with patch.object(retained, 'rename_new', side_effect=helper.local_rename):
+ proof = retained.run(change, node)
+ self.assertFalse(retained.run(change, node)['mutated'])
+ self.assertEqual(proof['locations'], ['retained'] * 5)
+ self.assertEqual(proof['baseline'], before['baseline'])
+ self.assertFalse(proof['originalSceneRestored'] or proof['backupRestorationUsed'] or proof['canonicalPassClaimed'])
+ target_state, target_roots = retained.destinations(state, roots, per_run_parent=True)
+ for path, expected in original.items():
+ source = next(folder for folder in (*roots, state) if path.is_relative_to(folder))
+ target = target_state if source == state else target_roots[roots.index(source)]
+ moved = target / path.relative_to(source)
+ self.assertEqual((moved.stat().st_ino, moved.read_bytes()), expected)
+ self.assertTrue(all(row['uid'] == 0 and row['mode'] == 0o700 for row in proof['privateArchiveParents']))
+ self.assertTrue(any(path.endswith('G01-object-0256.original') for path in opened))
+ self.assertFalse(any(path.endswith('service.env') or path.endswith('/credential') for path in opened))
+
+ def test_changed_business_backup_scope_gate_and_missing_root_refuse_before_move(self):
+ for kind in ('scene-sha', 'missing-field', 'business-drift', 'backup-drift', 'gate', 'missing-root'):
+ with self.subTest(kind=kind), tempfile.TemporaryDirectory() as directory:
+ helper, state, roots, node, request, _ = self.scene(directory)
+ with helper.root_stats():
+ baseline = retained.run(request, node)['baseline']
+ change = {**request, 'action': 'retained-scene-isolate', 'expectedBaseline': baseline}
+ if kind == 'scene-sha': change['expectedOriginalSceneSha256'] = '0' * 64
+ elif kind == 'missing-field': del change['expectedOriginalSceneSha256']
+ elif kind == 'business-drift': (roots[0] / 'data' / request['expectedOwner']['bucket'] / 'part.1').write_bytes(b'new ACK')
+ elif kind == 'backup-drift': (roots[0] / 'quarantine/G01-object-0256.original').write_bytes(b'changed backup')
+ elif kind == 'gate': (state / 'drive-gate.json').write_text('{}')
+ else: shutil.rmtree(roots[0])
+ with patch.object(retained, 'rename_new') as move, self.assertRaises(ValueError): retained.run(change, node)
+ move.assert_not_called()
+
+ def archive(self, directory):
+ source = Path(directory) / 'source'; source.mkdir()
+ _, _, _, _, request, scene = self.scene(source)
+ owner = request['expectedOwner']; run = Path(directory) / 'artifact/rustfs-validation-12345-1/run' / controller.CASE
+ evidence = run / 'evidence' / controller.CASE; folder = evidence / 'H2/overwrite-vs-deep'
+ folder.mkdir(parents=True); (run / 'cases').mkdir()
+ nodes = [f'vm00{i}' for i in range(4)]
+ binding = {'runId': request['runId'], 'candidateCommit': '1' * 40, 'harnessCommit': '2' * 40,
+ 'candidateBinarySha256': owner['binarySha256'], 'candidatePackageSha256': '3' * 64, 'bucketPrefix': owner['bucket']}
+ candidate = {'source_sha': binding['candidateCommit'], 'package_sha256': binding['candidatePackageSha256']}
+ def write(path, value): path.write_text(json.dumps(value))
+ write(run / 'cases' / (controller.CASE + '.json'), {'case_id': controller.CASE, 'verdict': 'FAIL',
+ 'cleanup_status': 'FAIL', 'provenance': {'source_commit': binding['candidateCommit'],
+ 'candidate_sha256': binding['candidatePackageSha256'], 'binary_sha256': owner['binarySha256'],
+ 'testing_commit': binding['harnessCommit']}})
+ write(evidence / 'healthy-summary.json', {'caseId': controller.CASE, 'cleanup': 'FAIL', 'results': [
+ {'caseId': controller.CASE, 'cleanup': 'FAIL', 'verdict': 'FAIL', 'topology': 'H2', 'branch': 'overwrite-vs-deep'}]})
+ write(folder / '00001-binding.json', binding)
+ failure = {}
+ for i, name in enumerate(nodes):
+ host = 'original-host-' + str(i)
+ write(folder / f'{i:05d}-reserved.json', {'node': name, 'reserved': True, 'owner': {**owner, 'hostname': host}})
+ failure[name] = {**copy.deepcopy(scene), 'hostname': host}
+ write(folder / '00005-retained-failure-scene.json', failure)
+ write(folder / '00006-last-ACK-rescue-required.json', {'state': 'DIRTY', 'namespacesRetained': True,
+ 'lastACKPhysicalAuthorityVerified': False, 'originalGenerationBlindRestorationProhibited': True})
+ for name in ('seed-ACKs', 'foreground-ACKs'):
+ journal = folder / name; journal.mkdir()
+ write(journal / '000001-request.json', {'runId': binding['runId'], 'operation': 'put_object'})
+ # Unknown/missing results are retained, never reconstructed or retried.
+ root = Path(directory) / 'artifact'
+ return root, candidate, folder
+
+ def test_g01_read_repair_journal_is_digest_bound_and_cannot_hide_a_mutation(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root, candidate, original = self.archive(directory)
+ folder = original.with_name('overwrite-vs-read-repair'); original.rename(folder)
+ summary_path = folder.parents[1] / 'healthy-summary.json'
+ summary = json.loads(summary_path.read_text()); summary['results'][0]['branch'] = folder.name
+ summary_path.write_text(json.dumps(summary))
+ binding = json.loads((folder / '00001-binding.json').read_text())
+ journal = folder / 'read-repair-ACKs'; journal.mkdir()
+ request = {'runId': binding['runId'], 'operation': 'get_object',
+ 'params': {'Bucket': binding['bucketPrefix'], 'Key': 'objects/0256.bin'}}
+ (journal / '000001-request.json').write_text(json.dumps(request))
+ ack = {'runId': binding['runId'], 'observation': {'operation': 'get_object', 'resultUnknown': True}}
+ receipt = journal / '000001-ack.json'; receipt.write_text(json.dumps(ack))
+ first = recovery.recovery_plan(root, 12345, candidate)[0]
+ ack['preservedUnknownReceipt'] = True; receipt.write_text(json.dumps(ack))
+ second = recovery.recovery_plan(root, 12345, candidate)[0]
+ self.assertNotEqual(first['ackFilesSha256'], second['ackFilesSha256'])
+ self.assertEqual(first['businessSceneSha256'], second['businessSceneSha256'])
+ request['operation'] = 'put_object'; (journal / '000001-request.json').write_text(json.dumps(request))
+ with self.assertRaises(ValueError): recovery.recovery_plan(root, 12345, candidate)
+
+ def test_archived_unknown_or_explicit_refusal_can_only_select_isolation_with_exact_identity(self):
+ for kind in ('valid', 'unknown', 'refusal', 'candidate', 'binary', 'host', 'live', 'mpu', 'omitted-owner', 'mixed-case', 'extra-failure'):
+ with self.subTest(kind=kind), tempfile.TemporaryDirectory() as directory:
+ root, candidate, folder = self.archive(directory)
+ if kind in ('unknown', 'refusal'):
+ (folder / 'foreground-ACKs/000001-ack.json').write_text(json.dumps({'runId': 'gh-12345-1-h2',
+ 'observation': {'operation': 'put_object', 'acknowledged': False, 'resultUnknown': kind == 'unknown',
+ 'response': {'ResponseMetadata': {'HTTPStatusCode': 503}}}}))
+ elif kind == 'candidate': candidate['source_sha'] = '4' * 40
+ elif kind in ('binary', 'host', 'live'):
+ path = folder / '00005-retained-failure-scene.json'; value = json.loads(path.read_text())
+ value['vm003'][{'binary': 'binarySha256', 'host': 'hostname', 'live': 'pid'}[kind]] = 1 if kind == 'live' else 'foreign'
+ path.write_text(json.dumps(value))
+ elif kind == 'mpu':
+ (folder / 'foreground-ACKs/000001-request.json').write_text(json.dumps({'runId': 'gh-12345-1-h2', 'operation': 'complete_multipart_upload'}))
+ elif kind == 'omitted-owner': (folder / '00003-reserved.json').unlink()
+ elif kind == 'mixed-case':
+ path = next(root.glob('*/run/*/cases')).parent / 'cases/another-case.json'; path.write_text('{}')
+ elif kind == 'extra-failure':
+ path = folder.parent.parent / 'healthy-summary.json'; value = json.loads(path.read_text()); value['results'] *= 2
+ path.write_text(json.dumps(value))
+ if kind in ('valid', 'unknown', 'refusal'):
+ plan = recovery.recovery_plan(root, 12345, candidate)[0]
+ self.assertEqual(plan['kind'], 'unresolved-g01-race'); self.assertEqual(plan['nodes'], [f'vm00{i}' for i in range(4)])
+ else:
+ with self.assertRaises(ValueError): recovery.recovery_plan(root, 12345, candidate)
+
+ def test_all_peers_preflight_before_any_move_and_failed_checks_never_issue_cleanup_or_restart(self):
+ for failure in (None, 'preflight-vm003', 'isolate-vm001', 'post-vm002'):
+ with self.subTest(failure=failure), tempfile.TemporaryDirectory() as directory:
+ root, candidate, _ = self.archive(directory); plan = recovery.recovery_plan(root, 12345, candidate)[0]
+ calls, audits = [], {}
+ def remote(node, action, **params):
+ calls.append((node, action))
+ if action == 'logs': return {'retained': True}
+ if action == 'retained-scene-isolate':
+ self.assertEqual(len(audits), 4)
+ if failure == 'isolate-' + node: raise RuntimeError('SSH response lost')
+ return {'baseline': audits[node], 'activeNamespaceIsolated': True, 'originalSceneRestored': False,
+ 'backupRestorationUsed': False, 'canonicalPassClaimed': False, 'credentialContentsRead': False}
+ self.assertEqual(action, 'retained-scene-audit')
+ post = node in audits
+ if failure == ('post-' if post else 'preflight-') + node: raise RuntimeError('drift')
+ audits[node] = {'node': node}
+ return {'baseline': audits[node], 'inspectionOnly': True, 'mutated': False, 'credentialContentsRead': False,
+ 'activeNamespaceIsolated': post, 'privateArchiveParents': [{'exists': True, 'uid': 0, 'mode': 0o700}] * 5}
+ fleet = SimpleNamespace(run_id=plan['runId'], nodes=plan['nodes'], remote=remote, record=lambda label, value: value)
+ if failure is None:
+ proof = controller.isolate(fleet, plan); self.assertEqual(proof['cleanup'], 'ISOLATED')
+ self.assertFalse(proof['namespaceDeletionPerformed'] or proof['backupRestorationUsed'] or proof['canonicalPassClaimed'])
+ else:
+ with self.assertRaises(RuntimeError): controller.isolate(fleet, plan)
+ self.assertTrue(all(action in {'retained-scene-audit', 'retained-scene-isolate', 'logs'} for _, action in calls))
+ if failure == 'preflight-vm003': self.assertFalse(any(action == 'retained-scene-isolate' for _, action in calls))
+
+ def test_recovery_routes_isolated_scene_before_absent_namespace_cleanup(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root, candidate, _ = self.archive(directory)
+ plan = recovery.recovery_plan(root, 12345, candidate)[0]
+ candidate_path = Path(directory) / 'candidate.json'; candidate_path.write_text(json.dumps(candidate))
+ evidence = Path(directory) / 'recovery'
+ fleet = SimpleNamespace(nodes=plan['nodes'], run_id=plan['runId'], record=lambda label, value: value)
+ proof = {'runId': plan['runId'], 'cleanup': 'ISOLATED', 'canonicalPassClaimed': False}
+ with patch('sys.argv', ['recover', '--failed-run-id', '12345', '--artifact', str(root),
+ '--candidate', str(candidate_path), '--evidence', str(evidence)]), \
+ patch.object(recovery, 'Fleet', return_value=fleet), \
+ patch.object(recovery, 'isolate_race_scene', return_value=proof) as isolate:
+ recovery.main()
+ isolate.assert_called_once_with(fleet, plan)
+ summary = json.loads((evidence / 'recovery-summary.json').read_text())
+ self.assertIn('ISOLATED', json.dumps(summary))
+
+ def test_version_append_failure_keeps_unknown_put_and_complete_historical_journal_for_isolation(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root, candidate, folder = self.archive(directory)
+ original = next(root.glob('*/run/*'))
+ case = 'heal.race-versioned-put'
+ run = original.with_name(case); original.rename(run)
+ old_case = run / 'cases' / (controller.CASE + '.json')
+ value = json.loads(old_case.read_text()); value['case_id'] = case
+ old_case.rename(old_case.with_name(case + '.json'))
+ (run / 'cases' / (case + '.json')).write_text(json.dumps(value))
+ evidence = run / 'evidence' / controller.CASE
+ evidence.rename(evidence.with_name(case)); evidence = evidence.with_name(case)
+ summary = json.loads((evidence / 'healthy-summary.json').read_text()); summary['caseId'] = case
+ summary['results'][0].update(caseId=case, branch='read-all-versions-during-heal')
+ (evidence / 'healthy-summary.json').write_text(json.dumps(summary))
+ branch = evidence / 'H2/overwrite-vs-deep'; branch.rename(branch.with_name('read-all-versions-during-heal'))
+ branch = branch.with_name('read-all-versions-during-heal')
+ reads = branch / 'historical-read-ACKs'; reads.mkdir()
+ path = reads / '000001-request.json'
+ path.write_text(json.dumps({'runId': 'gh-12345-1-h2', 'operation': 'get_object'}))
+ plan = recovery.recovery_plan(root, 12345, candidate)[0]
+ self.assertEqual(plan['kind'], 'unresolved-g02-race')
+ before = plan['ackFilesSha256']
+ path.write_text(json.dumps({'runId': 'gh-12345-1-h2', 'operation': 'get_object', 'identity': {'uncertain': True}}))
+ self.assertNotEqual(recovery.recovery_plan(root, 12345, candidate)[0]['ackFilesSha256'], before)
+ path.write_text(json.dumps({'runId': 'gh-12345-1-h2', 'operation': 'abort_multipart_upload'}))
+ with self.assertRaises(ValueError): recovery.recovery_plan(root, 12345, candidate)
+
+ def test_a09_source_summary_without_public_case_field_keeps_uncertain_delete_for_isolation(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root, candidate, folder = self.archive(directory)
+ original = next(root.glob('*/run/*'))
+ case = 'heal.admin-nolock-safety'
+ run = original.with_name(case); original.rename(run)
+ old_case = run / 'cases' / (controller.CASE + '.json')
+ value = json.loads(old_case.read_text()); value['case_id'] = case
+ old_case.rename(old_case.with_name(case + '.json'))
+ (run / 'cases' / (case + '.json')).write_text(json.dumps(value))
+ evidence = run / 'evidence' / controller.CASE
+ evidence.rename(evidence.with_name(case)); evidence = evidence.with_name(case)
+ summary = json.loads((evidence / 'healthy-summary.json').read_text()); summary['caseId'] = case
+ summary['results'][0].update(branch='delete-nolock'); del summary['results'][0]['caseId']
+ (evidence / 'healthy-summary.json').write_text(json.dumps(summary))
+ branch = evidence / 'H2/overwrite-vs-deep'; branch.rename(branch.with_name('delete-nolock'))
+ branch = branch.with_name('delete-nolock')
+ path = branch / 'foreground-ACKs/000001-request.json'
+ path.write_text(json.dumps({'runId': 'gh-12345-1-h2', 'operation': 'delete_object'}))
+ plan = recovery.recovery_plan(root, 12345, candidate)[0]
+ self.assertEqual(plan['kind'], 'unresolved-a09-race')
+ before = plan['ackFilesSha256']
+ path.write_text(json.dumps({'runId': 'gh-12345-1-h2', 'operation': 'delete_object', 'identity': {'uncertain': True}}))
+ self.assertNotEqual(recovery.recovery_plan(root, 12345, candidate)[0]['ackFilesSha256'], before)
+ summary['results'][0]['caseId'] = 'foreign-case'
+ (evidence / 'healthy-summary.json').write_text(json.dumps(summary))
+ with self.assertRaises(ValueError): recovery.recovery_plan(root, 12345, candidate)
+ del summary['results'][0]['caseId']
+ (evidence / 'healthy-summary.json').write_text(json.dumps(summary))
+ path.write_text(json.dumps({'runId': 'gh-12345-1-h2', 'operation': 'abort_multipart_upload'}))
+ with self.assertRaises(ValueError): recovery.recovery_plan(root, 12345, candidate)
+
+ def test_g06_original_copy_and_metadata_journals_require_explicit_source_case_identity(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root, candidate, folder = self.archive(directory)
+ original = next(root.glob('*/run/*'))
+ case = 'heal.race-copy-and-metadata-update'
+ run = original.with_name(case); original.rename(run)
+ old_case = run / 'cases' / (controller.CASE + '.json')
+ value = json.loads(old_case.read_text()); value['case_id'] = case
+ old_case.rename(old_case.with_name(case + '.json'))
+ (run / 'cases' / (case + '.json')).write_text(json.dumps(value))
+ evidence = run / 'evidence' / controller.CASE
+ evidence.rename(evidence.with_name(case)); evidence = evidence.with_name(case)
+ summary = json.loads((evidence / 'healthy-summary.json').read_text()); summary['caseId'] = case
+ summary['results'][0].update(caseId=case, branch='copy-source')
+ (evidence / 'healthy-summary.json').write_text(json.dumps(summary))
+ branch = evidence / 'H2/overwrite-vs-deep'; branch.rename(branch.with_name('copy-source'))
+ branch = branch.with_name('copy-source')
+ path = branch / 'foreground-ACKs/000001-request.json'
+ path.write_text(json.dumps({'runId': 'gh-12345-1-h2', 'operation': 'copy_object'}))
+ plan = recovery.recovery_plan(root, 12345, candidate)[0]
+ self.assertEqual(plan['kind'], 'unresolved-g06-race')
+ before = plan['ackFilesSha256']
+ path.write_text(json.dumps({'runId': 'gh-12345-1-h2', 'operation': 'copy_object', 'identity': {'uncertain': True}}))
+ self.assertNotEqual(recovery.recovery_plan(root, 12345, candidate)[0]['ackFilesSha256'], before)
+ summary['results'][0]['caseId'] = 'foreign-case'
+ (evidence / 'healthy-summary.json').write_text(json.dumps(summary))
+ with self.assertRaises(ValueError): recovery.recovery_plan(root, 12345, candidate)
+
+ del summary['results'][0]['caseId']
+ (evidence / 'healthy-summary.json').write_text(json.dumps(summary))
+ with self.assertRaises(ValueError): recovery.recovery_plan(root, 12345, candidate)
+ summary['results'][0]['caseId'] = case
+ (evidence / 'healthy-summary.json').write_text(json.dumps(summary))
+ path.write_text(json.dumps({'runId': 'gh-12345-1-h2', 'operation': 'abort_multipart_upload'}))
+ with self.assertRaises(ValueError): recovery.recovery_plan(root, 12345, candidate)
+
+ def test_g04_unknown_batch_and_all_negative_peer_proofs_stay_bound_for_isolation(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root,candidate,folder=self.archive(directory)
+ original=next(root.glob('*/run/*'));case='heal.race-markers-and-batch-delete'
+ run=original.with_name(case);original.rename(run)
+ old=run/'cases'/(controller.CASE+'.json');value=json.loads(old.read_text());value['case_id']=case
+ old.rename(old.with_name(case+'.json'));(run/'cases'/(case+'.json')).write_text(json.dumps(value))
+ old=run/'evidence'/controller.CASE;evidence=old.with_name(case);old.rename(evidence)
+ old=evidence/'H2/overwrite-vs-deep';folder=old.with_name('batch-version-delete');old.rename(folder)
+ summary={'caseId':case,'cleanup':'FAIL','results':[{'caseId':case,'cleanup':'FAIL','verdict':'FAIL',
+ 'topology':'H2','branch':'batch-version-delete'}]}
+ (evidence/'healthy-summary.json').write_text(json.dumps(summary))
+ binding=json.loads((folder/'00001-binding.json').read_text());bucket=binding['bucketPrefix']
+ for journal,operation,params in [('seed-ACKs','put_object',{'Bucket':bucket,'Key':'objects-0256.bin'}),
+ ('foreground-ACKs','delete_objects',{'Bucket':bucket,'Delete':{'Quiet':False,'Objects':[
+ {'Key':key,'VersionId':str(i)} for i,key in enumerate(['objects-0256.bin']*3+['objects-0002.bin'])]}})]:
+ request={'runId':binding['runId'],'operation':operation,'params':params}
+ (folder/journal/'000001-request.json').write_text(json.dumps(request))
+ # All peers may have read-only diagnostics even after another final assertion failed.
+ for i in range(1,5):
+ journal=folder/f'negative-read-ACKs-node{i}';journal.mkdir()
+ params={'Bucket':bucket,'Key':'objects-0256.bin','VersionId':'old'}
+ request={'runId':binding['runId'],'operation':'get_object','params':params}
+ path=journal/'000001-request.json';path.write_text(json.dumps(request))
+ (journal/'000001-ack.json').write_text(json.dumps({'runId':binding['runId'],'requestFile':path.name,
+ 'requestSha256':hashlib.sha256(path.read_bytes()).hexdigest(),'observation':{'operation':'get_object'}}))
+ (journal/'000001-negative-proof.json').write_text(json.dumps({'expected':{'params':params,'httpStatus':404},
+ 'state':'FAIL','canonicalCasePassClaimed':False}))
+ plan=recovery.recovery_plan(root,12345,candidate)[0]
+ self.assertEqual(plan['kind'],'unresolved-g04-race')
+ proof=folder/'negative-read-ACKs-node4/000001-negative-proof.json'
+ original=proof.read_bytes();value=json.loads(original);value['state']='PASS';proof.write_text(json.dumps(value))
+ self.assertNotEqual(recovery.recovery_plan(root,12345,candidate)[0]['ackFilesSha256'],plan['ackFilesSha256'])
+ for fault in ('foreign-bucket','foreign-key','unexpected-operation','digest','foreign-peer'):
+ with self.subTest(fault=fault):
+ proof.write_bytes(original);request=folder/'negative-read-ACKs-node4/000001-request.json';raw=request.read_bytes()
+ if fault=='foreign-peer':
+ foreign=folder/'negative-read-ACKs-node5';foreign.mkdir()
+ elif fault=='digest':request.write_text(raw.decode()+' ')
+ elif fault=='unexpected-operation':
+ item=json.loads(raw);item['operation']='delete_object';request.write_text(json.dumps(item))
+ else:
+ item=json.loads(original);item['expected']['params']['Bucket' if fault=='foreign-bucket' else 'Key']='foreign'
+ proof.write_text(json.dumps(item))
+ with self.assertRaises(ValueError):recovery.recovery_plan(root,12345,candidate)
+ request.write_bytes(raw)
+ if fault=='foreign-peer':foreign.rmdir()
+ proof.write_bytes(original)
+ # A pre-foreground failure retains seed uncertainty without inventing a writer receipt.
+ shutil.rmtree(folder/'foreground-ACKs')
+ summary['results'][0]['verdict']='NOT_OBSERVED';(evidence/'healthy-summary.json').write_text(json.dumps(summary))
+ self.assertEqual(recovery.recovery_plan(root,12345,candidate)[0]['kind'],'unresolved-g04-race')
+
+
+ def mpu_archive(self, directory, *, foreground=False):
+ root,candidate,folder = self.archive(directory)
+ original = next(root.glob('*/run/*'));case = 'heal.race-multipart-complete-abort'
+ run = original.with_name(case);original.rename(run)
+ old = run/'cases'/(controller.CASE+'.json');value = json.loads(old.read_text());value['case_id'] = case
+ old.rename(old.with_name(case+'.json'));(run/'cases'/(case+'.json')).write_text(json.dumps(value))
+ old = run/'evidence'/controller.CASE;evidence = old.with_name(case);old.rename(evidence)
+ old = evidence/'H2/overwrite-vs-deep';folder = old.with_name('upload-continue');old.rename(folder)
+ summary = {'caseId':case,'cleanup':'FAIL','results':[{'caseId':case,'cleanup':'FAIL','verdict':'FAIL',
+ 'topology':'H2','branch':'upload-continue'}]}
+ (evidence/'healthy-summary.json').write_text(json.dumps(summary))
+ binding = json.loads((folder/'00001-binding.json').read_text())
+ request = {'runId':binding['runId'],'operation':'create_multipart_upload',
+ 'params':{'Bucket':binding['bucketPrefix'],'Key':'objects-0256.bin'}}
+ path = folder/'seed-ACKs/000001-request.json';path.write_text(json.dumps(request))
+ receipt = {'runId':binding['runId'],'requestFile':path.name,
+ 'requestSha256':hashlib.sha256(path.read_bytes()).hexdigest(),
+ 'observation':{'acknowledged':False,'resultUnknown':True}}
+ (path.parent/'000001-ack.json').write_text(json.dumps(receipt))
+ (path.parent/'mpu-manifest-0001.json').write_text(json.dumps({'bucket':binding['bucketPrefix'],
+ 'key':'objects-0256.bin','fixture':'DM','poisoned':'unknown Create response','items':{}}))
+ fg = folder/'foreground-ACKs/000001-request.json'
+ if foreground: fg.write_text(json.dumps({**request,'operation':'complete_multipart_upload'}))
+ else: fg.unlink()
+ raw = {}
+ for node in ('vm000','vm001','vm002','vm003'):
+ value = {'bucket':binding['bucketPrefix'],'keys':['objects-0256.bin'], 'scopes':[],
+ 'directories':[], 'files':[], 'bytesHashed':0}
+ raw[node] = {**value,'sceneSha256':controller.digest(value),'runId':binding['runId'],
+ 'binarySha256':binding['candidateBinarySha256'],'quiescent':True,'readOnly':True,
+ 'originalPhysicalDrives':4,'payloadIntegrityClaimed':False}
+ (folder/'00007-raw-mpu-failure-scene.json').write_text(json.dumps(raw))
+ return root,candidate,folder
+
+ def test_g05_unknown_create_or_complete_retains_raw_scene_without_inventing_ack(self):
+ for foreground in (False,True):
+ with self.subTest(foreground=foreground),tempfile.TemporaryDirectory() as d:
+ root,candidate,folder = self.mpu_archive(d,foreground=foreground)
+ plan = recovery.recovery_plan(root,12345,candidate)[0]
+ self.assertEqual(plan['kind'],'unresolved-g05-race')
+ self.assertEqual(plan['mpuKeys'],['objects-0256.bin'])
+ self.assertEqual(set(plan['mpuSceneSha256']),set(plan['nodes']))
+ before = plan['ackFilesSha256'];path = folder/'seed-ACKs/mpu-manifest-0001.json'
+ value = json.loads(path.read_text());value['poisoned'] = 'original uncertainty, no retry'
+ path.write_text(json.dumps(value))
+ self.assertNotEqual(recovery.recovery_plan(root,12345,candidate)[0]['ackFilesSha256'],before)
+
+ def test_g05_archive_rejects_missing_foreign_or_unbound_multipart_witnesses(self):
+ for fault in ('missing-peer','run','binary','live','digest','key','sdk-key','sdk-bucket','receipt',
+ 'model-only','foreign-manifest','forbidden-operation'):
+ with self.subTest(fault=fault),tempfile.TemporaryDirectory() as d:
+ root,candidate,folder = self.mpu_archive(d)
+ raw_path = folder/'00007-raw-mpu-failure-scene.json';raw = json.loads(raw_path.read_text())
+ if fault == 'missing-peer': raw.pop('vm003')
+ elif fault in ('run','binary','live','digest','key'):
+ field = {'run':'runId','binary':'binarySha256','live':'quiescent','digest':'sceneSha256','key':'keys'}[fault]
+ raw['vm003'][field] = False if fault == 'live' else ['foreign-key'] if fault == 'key' else 'foreign'
+ elif fault in ('sdk-key','sdk-bucket','forbidden-operation'):
+ path = folder/'seed-ACKs/000001-request.json';value = json.loads(path.read_text())
+ if fault == 'forbidden-operation': value['operation'] = 'delete_bucket'
+ else: value['params']['Key' if fault == 'sdk-key' else 'Bucket'] = 'foreign'
+ path.write_text(json.dumps(value))
+ elif fault == 'receipt':
+ path = folder/'seed-ACKs/000001-ack.json';value = json.loads(path.read_text());value['requestSha256']='f'*64
+ path.write_text(json.dumps(value))
+ elif fault == 'model-only':
+ (folder/'seed-ACKs/000001-request.json').unlink();(folder/'seed-ACKs/000001-ack.json').unlink()
+ else:
+ path = folder/'seed-ACKs/mpu-manifest-0001.json';value = json.loads(path.read_text());value['key']='foreign'
+ path.write_text(json.dumps(value))
+ raw_path.write_text(json.dumps(raw))
+ with self.assertRaises(ValueError): recovery.recovery_plan(root,12345,candidate)
+
+ def test_g05_final_peer_read_journals_bind_original_requests_and_cannot_authorize_mutations(self):
+ for fault in ('changed', 'foreign-peer', 'foreign-key', 'mutation'):
+ with self.subTest(fault=fault),tempfile.TemporaryDirectory() as d:
+ root,candidate,folder=self.mpu_archive(d)
+ binding=json.loads((folder/'00001-binding.json').read_text())
+ journal=folder/'final-read-ACKs-node4';journal.mkdir()
+ path=journal/'000001-request.json'
+ request={'runId':binding['runId'],'operation':'get_object',
+ 'params':{'Bucket':binding['bucketPrefix'],'Key':'objects-0256.bin'}}
+ path.write_text(json.dumps(request))
+ before=recovery.recovery_plan(root,12345,candidate)[0]['ackFilesSha256']
+ if fault=='foreign-peer':journal.rename(journal.with_name('final-read-ACKs-node5'))
+ else:
+ if fault=='changed':request['identity']={'original':'read'}
+ elif fault=='foreign-key':request['params']['Key']='foreign'
+ else:request['operation']='abort_multipart_upload'
+ path.write_text(json.dumps(request))
+ if fault=='changed':
+ self.assertNotEqual(recovery.recovery_plan(root,12345,candidate)[0]['ackFilesSha256'],before)
+ else:
+ with self.assertRaises(ValueError):recovery.recovery_plan(root,12345,candidate)
+
+ def test_g05_controller_preflights_all_original_mpu_peers_before_any_move(self):
+ with tempfile.TemporaryDirectory() as d:
+ root,candidate,_ = self.mpu_archive(d);plan = recovery.recovery_plan(root,12345,candidate)[0]
+ audits = {};calls = []
+ def remote(node,action,**params):
+ calls.append((node,action))
+ if action == 'logs': return {}
+ self.assertEqual(params['mpuKeys'],['objects-0256.bin'])
+ self.assertEqual(params['expectedMpuSceneSha256'],plan['mpuSceneSha256'][node])
+ if action == 'retained-scene-isolate':
+ self.assertEqual(set(audits),set(plan['nodes']))
+ return {'baseline':audits[node],'activeNamespaceIsolated':True,'originalSceneRestored':False,
+ 'backupRestorationUsed':False,'canonicalPassClaimed':False,'credentialContentsRead':False}
+ post = node in audits;audits[node] = {'node':node}
+ return {'baseline':audits[node],'inspectionOnly':True,'mutated':False,'credentialContentsRead':False,
+ 'activeNamespaceIsolated':post,'privateArchiveParents':[{'exists':True,'uid':0,'mode':0o700}]*5}
+ fleet = SimpleNamespace(run_id=plan['runId'],nodes=plan['nodes'],remote=remote,record=lambda label,value:value)
+ result = controller.isolate(fleet,plan)
+ self.assertEqual(result['cleanup'],'ISOLATED');self.assertFalse(result['namespaceDeletionPerformed'])
+ self.assertTrue(all(action == 'retained-scene-audit' for _,action in calls[:4]))
+
+ def test_g02_original_quarantine_contents_are_hashed_without_reading_credentials(self):
+ with tempfile.TemporaryDirectory() as directory:
+ helper, state, roots, node, request, _ = self.scene(directory)
+ for root in roots:
+ for path in (root / 'quarantine').glob('G01-*'):
+ path.rename(path.with_name(path.name.replace('G01-', 'G02-', 1)))
+ opened, original_open = [], os.open
+ def tracked(path, *args, **kwargs):
+ opened.append(str(path)); return original_open(path, *args, **kwargs)
+ with helper.root_stats(), patch.object(os, 'open', side_effect=tracked):
+ result = retained.run(request, node)
+ self.assertEqual(sum(path.endswith('G02-object-0256.original') for path in opened), 4)
+ self.assertFalse(any(path.endswith('service.env') or path.endswith('/credential') for path in opened))
+ self.assertFalse(result['canonicalPassClaimed'] or result['backupRestorationUsed'])
+
+ def test_a09_original_quarantine_contents_are_hashed_without_reading_credentials(self):
+ with tempfile.TemporaryDirectory() as directory:
+ helper, state, roots, node, request, _ = self.scene(directory)
+ for root in roots:
+ for path in (root / 'quarantine').glob('G01-*'):
+ path.rename(path.with_name(path.name.replace('G01-', 'A09-', 1)))
+ opened, original_open = [], os.open
+ def tracked(path, *args, **kwargs):
+ opened.append(str(path)); return original_open(path, *args, **kwargs)
+ with helper.root_stats(), patch.object(os, 'open', side_effect=tracked):
+ result = retained.run(request, node)
+ self.assertEqual(sum(path.endswith('A09-object-0256.original') for path in opened), 4)
+ self.assertFalse(any(path.endswith('service.env') or path.endswith('/credential') for path in opened))
+ self.assertFalse(result['canonicalPassClaimed'] or result['backupRestorationUsed'])
+
+ def test_g06_original_quarantine_contents_are_hashed_without_reading_credentials(self):
+ with tempfile.TemporaryDirectory() as directory:
+ helper, state, roots, node, request, _ = self.scene(directory)
+ for root in roots:
+ for path in (root / 'quarantine').glob('G01-*'):
+ path.rename(path.with_name(path.name.replace('G01-', 'G06-', 1)))
+ opened, original_open = [], os.open
+ def tracked(path, *args, **kwargs):
+ opened.append(str(path)); return original_open(path, *args, **kwargs)
+ with helper.root_stats(), patch.object(os, 'open', side_effect=tracked):
+ result = retained.run(request, node)
+ self.assertEqual(sum(path.endswith('G06-object-0256.original') for path in opened), 4)
+ self.assertFalse(any(path.endswith('service.env') or path.endswith('/credential') for path in opened))
+ self.assertFalse(result['canonicalPassClaimed'] or result['backupRestorationUsed'])
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/test_validation_heal_recovery.py b/test_validation_heal_recovery.py
new file mode 100644
index 00000000..e6b2cc97
--- /dev/null
+++ b/test_validation_heal_recovery.py
@@ -0,0 +1,679 @@
+"""A rejected fault ID cannot bypass ownership, intent or physical-scene checks."""
+
+import hashlib
+import base64
+import binascii
+import copy
+import json
+import sys
+from pathlib import Path
+import tempfile
+import types
+import unittest
+from unittest.mock import patch
+
+from scripts.validation_heal.heal_fault import bounded_fault_id
+from scripts.validation_heal.fleet_node import audit_finalize, quota_audit_finalize
+from scripts.validation_heal.recover_environment import recovery_plan, recovery_scene_matches
+from scripts.validation_heal.capacity import release_quota
+from scripts.validation_heal.healthy import Fleet
+from scripts.validation_heal import recover_environment as recovery
+from scripts.validation_heal import fleet_node, heal_fault
+
+
+class NamespaceOwnerEvidenceTests(unittest.TestCase):
+ def test_read_only_owner_fingerprints_keep_missing_roots_and_hide_owner_values(self):
+ with tempfile.TemporaryDirectory() as directory:
+ base = Path(directory).resolve()
+ state = base / "state"; state.mkdir()
+ roots = [base / str(index) for index in range(4)]
+ value = {"runId": "gh-12345-1-h2", "bucket": "rv-heal-0123456789abcdef", "privateValue": "must-not-be-emitted"}
+ raw = json.dumps(value).encode()
+ (state / "owner.json").write_bytes(raw)
+ (roots[3] / "data").mkdir(parents=True)
+ (roots[3] / "owner.json").write_bytes(json.dumps(value, sort_keys=True).encode())
+ before = {(state / "owner.json"): raw, (roots[3] / "owner.json"): (roots[3] / "owner.json").read_bytes()}
+ def command(*args, **kwargs):
+ return types.SimpleNamespace(stdout="0" if "MainPID" in args else "inactive")
+ with patch.object(fleet_node, "paths", return_value=(state, roots)), patch.object(fleet_node, "command", side_effect=command):
+ observed = fleet_node.namespace_state({"runId": value["runId"], "action": "namespace-state"})
+ self.assertTrue(observed["inspectionOnly"])
+ self.assertFalse(observed["mutated"])
+ self.assertEqual([row["ownerEvidence"]["present"] for row in observed["roots"]], [False, False, False, True])
+ expected = hashlib.sha256(json.dumps(value, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
+ self.assertEqual(observed["state"]["ownerEvidence"]["canonicalSha256"], expected)
+ self.assertEqual(observed["roots"][3]["ownerEvidence"]["canonicalSha256"], expected)
+ self.assertNotIn(value["privateValue"], json.dumps(observed))
+ for path, raw in before.items(): self.assertEqual(path.read_bytes(), raw)
+ self.assertFalse(any(root.exists() for root in roots[:3]))
+
+ def test_owner_fingerprint_refuses_links_and_oversized_or_non_object_markers(self):
+ for change in ("symlink", "hardlink", "oversized", "non-object"):
+ with tempfile.TemporaryDirectory() as directory, self.subTest(change=change):
+ base = Path(directory).resolve(); state = base / "state"; state.mkdir()
+ marker = state / "owner.json"
+ marker.write_text("[]" if change == "non-object" else "x" * 16385 if change == "oversized" else "{}")
+ if change == "symlink": marker.rename(state / "original"); marker.symlink_to(state / "original")
+ elif change == "hardlink": __import__("os").link(marker, state / "linked")
+ with patch.object(fleet_node, "paths", return_value=(state, [])), \
+ patch.object(fleet_node, "command", return_value=types.SimpleNamespace(stdout="0")), self.assertRaises(ValueError):
+ fleet_node.namespace_state({"runId": "gh-12345-1-h2", "action": "namespace-state"})
+
+
+class RejectedFaultRecoveryTest(unittest.TestCase):
+ def test_expanded_branches_have_bounded_distinct_fault_ids(self):
+ branch = "stripe-minus-exact-plus-s1048575-parity-missing"
+ original = "D01-" + branch + "-0"
+ self.assertEqual(len(original.split("-", 1)[1]), 49)
+ identifier = bounded_fault_id("D01", branch, 0)
+ self.assertRegex(identifier, r"^D01-[a-z0-9-]{1,48}$")
+ self.assertNotEqual(identifier, bounded_fault_id("D01", branch, 1))
+ self.assertNotEqual(identifier, bounded_fault_id("D01", branch + "-another", 0))
+ with self.assertRaises(ValueError): bounded_fault_id("bad", branch, 0)
+
+ def test_finalization_needs_no_intent_and_exact_archived_scene(self):
+ snapshot = {"files": [{"sha256": "actual", "path": "owned/key/xl.meta"}], "formats": [{"disk": "original"}]}
+ scene = hashlib.sha256(json.dumps(snapshot, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
+ request = {"runId": "gh-12345-1-h1", "disk": 0, "binarySha256": "a" * 64,
+ "rejectedFaultId": "D01-" + "a" * 49, "expectedSceneSha256": scene}
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory).resolve()
+ (root / "quarantine").mkdir()
+ with patch("scripts.validation_heal.fleet_node.owned", return_value=(None, None, {"binarySha256": "a" * 64})), \
+ patch("scripts.validation_heal.fleet_node.stopped"), \
+ patch("scripts.validation_heal.fleet_node.fault_root", return_value=(root, {})), \
+ patch("scripts.validation_heal.fleet_node.snapshot", return_value=snapshot), \
+ patch("scripts.validation_heal.fleet_node.cleanup", return_value={"cleanup": "PASS"}) as cleanup:
+ result = audit_finalize(request)
+ self.assertTrue(result["faultNeverApplied"])
+ self.assertFalse(result["productRepairClaimed"])
+ cleanup.reset_mock()
+ for changed in ({**request, "expectedSceneSha256": "b" * 64},
+ {**request, "binarySha256": "b" * 64},
+ {**request, "rejectedFaultId": "D01-valid-length"}):
+ with self.assertRaises(ValueError): audit_finalize(changed)
+ cleanup.assert_not_called()
+ (root / "quarantine" / (request["rejectedFaultId"] + ".intent.json")).write_text("intent")
+ with self.assertRaisesRegex(ValueError, "intent"):
+ audit_finalize(request)
+ cleanup.assert_not_called()
+
+
+class RejectedMrfRecoveryTests(unittest.TestCase):
+ def test_remote_rejection_audit_loads_every_real_snapshot_and_cleanup_backend(self):
+ with tempfile.TemporaryDirectory() as directory:
+ fleet = Fleet(["vm000"], Path(directory), "gh-12345-1-h2")
+ def execute(*args, **kwargs):
+ source = kwargs["input"].rsplit("\nmain(", 1)[0]
+ namespace = {"__name__": "__validation_node_contract__"}
+ with patch.dict(sys.modules):
+ exec(compile(source, "remote-node.py", "exec"), namespace)
+ self.assertTrue({"fault", "mrf", "encryption_runtime", "gate", "quorum_gate_backend", "replacement"} <= set(namespace))
+ return types.SimpleNamespace(returncode=0, stdout="{}", stderr="")
+ with patch("scripts.validation_heal.healthy.subprocess.run", side_effect=execute):
+ for action in ("journal-rejection-audit", "journal-rejection-audit-finalize"):
+ fleet.remote("vm000", action)
+
+ def archive(self, root):
+ case, bucket = "heal.checkpoint-and-mrf-corruption", "rv-heal-0123456789abcdef"
+ base = root / "artifact" / "run" / case
+ folder = base / "evidence" / case / "DB/H2/mrf-checksum"
+ (base / "cases").mkdir(parents=True); (folder / "fixture").mkdir(parents=True)
+ write = lambda p, v: p.write_text(json.dumps(v))
+ candidate = {"source_sha": "a" * 40, "package_sha256": "b" * 64}
+ provenance = {"source_commit": "a" * 40, "candidate_sha256": "b" * 64,
+ "binary_sha256": "c" * 64, "testing_commit": "d" * 40}
+ write(base / "cases" / (case + ".json"), {"case_id": case, "verdict": "FAIL", "cleanup_status": "FAIL", "provenance": provenance})
+ row = {"topology": "H2", "fixture": "DB", "branch": "mrf-checksum", "verdict": "FAIL", "cleanup": "FAIL",
+ "failure": {"message": "MRF fault lacks an actual committed active owned responsibility on this exact slot"}}
+ write(folder.parents[2] / "healthy-summary.json", {"caseId": case, "cleanup": "FAIL", "complete": False,
+ "canonicalCasePassClaimed": False, "results": [row]})
+ write(folder / "00001-binding.json", {"runId": "gh-12345-1-h2", "bucketPrefix": bucket,
+ "candidateCommit": "a" * 40, "candidatePackageSha256": "b" * 64,
+ "candidateBinarySha256": "c" * 64, "harnessCommit": "d" * 40})
+ scene = {f"vm00{i}": {"quiescent": True, "pid": 0, "binarySha256": "c" * 64, "files": [], "formats": []} for i in range(4)}
+ records = {node: {"readOnly": True, "stable": True, "records": []} for node in scene}
+ for label, value in (("original-partial-write-physical", scene), ("original-physical-failure-scene", scene),
+ ("original-stopped-MRF-records", records), ("original-MRF-failure-scene", records)):
+ write(folder / ("00002-" + label + ".json"), value)
+ write(folder / "00003-gate-recovery.json", {"node": "vm003", "state": "CLEAR", "originalPermissionsRestored": True})
+ for seq in range(1, 8):
+ request = {"sequence": seq, "runId": "gh-12345-1-h2", "operation": "create_bucket" if seq == 1 else "put_object", "params": {"Bucket": bucket}}
+ path = folder / "fixture" / f"{seq:06d}-request.json"; write(path, request)
+ write(folder / "fixture" / f"{seq:06d}-ack.json", {"sequence": seq, "runId": request["runId"],
+ "requestFile": path.name, "requestSha256": hashlib.sha256(path.read_bytes()).hexdigest(), "operationCheck": "PASS",
+ "observation": {"operation": request["operation"], "params": request["params"], "clientCallInvoked": True,
+ "resultUnknown": False, "acknowledged": True}})
+ return candidate, folder
+
+ def test_unknown_mutation_drift_or_another_injection_keeps_namespace(self):
+ for change in (None, "unknown", "missing-ack", "digest", "physical", "records", "injection", "earlier-dirty"):
+ with tempfile.TemporaryDirectory() as directory, self.subTest(change=change):
+ root = Path(directory); candidate, folder = self.archive(root)
+ ack_path = folder / "fixture/000007-ack.json"
+ ack = json.loads(ack_path.read_text())
+ if change == "unknown": ack["observation"]["resultUnknown"] = True; ack_path.write_text(json.dumps(ack))
+ elif change == "missing-ack": ack_path.unlink()
+ elif change == "digest": ack["requestSha256"] = "e" * 64; ack_path.write_text(json.dumps(ack))
+ elif change in {"physical", "records"}:
+ label = "original-physical-failure-scene" if change == "physical" else "original-MRF-failure-scene"
+ path = next(folder.glob("*-" + label + ".json")); value = json.loads(path.read_text()); value["vm000"]["extra"] = True
+ if change == "physical": value["vm000"]["files"] = [{"disk": 0, "path": "changed"}]
+ path.write_text(json.dumps(value))
+ elif change == "injection": (folder / "00004-actual-original-MRF-CRC-injection.json").write_text("{}")
+ elif change == "earlier-dirty":
+ path = folder.parents[2] / "healthy-summary.json"; value = json.loads(path.read_text())
+ value["results"].insert(0, {"cleanup": "FAIL"}); path.write_text(json.dumps(value))
+ if change is None:
+ plan = recovery_plan(root, 12345, candidate)[0]
+ self.assertEqual(plan["kind"], "rejected-mrf-injection")
+ else:
+ with self.assertRaises(ValueError): recovery_plan(root, 12345, candidate)
+
+ def test_node_requires_empty_quarantine_and_exact_stopped_scene(self):
+ scene = {"files": [], "formats": []}
+ digest = hashlib.sha256(json.dumps(scene, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
+ with tempfile.TemporaryDirectory() as directory:
+ state = Path(directory).resolve(); roots = tuple(state / f"disk{i}" for i in range(4))
+ for root in roots: (root / "quarantine").mkdir(parents=True)
+ owner = {"runtimeProfile": "mrf-crash", "bucket": "rv-heal-0123456789abcdef", "binarySha256": "c" * 64}
+ request = {"runId": "gh-12345-1-h2", "action": "journal-rejection-audit-finalize", "bucket": owner["bucket"],
+ "binarySha256": "c" * 64, "expectedSceneSha256": digest, "rejectedFaultId": "F07-mrf-checksum-0-0-0",
+ "expectedMrfSha256": hashlib.sha256(b'{}').hexdigest()}
+ with patch.object(fleet_node, "owned", return_value=(state, roots, owner)), patch.object(fleet_node, "stopped"), \
+ patch.object(fleet_node, "digest", return_value="c" * 64), patch.object(fleet_node, "fault", heal_fault, create=True), \
+ patch.object(heal_fault, "mount_id", return_value=1), patch.object(fleet_node, "snapshot", return_value=scene), \
+ patch.object(fleet_node, "mrf_records", return_value={}), \
+ patch.object(fleet_node, "cleanup", return_value={"cleanup": "PASS"}) as cleanup:
+ self.assertTrue(fleet_node.journal_rejection_audit_finalize(request)["faultNeverApplied"])
+ cleanup.reset_mock()
+ self.assertTrue(fleet_node.journal_rejection_audit_finalize({**request, "action": "journal-rejection-audit"})["readyToFinalize"])
+ cleanup.assert_not_called()
+ for change in ({"expectedSceneSha256": "e" * 64}, {"expectedMrfSha256": "e" * 64}, {"binarySha256": "e" * 64},
+ {"rejectedFaultId": "F07-mrf-checksum-0-0-1"}, {"path": "/shared"}):
+ cleanup.reset_mock()
+ with self.assertRaises(ValueError): fleet_node.journal_rejection_audit_finalize({**request, **change})
+ cleanup.assert_not_called()
+ (roots[2] / "quarantine/interrupted.original").write_bytes(b"evidence")
+ cleanup.reset_mock()
+ with self.assertRaisesRegex(ValueError, "backup or intent"):
+ fleet_node.journal_rejection_audit_finalize(request)
+ cleanup.assert_not_called()
+
+
+class AdvancedMrfRecoveryTests(unittest.TestCase):
+ def archive(self, root):
+ from test_validation_heal_mrf import DurableMrfTests
+ from scripts.validation_heal import journal_fault as journal
+ candidate, folder = RejectedMrfRecoveryTests().archive(root)
+ def one(label): return next(folder.glob("*-" + label + ".json"))
+ summary = json.loads((folder.parents[2] / "healthy-summary.json").read_text())
+ summary["results"][0]["failure"]["message"] = "NOT_OBSERVED: original MRF corruption has no candidate diagnostic on vm000"
+ (folder.parents[2] / "healthy-summary.json").write_text(json.dumps(summary))
+ helper = DurableMrfTests(); observed = helper.observed()
+ observed["binarySha256"] = "c" * 64
+ original = {node: copy.deepcopy(observed) for node in (f"vm00{i}" for i in range(4))}
+ raw = helper.journal()
+ receipt = {"schemaVersion": 1, "runId": "gh-12345-1-h2", "disk": 0, "family": "mrf", "taskId": None,
+ "relativePath": ".rustfs.sys/.heal-mrf-snapshot.0.bin", "faultId": "F07-mrf-checksum-0-0-0",
+ "binarySha256": "c" * 64, "device": 1, "mountId": 2, "productRepairClaimed": False, "kind": "checksum",
+ "before": journal.entry(raw), "injected": journal.entry(raw[:-1] + bytes([raw[-1] ^ 1])),
+ "node": "vm000", "applied": True, "originalBackupRetained": True}
+ injected = copy.deepcopy(original)
+ row = next(row for row in injected["vm000"]["records"] if row["disk"] == 0 and row["relativePath"] == receipt["relativePath"])
+ row.update(receipt["injected"], rawBase64=base64.b64encode(raw[:-1] + bytes([raw[-1] ^ 1])).decode())
+ for label, value in (("original-stopped-MRF-records", original), ("injected-stopped-MRF-records", injected),
+ ("original-MRF-failure-scene", original)):
+ one(label).write_text(json.dumps(value)) if list(folder.glob("*-" + label + ".json")) else (folder / ("00004-" + label + ".json")).write_text(json.dumps(value))
+ (folder / "00005-actual-original-MRF-CRC-injection.json").write_text(json.dumps(receipt))
+ (folder / "00006-recovery.json").write_text(json.dumps({"cleanup": "FAIL", "errors": [{"state": "DIRTY",
+ "namespacesRetained": True, "error": "product changed or advanced the durable responsibility; refusing old-record resurrection"}]}))
+ return candidate, folder
+
+ def test_plan_requires_complete_acks_exact_injection_and_stopped_failure(self):
+ for change in (None, "unknown", "receipt", "records", "candidate", "extra-injection", "earlier-dirty"):
+ with tempfile.TemporaryDirectory() as temp, self.subTest(change=change):
+ root = Path(temp); candidate, folder = self.archive(root)
+ if change == "unknown":
+ path = folder / "fixture/000007-ack.json"; value = json.loads(path.read_text())
+ value["observation"]["resultUnknown"] = True; path.write_text(json.dumps(value))
+ elif change == "receipt":
+ path = next(folder.glob("*-actual-original-MRF-CRC-injection.json")); value = json.loads(path.read_text())
+ value["injected"]["sha256"] = "f" * 64; path.write_text(json.dumps(value))
+ elif change == "records":
+ path = next(folder.glob("*-original-MRF-failure-scene.json")); value = json.loads(path.read_text())
+ value["vm000"]["records"][0]["rawBase64"] = "bad"; path.write_text(json.dumps(value))
+ elif change == "candidate": candidate["package_sha256"] = "f" * 64
+ elif change == "extra-injection": (folder / "00007-actual-original-MRF-CRC-injection.json").write_text("{}")
+ elif change == "earlier-dirty":
+ path = folder.parents[2] / "healthy-summary.json"; value = json.loads(path.read_text())
+ value["results"].insert(0, {"cleanup": "FAIL"}); path.write_text(json.dumps(value))
+ if change is None:
+ plan = recovery_plan(root, 12345, candidate)[0]
+ self.assertEqual(plan["kind"], "advanced-mrf-fixture")
+ self.assertEqual(set(plan["faults"]), {"vm000"})
+ else:
+ with self.assertRaises((ValueError, binascii.Error)): recovery_plan(root, 12345, candidate)
+
+ def test_remote_retained_audit_loads_real_guard_backends(self):
+ with tempfile.TemporaryDirectory() as temp:
+ fleet = Fleet(["vm000"], Path(temp), "gh-12345-1-h2")
+ def execute(*args, **kwargs):
+ namespace = {"__name__": "__validation_node_contract__"}
+ with patch.dict(sys.modules): exec(compile(kwargs["input"].rsplit("\nmain(", 1)[0], "remote-node.py", "exec"), namespace)
+ self.assertTrue({"fault", "mrf", "journal_fault_backend", "encryption_runtime", "gate", "quorum_gate_backend", "replacement"} <= set(namespace))
+ return types.SimpleNamespace(returncode=0, stdout="{}", stderr="")
+ with patch("scripts.validation_heal.healthy.subprocess.run", side_effect=execute):
+ for action in ("journal-retained-audit", "journal-retained-audit-finalize"): fleet.remote("vm000", action)
+
+ def test_node_exports_original_backup_and_rejects_drift_before_deletion(self):
+ import base64
+ from test_validation_heal_mrf import DurableMrfTests
+ from scripts.validation_heal import journal_fault as journal, mrf_records as mrf
+ for change in (None, "foreign-backup", "extra", "missing", "intent", "scene", "records", "gate", "path"):
+ with tempfile.TemporaryDirectory() as temp, self.subTest(change=change):
+ state = Path(temp).resolve(); roots = tuple(state / f"disk{i}" for i in range(4))
+ for root in roots: (root / "quarantine").mkdir(parents=True)
+ owner = {"runId": "gh-12345-1-h2", "runtimeProfile": "mrf-crash", "bucket": "rv-heal-0123456789abcdef", "binarySha256": "c" * 64}
+ raw = DurableMrfTests().journal()
+ params = {"runId": owner["runId"], "disk": 0, "family": "mrf", "taskId": None,
+ "relativePath": ".rustfs.sys/.heal-mrf-snapshot.0.bin", "faultId": "F07-mrf-checksum-0-0-0", "kind": "checksum"}
+ with patch.object(heal_fault, "mount_id", return_value=2), heal_fault.directory(roots[0]) as fd:
+ intent = {**journal.binding(fd, params, owner, heal_fault), "kind": "checksum",
+ "before": journal.entry(raw), "injected": journal.entry(journal.transform(raw, params, owner, mrf))}
+ backup = roots[0] / "quarantine"
+ for suffix, data in ((".original", raw), (".intent.json", heal_fault.encode(intent)), (".applied.json", heal_fault.encode(intent))):
+ (backup / (params["faultId"] + suffix)).write_bytes(data)
+ scene = {"files": [], "formats": []}; records = {"readOnly": True, "stable": True, "records": []}
+ sha = lambda value: hashlib.sha256(json.dumps(value, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
+ request = {"runId": owner["runId"], "action": "journal-retained-audit-finalize", "bucket": owner["bucket"],
+ "binarySha256": owner["binarySha256"], "faults": [intent], "originalBackup": {params["faultId"]: base64.b64encode(raw).decode()},
+ "expectedSceneSha256": sha(scene), "expectedMrfSha256": sha(records)}
+ if change == "foreign-backup": (backup / (params["faultId"] + ".original")).write_bytes(b"foreign")
+ elif change == "extra": (roots[1] / "quarantine/unregistered.original").write_bytes(b"evidence")
+ elif change == "missing": (backup / (params["faultId"] + ".applied.json")).unlink()
+ elif change == "intent": (backup / (params["faultId"] + ".intent.json")).write_text("{}")
+ elif change == "scene": request["expectedSceneSha256"] = "f" * 64
+ elif change == "records": request["expectedMrfSha256"] = "f" * 64
+ elif change == "path": intent["relativePath"] = ".rustfs.sys/foreign"
+ with patch.object(fleet_node, "owned", return_value=(state, roots, owner)), patch.object(fleet_node, "stopped"), \
+ patch.object(fleet_node, "digest", return_value="c" * 64), patch.object(fleet_node, "fault", heal_fault, create=True), \
+ patch.object(heal_fault, "mount_id", return_value=2), patch.object(fleet_node, "journal_fault_backend", journal, create=True), \
+ patch.object(fleet_node, "mrf", mrf, create=True), patch.object(fleet_node, "mrf_records", return_value=records), \
+ patch.object(fleet_node, "snapshot", return_value=scene), patch.object(fleet_node, "gate_module") as gate, \
+ patch.object(fleet_node, "cleanup", return_value={"cleanup": "PASS", "unitStopped": True, "rootsRemoved": True}) as cleanup:
+ gate.return_value.checked_gate.return_value = {} if change == "gate" else None
+ if change is None:
+ audit = fleet_node.journal_retained_audit_finalize({**request, "action": "journal-retained-audit"})
+ cleanup.assert_not_called()
+ self.assertEqual(base64.b64decode(audit["originalJournalBackups"][0]["original"]["rawBase64"]), raw)
+ result = fleet_node.journal_retained_audit_finalize(request)
+ self.assertFalse(result["backupRestorationUsed"])
+ self.assertFalse(result["productRepairClaimed"])
+ cleanup.assert_called_once()
+ else:
+ with self.assertRaises(ValueError): fleet_node.journal_retained_audit_finalize(request)
+ cleanup.assert_not_called()
+
+
+class RecreationRecoveryTests(unittest.TestCase):
+ def archive(self, root):
+ case, bucket = "heal.bucket-incarnation-recreation", "rv-heal-0123456789abcdef"
+ base = root / "rustfs-validation-12345-1" / "run" / case
+ folder = base / "evidence" / case / "H1/old-metadata-rejoin"
+ (base / "cases").mkdir(parents=True)
+ (folder / "fixture").mkdir(parents=True)
+ candidate = {"source_sha": "a" * 40, "package_sha256": "b" * 64}
+ provenance = {"source_commit": "a" * 40, "candidate_sha256": "b" * 64,
+ "binary_sha256": "c" * 64, "testing_commit": "d" * 40}
+ write = lambda path, value: path.write_text(json.dumps(value))
+ write(base / "cases" / (case + ".json"), {"case_id": case, "verdict": "FAIL",
+ "cleanup_status": "FAIL", "provenance": provenance})
+ write(folder.parent.parent / "healthy-summary.json", {"caseId": case, "cleanup": "FAIL",
+ "complete": False, "canonicalCasePassClaimed": False, "results": [{"topology": "H1",
+ "fixture": "DV", "branch": "old-metadata-rejoin", "verdict": "FAIL", "cleanup": "FAIL",
+ "failure": {"type": "RecordedCallError", "message": "unexpected delete_bucket result; inspect ACK 000005, do not silently retry"},
+ "unresolvedMutation": {"operation": "normal-S3-delete-and-recreate"}}]})
+ write(folder / "00001-binding.json", {"runId": "gh-12345-1-h1", "bucketPrefix": bucket,
+ "candidateCommit": "a" * 40, "candidatePackageSha256": "b" * 64,
+ "candidateBinarySha256": "c" * 64, "harnessCommit": "d" * 40})
+ write(folder / "00002-original-recreation-failure-scene.json", {"vm000": {"quiescent": True,
+ "pid": 0, "binarySha256": "c" * 64, "formats": [], "files": [{"disk": 0,
+ "path": bucket + "/objects/key/xl.meta", "size": 1, "sha256": "e" * 64}]}})
+ write(folder / "00003-old-ACK-manifest.json", {"expected": [{"key": "objects/key", "versionId": version}
+ for version in ("old-data", "old-marker")]})
+ write(folder / "00004-gate-recovery.json", {"node": "vm000", "state": "CLEAR", "originalPermissionsRestored": True})
+ calls = [("create_bucket", {"Bucket": bucket}),
+ ("delete_object", {"Bucket": bucket, "Key": "objects/key", "VersionId": "old-data"}),
+ ("delete_object", {"Bucket": bucket, "Key": "objects/key", "VersionId": "old-marker"}),
+ ("list_multipart_uploads", {"Bucket": bucket}), ("delete_bucket", {"Bucket": bucket})]
+ for sequence, (operation, params) in enumerate(calls, 1):
+ request_path = folder / "fixture" / f"{sequence:06d}-request.json"
+ write(request_path, {"sequence": sequence, "runId": "gh-12345-1-h1", "operation": operation, "params": params})
+ write(folder / "fixture" / f"{sequence:06d}-ack.json", {"sequence": sequence, "runId": "gh-12345-1-h1",
+ "requestFile": request_path.name, "requestSha256": hashlib.sha256(request_path.read_bytes()).hexdigest(),
+ "operationCheck": "FAIL" if sequence == 5 else "PASS", "observation": {"operation": operation,
+ "params": params, "clientCallInvoked": True, "resultUnknown": False, "acknowledged": sequence != 5,
+ "response": {"Error": {"Code": "InternalError", "Message": "File access denied"},
+ "ResponseMetadata": {"HTTPStatusCode": 500}} if sequence == 5 else {}}})
+ return candidate, folder
+
+ def test_known_refusal_retains_original_failure_without_claiming_product_repair(self):
+ with tempfile.TemporaryDirectory() as temp:
+ root = Path(temp); candidate, _ = self.archive(root)
+ plan = recovery_plan(root, 12345, candidate)[0]
+ self.assertEqual(plan["kind"], "rejected-incarnation-delete")
+ self.assertEqual(plan["acknowledgedVersionDeletions"], 2)
+ self.assertFalse(plan["unknownMutationClaimed"])
+ self.assertEqual(plan["originalProductError"]["Code"], "InternalError")
+
+ def test_unknown_calls_missing_ack_drifted_digest_and_different_refusal_are_rejected(self):
+ for mutation in ("unknown-final", "unknown-prior", "digest", "missing", "different-error", "foreign-request"):
+ with self.subTest(mutation=mutation), tempfile.TemporaryDirectory() as temp:
+ root = Path(temp); candidate, folder = self.archive(root)
+ path = folder / "fixture" / ("000002-ack.json" if mutation == "unknown-prior" else "000005-ack.json")
+ value = json.loads(path.read_text())
+ if mutation.startswith("unknown-"): value["observation"]["resultUnknown"] = True
+ elif mutation == "digest": value["requestSha256"] = "f" * 64
+ elif mutation == "different-error": value["observation"]["response"]["Error"]["Code"] = "BucketNotEmpty"
+ elif mutation == "foreign-request": value["observation"]["params"]["Bucket"] = "unowned"
+ if mutation == "missing": path.unlink()
+ else: path.write_text(json.dumps(value))
+ with self.assertRaises(ValueError): recovery_plan(root, 12345, candidate)
+
+ def test_running_scene_additional_case_or_replay_transaction_cannot_be_finalized(self):
+ for mutation in ("running", "source", "another-case", "replay", "successor", "unrecovered-gate"):
+ with self.subTest(mutation=mutation), tempfile.TemporaryDirectory() as temp:
+ root = Path(temp); candidate, folder = self.archive(root)
+ if mutation == "running":
+ path = folder / "00002-original-recreation-failure-scene.json"
+ value = json.loads(path.read_text()); value["vm000"]["pid"] = 99; path.write_text(json.dumps(value))
+ elif mutation == "source": candidate["source_sha"] = "f" * 40
+ elif mutation == "another-case":
+ path = folder.parents[4] / "another/cases/another.json"
+ path.parent.mkdir(parents=True); path.write_text("{}")
+ elif mutation == "unrecovered-gate":
+ path = folder / "00004-gate-recovery.json"
+ value = json.loads(path.read_text()); value["state"] = "ACTIVE"; path.write_text(json.dumps(value))
+ else: (folder / ("00005-" + ("archive-and-detach-original-replay" if mutation == "replay" else "successor-ACK-bound-S3") + ".json")).write_text("{}")
+ with self.assertRaises(ValueError): recovery_plan(root, 12345, candidate)
+
+ def test_finalization_requires_stopped_unchanged_candidate_gate_and_credential_boundary(self):
+ for mutation in (None, "scene-drift", "binary", "gate", "credential"):
+ with self.subTest(mutation=mutation), tempfile.TemporaryDirectory() as temp:
+ root = Path(temp); candidate, _ = self.archive(root)
+ plan = recovery_plan(root, 12345, candidate)[0]
+ events = []
+ def snapshot(self, label, **kwargs):
+ events.append(label)
+ scene = copy.deepcopy(plan["before"])
+ if mutation == "scene-drift" and label == "physical-immediately-before-finalization": scene["vm000"]["files"] = []
+ if mutation == "binary": scene["vm000"]["binarySha256"] = "f" * 64
+ return scene
+ def remote(self, node, action, **params):
+ events.append(action)
+ if action == "namespace-state": return {"state": {"exists": True}, "roots": [{"exists": True}]}
+ if action == "gate-state": return {"state": "ACTIVE" if mutation == "gate" else "CLEAR"}
+ if action == "bucket-record-snapshot": return {"credentialsRead": mutation == "credential"}
+ if action == "cleanup": return {"cleanup": "PASS", "unitStopped": True, "rootsRemoved": True}
+ return {}
+ candidate_path = root / "candidate.json"; candidate_path.write_text(json.dumps(candidate))
+ argv = ["recover_environment.py", "--failed-run-id", "12345", "--artifact", str(root),
+ "--candidate", str(candidate_path), "--evidence", str(root / "recovery")]
+ with patch("sys.argv", argv), patch.object(Fleet, "remote", remote), patch.object(Fleet, "snapshot", snapshot), \
+ patch.dict("os.environ", {"RUSTFS_NODES": "vm000 vm001 vm002 vm003"}):
+ if mutation is None: recovery.main()
+ else:
+ with self.assertRaises(ValueError): recovery.main()
+ if mutation is None:
+ self.assertLess(events.index("bucket-record-snapshot"), events.index("cleanup"))
+ summary = json.loads((root / "recovery/recovery-summary.json").read_text())
+ self.assertFalse(summary["canonicalPassClaimed"])
+ self.assertFalse(summary["environments"][0]["originalVerdictRewritten"])
+ else: self.assertNotIn("cleanup", events)
+
+
+class QuotaRecoveryTests(unittest.TestCase):
+ def test_standalone_remote_recovery_loads_all_cleanup_backends(self):
+ import types
+ with tempfile.TemporaryDirectory() as temp, patch("scripts.validation_heal.healthy.subprocess.run",
+ return_value=types.SimpleNamespace(returncode=0, stdout='{}', stderr='')) as command:
+ fleet = Fleet(["node"], Path(temp), "gh-12345-1-h1")
+ fleet.remote("node", "quota-audit-finalize", bucket="rv-heal-0123456789abcdef",
+ binarySha256="a" * 64, expectedSceneSha256="b" * 64)
+ source = command.call_args.kwargs["input"].rsplit("\nmain(", 1)[0]
+ scope = {"__name__": "validation_remote_recovery"}
+ exec(compile(source, "standalone-recovery", "exec"), scope)
+ self.assertTrue(callable(scope["quota_audit_finalize"]))
+ self.assertTrue(callable(scope["namespace_state"]))
+ self.assertTrue(callable(scope["quorum_gate_backend"].release))
+ self.assertTrue(callable(scope["replacement"].attest))
+ self.assertTrue(callable(scope["network_gate_backend"].release))
+ self.assertTrue(callable(scope["encryption_module"]().attest))
+
+ def test_standalone_rejected_id_finalization_loads_snapshot_backend(self):
+ import types
+ with tempfile.TemporaryDirectory() as temp, patch("scripts.validation_heal.healthy.subprocess.run",
+ return_value=types.SimpleNamespace(returncode=0, stdout='{}', stderr='')) as command:
+ Fleet(["node"], Path(temp), "gh-12345-1-h1").remote("node", "audit-finalize")
+ source = command.call_args.kwargs["input"].rsplit("\nmain(", 1)[0]
+ scope = {"__name__": "validation_remote_recovery"}
+ exec(compile(source, "standalone-recovery", "exec"), scope)
+ self.assertTrue(callable(scope["encryption_module"]().attest))
+
+ def archive(self, root):
+ case = "heal.enospc-inodes-and-quota"
+ base = root / "rustfs-validation-12345-1" / "run" / case
+ directory = base / "evidence" / case
+ branch = directory / "H1/bucket-quota"
+ branch.mkdir(parents=True)
+ def write(path, value): path.write_text(json.dumps(value))
+ candidate = {"source_sha": "a" * 40, "package_sha256": "b" * 64}
+ provenance = {"source_commit": "a" * 40, "candidate_sha256": "b" * 64,
+ "binary_sha256": "c" * 64, "testing_commit": "d" * 40}
+ (base / "cases").mkdir()
+ write(base / "cases" / (case + ".json"), {"case_id": case, "verdict": "FAIL", "cleanup_status": "FAIL", "provenance": provenance})
+ write(directory / "healthy-summary.json", {"cleanup": "FAIL", "results": [{"topology": "H1", "branch": "bucket-quota",
+ "verdict": "FAIL", "cleanup": "FAIL", "failure": {"message": "peer did not observe the exact acknowledged quota limit"}, "recoveryError": "connection refused"}]})
+ bucket = "rv-heal-0123456789abcdef"
+ binding = {"runId": "gh-12345-1-h1", "bucketPrefix": bucket, "candidateBinarySha256": "c" * 64, "harnessCommit": "d" * 40}
+ write(branch / "00001-binding.json", binding)
+ entries = [{"disk": 0, "path": f"{bucket}/objects/{index}/part.1", "size": 100 + index, "sha256": str(index) * 64} for index in range(2)]
+ control = {"disk": 1, "path": bucket + "/control/xl.meta", "size": 5, "sha256": "e" * 64}
+ scene = {"node": {"quiescent": True, "formats": [{"disk": "actual-format"}], "files": [*entries, control], "backgroundConfig": None}}
+ write(branch / "00002-original-complete-physical.json", scene)
+ write(branch / "00003-physical-original-failure-before-any-rescue.json", {"node": {**scene["node"], "files": [control]}})
+ write(branch / "00004-manual-source-isolation-before-fault.json", {"node": {"backgroundConfig": {"RUSTFS_SCANNER_ENABLED": "false"}, "binarySha256": "c" * 64}})
+ write(branch / "00005-owned-bucket-quota-PUT.json", {"path": "/rustfs/admin/v3/quota/" + bucket, "method": "PUT", "httpStatus": 200,
+ "response": {"quota": 1024**2, "quota_type": "HARD"}})
+ write(branch / "00006-all-peer-owned-quota-GET.json", {"path": "/rustfs/admin/v3/quota/" + bucket, "method": "GET", "httpStatus": 503,
+ "response": "authoritative bucket usage is not available yet"})
+ for index, entry in enumerate(entries):
+ write(branch / f"0001{index}-exact-missing-part.json", {"runId": binding["runId"], "faultId": f"I02-target-{index}",
+ "kind": "missing", "relativePath": entry["path"], "before": {"size": entry["size"], "sha256": entry["sha256"]}})
+ return candidate, branch, directory
+
+ def test_recovery_is_bound_to_failed_workflow_candidate_and_both_backups(self):
+ with tempfile.TemporaryDirectory() as temp:
+ root = Path(temp)
+ candidate, _, _ = self.archive(root)
+ plan = recovery_plan(root, 12345, candidate)[0]
+ self.assertEqual(plan["kind"], "owned-quota")
+ self.assertEqual(len(plan["faults"]), 2)
+ self.assertEqual(plan["nodes"], ["node"])
+ for run, value in [(12346, candidate), (12345, {**candidate, "package_sha256": "f" * 64})]:
+ with self.assertRaises(ValueError): recovery_plan(root, run, value)
+
+
+ def test_retry_accepts_only_original_rescued_shards_and_no_other_drift(self):
+ with tempfile.TemporaryDirectory() as temp:
+ root = Path(temp)
+ candidate, _, _ = self.archive(root)
+ plan = recovery_plan(root, 12345, candidate)[0]
+ self.assertTrue(recovery_scene_matches(plan["failure"], plan))
+ self.assertTrue(recovery_scene_matches(plan["before"], plan))
+ partial = json.loads(json.dumps(plan["failure"]))
+ partial["node"]["files"].append(plan["before"]["node"]["files"][0])
+ self.assertTrue(recovery_scene_matches(partial, plan))
+ for changed in ["format", "control", "rescued", "missing-control", "extra", "duplicate", "node"]:
+ current = json.loads(json.dumps(partial))
+ if changed == "format": current["node"]["formats"] = []
+ elif changed == "control": current["node"]["files"][0]["sha256"] = "wrong"
+ elif changed == "rescued": current["node"]["files"][1]["sha256"] = "wrong"
+ elif changed == "missing-control": current["node"]["files"].pop(0)
+ elif changed == "extra": current["node"]["files"].append({"disk": 3, "path": "foreign"})
+ elif changed == "duplicate": current["node"]["files"].append(current["node"]["files"][0])
+ else: current["foreign"] = current.pop("node")
+ with self.subTest(changed=changed):
+ self.assertFalse(recovery_scene_matches(current, plan))
+
+ def test_uncertain_workload_and_collateral_drift_always_keep_namespace(self):
+ for changed in ["invoked-PUT", "unknown-ACK", "collateral", "wrong-run", "duplicate-fault", "other-retained"]:
+ with self.subTest(changed=changed), tempfile.TemporaryDirectory() as temp:
+ root = Path(temp); candidate, branch, directory = self.archive(root)
+ if changed == "invoked-PUT": (branch / "negative-PUT").mkdir()
+ elif changed == "unknown-ACK":
+ path = directory / "healthy-summary.json"; data = json.loads(path.read_text())
+ data["results"][0]["negativePUT"] = {"state": "UNKNOWN"}; path.write_text(json.dumps(data))
+ elif changed == "collateral":
+ path = branch / "00003-physical-original-failure-before-any-rescue.json"; data = json.loads(path.read_text())
+ data["node"]["files"][0]["sha256"] = "new"; path.write_text(json.dumps(data))
+ elif changed in {"wrong-run", "duplicate-fault"}:
+ path = branch / "00011-exact-missing-part.json"; data = json.loads(path.read_text())
+ data["runId" if changed == "wrong-run" else "faultId"] = "gh-12346-1-h1" if changed == "wrong-run" else "I02-target-0"
+ path.write_text(json.dumps(data))
+ else:
+ path = directory / "healthy-summary.json"; data = json.loads(path.read_text())
+ data["results"].append({"cleanup": "FAIL", "branch": "another"}); path.write_text(json.dumps(data))
+ with self.assertRaises(ValueError): recovery_plan(root, 12345, candidate)
+
+ def test_node_finalization_requires_same_private_candidate_and_full_rescue(self):
+ scene = {"files": [{"path": "owned/key/part.1", "sha256": "actual"}], "formats": [{"disk": "original"}]}
+ scene_sha = hashlib.sha256(json.dumps(scene, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
+ with tempfile.TemporaryDirectory() as temp:
+ state = Path(temp); roots = [state / str(index) for index in range(4)]
+ bucket = "rv-heal-0123456789abcdef"
+ owner = {"runtimeProfile": "manual", "bucket": bucket, "binarySha256": "a" * 64,
+ "volumes": [str(root / "data") for root in roots]}
+ request = {"runId": "gh-12345-1-h1", "action": "quota-audit-finalize", "bucket": bucket,
+ "binarySha256": "a" * 64, "expectedSceneSha256": scene_sha}
+ with patch("scripts.validation_heal.fleet_node.owned", return_value=(state, roots, owner)), \
+ patch("scripts.validation_heal.fleet_node.stopped"), patch("scripts.validation_heal.fleet_node.digest", return_value="a" * 64), \
+ patch("scripts.validation_heal.fleet_node.snapshot", return_value=scene), \
+ patch("scripts.validation_heal.fleet_node.cleanup", return_value={"cleanup": "PASS"}) as cleanup:
+ value = quota_audit_finalize(request)
+ self.assertTrue(value["privateBucketQuotaNamespaceRemoved"])
+ self.assertFalse(value["serviceStartedDuringRecovery"])
+ self.assertFalse(value["productRepairClaimed"])
+ cleanup.reset_mock()
+ for changes in [{"bucket": "foreign"}, {"binarySha256": "b" * 64}, {"runId": "gh-12345-1-h2"}, {"expectedSceneSha256": "b" * 64}]:
+ with self.assertRaises(ValueError): quota_audit_finalize({**request, **changes})
+ cleanup.assert_not_called()
+ (state / "drive-gate.json").write_text("active")
+ with self.assertRaises(ValueError): quota_audit_finalize(request)
+ cleanup.assert_not_called()
+
+ def test_release_restarts_stopped_service_and_reads_config_without_usage_claim(self):
+ import types
+ events = []
+ binding = {"endpoints": {"node1": "http://owned"}}
+ def admin(path, query, body, method):
+ events.append((method, path, query))
+ if method == "PUT": return {"httpStatus": 200, "response": {"bucket": "owned", "quota": 100, "quota_type": "HARD"}}
+ return {"httpStatus": 200, "response": {"quota": 100, "size": 100, "quotatype": "hard"}}
+ probe = types.SimpleNamespace(binding=binding, admin=admin)
+ fleet = types.SimpleNamespace(nodes=["node"], remote=lambda *args: {"pid": 0},
+ start=lambda: events.append("start"), record=lambda label, value: value)
+ with patch("scripts.validation_heal.capacity.Probe", return_value=probe):
+ result = release_quota(probe, fleet, "owned", 100)
+ self.assertEqual(events[0], "start")
+ self.assertEqual(events[-1], ("GET", "/rustfs/admin/v3/get-bucket-quota", [("bucket", "owned")]))
+ self.assertFalse(result["authoritativeUsageClaimed"])
+
+
+class PerformanceBoundaryRecoveryTests(unittest.TestCase):
+ def archive(self, root):
+ case = "heal.performance-abba"
+ base = root / "rustfs-validation-12345-1" / "run" / case
+ evidence = base / "evidence" / case / "H2"
+ (base / "cases").mkdir(parents=True)
+ evidence.mkdir(parents=True)
+ candidate = {"source_sha": "a" * 40, "package_sha256": "b" * 64}
+ provenance = {"source_commit": "a" * 40, "candidate_sha256": "b" * 64,
+ "binary_sha256": "c" * 64, "testing_commit": "d" * 40}
+ write = lambda path, value: path.write_text(json.dumps(value))
+ write(base / "cases" / (case + ".json"), {"case_id": case, "verdict": "FAIL",
+ "cleanup_status": "FAIL", "provenance": provenance})
+ write(evidence.parent / "healthy-summary.json", {"cleanup": "FAIL", "results": [{"topology": "H2",
+ "verdict": "FAIL", "cleanup": "FAIL", "failure": {"type": "ValueError",
+ "message": "Heal node vm000 apply-fault failed: fault lease expired"}}]})
+ scene = {node: {"formats": [{"disk": node}], "files": [
+ {"path": "rv-heal-0123456789abcd/objects/key/part.1", "sha256": node, "size": 1},
+ {"path": "rv-heal-0123456789abcd-load/load/00.bin/xl.meta", "sha256": node, "size": 1},
+ ]} for node in ("vm000", "vm001", "vm002", "vm003")}
+ binding = {"runId": "gh-12345-1-h2", "bucketPrefix": "rv-heal-0123456789abcd",
+ "candidateBinarySha256": "c" * 64, "harnessCommit": "d" * 40}
+ write(evidence / "00001-binding.json", binding)
+ write(evidence / "00002-phase-protocol-g3-p3-B.json", {"group": 3, "phase": 3, "mode": "B"})
+ write(evidence / "00003-physical-phase-baseline-g3-p3-B.json", scene)
+ write(evidence / "00004-physical-failure.json", scene)
+ return candidate, evidence
+
+ def test_only_unapplied_fault_boundary_can_be_finalized(self):
+ with tempfile.TemporaryDirectory() as temp:
+ root = Path(temp)
+ candidate, evidence = self.archive(root)
+ plan = recovery_plan(root, 12345, candidate)[0]
+ self.assertEqual(plan["kind"], "performance-no-fault-applied")
+ self.assertEqual(plan["nodes"], ["vm000", "vm001", "vm002", "vm003"])
+ (evidence / "00005-fault-g3-p3-B.json").write_text("{}")
+ with self.assertRaises(ValueError): recovery_plan(root, 12345, candidate)
+
+ def test_full_performance_finalization_cleans_each_node_once(self):
+ with tempfile.TemporaryDirectory() as temp:
+ root = Path(temp)
+ candidate, _ = self.archive(root)
+ plan = recovery_plan(root, 12345, candidate)[0]
+ candidate_path = root / "candidate.json"
+ candidate_path.write_text(json.dumps(candidate))
+ calls = []
+ def remote(node, action, **params):
+ calls.append((node, action))
+ if action == "namespace-state":
+ return {"state": {"exists": True}, "roots": [{"exists": True}]}
+ if action == "logs": return {}
+ self.assertEqual(action, "cleanup")
+ return {"cleanup": "PASS", "unitStopped": True, "rootsRemoved": True}
+ fake = types.SimpleNamespace(record=lambda label, value: value,
+ snapshot=lambda *args, **kwargs: plan["before"], remote=remote)
+ argv = ["recover_environment.py", "--failed-run-id", "12345", "--artifact", str(root),
+ "--candidate", str(candidate_path), "--evidence", str(root / "recovery")]
+ with patch.object(recovery, "Fleet", return_value=fake), patch("sys.argv", argv), \
+ patch.dict("os.environ", RUSTFS_NODES="vm000 vm001 vm002 vm003"):
+ recovery.main()
+ self.assertEqual([node for node, action in calls if action == "cleanup"], plan["nodes"])
+ self.assertEqual(json.loads((root / "recovery/recovery-summary.json").read_text())["cleanup"], "PASS")
+
+ def test_absent_namespace_with_active_or_unknown_service_cannot_claim_cleanup(self):
+ for service in ({"MainPID": "7", "ActiveState": "active", "SubState": "running"},
+ {}, {"MainPID": "0", "ActiveState": "active", "SubState": "exited"}):
+ with self.subTest(service=service), tempfile.TemporaryDirectory() as temp:
+ root = Path(temp)
+ candidate, _ = self.archive(root)
+ candidate_path = root / "candidate.json"
+ candidate_path.write_text(json.dumps(candidate))
+ def remote(node, action):
+ self.assertEqual(action, "namespace-state")
+ return {"state": {"exists": False}, "roots": [{"exists": False}], "service": service}
+ fake = types.SimpleNamespace(record=lambda label, value: value, remote=remote)
+ argv = ["recover_environment.py", "--failed-run-id", "12345", "--artifact", str(root),
+ "--candidate", str(candidate_path), "--evidence", str(root / "recovery")]
+ with patch.object(recovery, "Fleet", return_value=fake), patch("sys.argv", argv), \
+ patch.dict("os.environ", RUSTFS_NODES="vm000 vm001 vm002 vm003"):
+ with self.assertRaisesRegex(ValueError, "active or unknown service"):
+ recovery.main()
+ self.assertFalse((root / "recovery/recovery-summary.json").exists())
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test_validation_heal_retained_scene.py b/test_validation_heal_retained_scene.py
new file mode 100644
index 00000000..044ab682
--- /dev/null
+++ b/test_validation_heal_retained_scene.py
@@ -0,0 +1,302 @@
+"""Incomplete scenes remain preserved and cannot become restored PASS evidence."""
+
+import copy
+import hashlib
+import json
+import os
+from pathlib import Path
+import shutil
+import socket
+import stat
+import tempfile
+import types
+import unittest
+from unittest.mock import patch
+
+from scripts.validation_heal import fleet_node, retained_scene as retained, recover_environment as recovery, mpu_scene
+from scripts.validation_heal.healthy import Fleet
+
+
+class RetainedSceneTests(unittest.TestCase):
+ def scene(self, directory):
+ base = Path(directory).resolve()
+ mounts = [base / str(index) for index in range(4)]
+ for mount in mounts: mount.mkdir()
+ run_id = "gh-12345-1-h2"
+ state = base / "state" / run_id; state.mkdir(parents=True, mode=0o700)
+ roots = tuple(mount / "heal-validation" / run_id for mount in mounts)
+ binary = b"original-candidate"
+ owner = {"runId": run_id, "unit": "rustfs-validation-heal-" + run_id + ".service",
+ "hostname": socket.gethostname(), "runtimeProfile": "mrf-crash", "bucket": "rv-heal-0123456789abcdef",
+ "binarySha256": hashlib.sha256(binary).hexdigest(), "mounts": [
+ {key: str(index) for key in ("target", "source", "fstype", "uuid", "maj:min")} for index in range(4)]}
+ (state / "rustfs").write_bytes(binary)
+ (state / "owner.json").write_text(json.dumps(owner))
+ (state / "service.env").write_bytes(b"credential-must-never-be-read")
+ for root in roots:
+ (root / "data" / owner["bucket"]).mkdir(parents=True)
+ (root / "data" / owner["bucket"] / "part.1").write_bytes(b"acknowledged-fixture")
+ (root / "data" / ".rustfs.sys" / "config" / "iam").mkdir(parents=True)
+ (root / "data" / ".rustfs.sys" / "config" / "iam" / "credential").write_bytes(b"private-iam")
+ (root / "quarantine").mkdir()
+ (root / "quarantine" / "original").write_bytes(b"original-backup")
+ (root / "owner.json").write_text(json.dumps(owner))
+ shutil.rmtree(roots[0])
+ node = {"paths": lambda run: (state, roots), "unit": lambda run: owner["unit"], "socket": socket,
+ "mounts": lambda: copy.deepcopy(owner["mounts"]), "stopped": lambda value: {}, "MOUNTS": mounts,
+ "digest": fleet_node.digest, "checked": fleet_node.checked}
+ request = {"runId": run_id, "action": "retained-scene-audit", "expectedOwner": owner}
+ return state, roots, node, request
+
+ @staticmethod
+ def local_rename(source, target):
+ if target.exists(): raise ValueError("destination exists")
+ source.rename(target)
+ retained.sync_directory(source.parent)
+ retained.sync_directory(target.parent)
+
+ def test_real_files_keep_inodes_bytes_and_retry_after_each_interrupted_move(self):
+ for interrupt_after in (1, 2, 3, 4):
+ with self.subTest(interrupt_after=interrupt_after), tempfile.TemporaryDirectory() as directory:
+ state, roots, node, request = self.scene(directory)
+ # Run the full production ownership check using root-owned
+ # stat proxies, without replacing filesystem identities.
+ with self.root_stats(), patch.object(retained, "rename_new", side_effect=self.local_rename):
+ before = retained.run(request, node)
+ self.assertFalse((state.parent / "retained").exists())
+ original = {p: (p.stat().st_ino, p.read_bytes()) for root in (*roots[1:], state)
+ for p in root.rglob("*") if p.is_file()}
+ count = 0
+ def interrupted(source, target):
+ nonlocal count
+ self.local_rename(source, target)
+ if target.is_dir():
+ count += 1
+ if count == interrupt_after: raise RuntimeError("lost SSH after atomic rename")
+ change = {**request, "action": "retained-scene-isolate", "expectedBaseline": before["baseline"]}
+ with patch.object(retained, "rename_new", side_effect=interrupted), self.assertRaises(RuntimeError):
+ retained.run(change, node)
+ result = retained.run(change, node)
+ self.assertTrue(result["activeNamespaceIsolated"])
+ self.assertEqual(result["baseline"], before["baseline"])
+ self.assertEqual(result["locations"], ["absent", "retained", "retained", "retained", "retained"])
+ retained_state, retained_roots = retained.destinations(state, roots)
+ for old, (inode, raw) in original.items():
+ source = next(root for root in (*roots[1:], state) if old.is_relative_to(root))
+ target = retained_state if source == state else retained_roots[roots.index(source)]
+ moved = target / old.relative_to(source)
+ self.assertEqual((moved.stat().st_ino, moved.read_bytes()), (inode, raw))
+ self.assertFalse(result["originalSceneRestored"])
+ self.assertFalse(result["canonicalPassClaimed"])
+ self.assertEqual(retained.run(request, node)["baseline"], before["baseline"])
+ self.assertFalse(retained.run(change, node)["mutated"])
+
+ def root_stats(self):
+ # Root UID checks are meaningful in production. Only rewrite st_uid in
+ # returned host stat objects; preserve inode, mode and nanosecond times.
+ original_lstat, original_stat, original_fstat = Path.lstat, Path.stat, os.fstat
+ def mapped(value):
+ values = {name: getattr(value, name) for name in dir(value) if name.startswith("st_")}
+ values["st_uid"] = 0
+ return types.SimpleNamespace(**values)
+ from contextlib import ExitStack
+ stack = ExitStack()
+ stack.enter_context(patch.object(retained, "mounted_paths", return_value=[]))
+ stack.enter_context(patch.object(Path, "lstat", lambda path: mapped(original_lstat(path))))
+ stack.enter_context(patch.object(Path, "stat", lambda path, **kw: mapped(original_stat(path, **kw))))
+ stack.enter_context(patch.object(os, "fstat", lambda fd: mapped(original_fstat(fd))))
+ return stack
+
+ def test_no_credentials_read_and_no_paths_or_marker_content_emitted(self):
+ with tempfile.TemporaryDirectory() as directory:
+ state, roots, node, request = self.scene(directory)
+ opened = []
+ original_open = os.open
+ def record(path, *args, **kwargs):
+ opened.append(str(path)); return original_open(path, *args, **kwargs)
+ with self.root_stats(), patch.object(os, "open", side_effect=record):
+ result = retained.run(request, node)
+ self.assertFalse(any(name.endswith("service.env") or name.endswith("credential") for name in opened))
+ serialized = json.dumps(result)
+ for private in ("credential-must-never-be-read", "private-iam", request["expectedOwner"]["bucket"], str(state)):
+ self.assertNotIn(private, serialized)
+
+ def test_drift_or_unsafe_paths_refuse_before_any_move(self):
+ for variant in ("owner", "binary", "gate", "symlink", "hardlink", "mount", "live", "content-drift", "collision", "nested-mount"):
+ with self.subTest(variant=variant), tempfile.TemporaryDirectory() as directory:
+ state, roots, node, request = self.scene(directory)
+ with self.root_stats():
+ baseline = retained.run(request, node)["baseline"]
+ if variant == "owner": (roots[1] / "owner.json").write_text("{}")
+ elif variant == "binary": (state / "rustfs").write_bytes(b"different")
+ elif variant == "gate": (state / "drive-gate.json").write_text("{}")
+ elif variant == "symlink": (roots[1] / "escape").symlink_to(state)
+ elif variant == "hardlink": os.link(roots[1] / "quarantine/original", roots[1] / "alias")
+ elif variant == "mount": node["mounts"] = lambda: [{**row, "uuid": "foreign"} for row in request["expectedOwner"]["mounts"]]
+ elif variant == "live": node["stopped"] = lambda owner: (_ for _ in ()).throw(ValueError("live"))
+ elif variant == "content-drift": (roots[1] / "quarantine/original").write_bytes(b"advanced-record")
+ elif variant == "nested-mount": retained.mounted_paths.return_value = [roots[1] / "data"]
+ elif variant == "collision":
+ target = retained.destinations(state, roots)[1][1]; target.mkdir(parents=True, mode=0o700)
+ with patch.object(retained, "rename_new") as move, self.assertRaises(ValueError):
+ retained.run({**request, "action": "retained-scene-isolate", "expectedBaseline": baseline}, node)
+ move.assert_not_called()
+ self.assertTrue(state.exists()); self.assertTrue(roots[1].exists())
+
+ def test_disappeared_retained_directory_cannot_be_reclassified_as_originally_missing(self):
+ with tempfile.TemporaryDirectory() as directory:
+ state, roots, node, request = self.scene(directory)
+ with self.root_stats(), patch.object(retained, "rename_new", side_effect=self.local_rename):
+ baseline = retained.run(request, node)["baseline"]
+ retained.run({**request, "action": "retained-scene-isolate", "expectedBaseline": baseline}, node)
+ shutil.rmtree(retained.destinations(state, roots)[1][1])
+ with self.assertRaisesRegex(ValueError, "durable isolation intent"):
+ retained.run(request, node)
+
+ def test_remote_adapter_loads_real_module(self):
+ def execute(*args, **kwargs):
+ source = kwargs["input"].rsplit("\nmain(", 1)[0]
+ scope = {"__name__": "__node_test__"}
+ exec(compile(source, "retained-node", "exec"), scope)
+ self.assertTrue(callable(scope["retained_scene_backend"].run))
+ self.assertTrue(callable(scope["mpu_scene_backend"].run))
+ return types.SimpleNamespace(returncode=0, stdout="{}", stderr="")
+ with tempfile.TemporaryDirectory() as directory, patch("scripts.validation_heal.healthy.subprocess.run", side_effect=execute):
+ Fleet(["vm000"], Path(directory), "gh-12345-1-h2").remote("vm000", "retained-scene-audit", expectedOwner={})
+
+ def test_mountinfo_decodes_escaped_paths_without_accepting_a_missing_inventory(self):
+ with patch.object(Path, "read_text", return_value=r"1 0 8:1 / /data/rustfs1/a\040b rw - xfs /dev/disk rw"):
+ self.assertEqual(retained.mounted_paths(), [Path("/data/rustfs1/a b")])
+ with patch.object(Path, "read_text", return_value="invalid"), self.assertRaises(ValueError):
+ retained.mounted_paths()
+
+ def test_linux_rename_explicitly_uses_noreplace_and_syncs_both_parents(self):
+ call = unittest.mock.Mock(return_value=0)
+ with patch.object(retained.ctypes, "CDLL", return_value=types.SimpleNamespace(renameat2=call)), \
+ patch.object(retained, "sync_directory") as sync:
+ retained.rename_new(Path("/original/run"), Path("/retained/run"))
+ call.assert_called_once_with(-100, b"/original/run", -100, b"/retained/run", 1)
+ self.assertEqual(sync.call_args_list, [unittest.mock.call(Path("/original")), unittest.mock.call(Path("/retained"))])
+
+ def mpu_partial_scene(self, directory):
+ state, roots, node, request = self.scene(directory)
+ shutil.copytree(roots[1], roots[0])
+ owner = request['expectedOwner'];owner['runtimeProfile'] = 'manual'
+ for folder in (*roots, state): (folder/'owner.json').write_text(json.dumps(owner))
+ snapshot = {'files':[], 'formats':[], 'pid':0, 'quiescent':True, 'binarySha256':owner['binarySha256']}
+ node.update(owned=lambda run:(state, roots, copy.deepcopy(owner)),
+ snapshot=lambda value:copy.deepcopy(snapshot), mpu_scene_backend=mpu_scene)
+ key_hash = hashlib.sha256(f"{owner['bucket']}/{mpu_scene.G05_KEY}".encode()).hexdigest()
+ for root in roots:
+ folder = root/'data/.rustfs.sys/multipart'/key_hash/'unknown-create-before-ACK'
+ folder.mkdir(parents=True)
+ (folder/'part.1').write_bytes(b'unknown MPU original bytes')
+ (root/'quarantine/G05-partial.original').write_bytes(b'original missing-shard backup')
+ raw = mpu_scene.run({'runId':request['runId'], 'action':'mpu-scene', 'bucket':owner['bucket'],
+ 'keys':[mpu_scene.G05_KEY], 'expectedBinarySha256':owner['binarySha256']}, node)
+ request.update(expectedOriginalSceneSha256=retained.sha({k:snapshot[k] for k in ('files','formats')}),
+ mpuKeys=[mpu_scene.G05_KEY], expectedMpuSceneSha256=raw['sceneSha256'])
+ return state, roots, node, request
+
+ def test_g05_unknown_partial_seed_isolates_original_mpu_inodes_without_reading_credentials(self):
+ with tempfile.TemporaryDirectory() as directory:
+ state, roots, node, request = self.mpu_partial_scene(directory)
+ original = {p:(p.stat().st_ino,p.read_bytes()) for root in roots
+ for p in root.rglob('*') if p.is_file()}
+ opened, real_open = [], os.open
+ def record(path, *args, **kwargs):
+ opened.append(str(path));return real_open(path, *args, **kwargs)
+ with self.root_stats(), patch.object(os,'open',side_effect=record), \
+ patch.object(retained,'rename_new',side_effect=self.local_rename):
+ before = retained.run(request,node)
+ self.assertTrue(all(row['contentFilesHashed']==3 for row in before['baseline']['roots']))
+ change = {**request,'action':'retained-scene-isolate','expectedBaseline':before['baseline']}
+ def interrupted(source, target):
+ self.local_rename(source, target)
+ if source == roots[0]: raise RuntimeError('lost response after first MPU root move')
+ with patch.object(retained,'rename_new',side_effect=interrupted),self.assertRaises(RuntimeError):
+ retained.run(change,node)
+ result = retained.run(change,node)
+ self.assertEqual(result['baseline'],before['baseline'])
+ self.assertEqual(result['locations'],['retained']*5)
+ self.assertTrue(result['activeNamespaceIsolated'])
+ self.assertFalse(result['originalSceneRestored'] or result['canonicalPassClaimed'])
+ _, targets = retained.destinations(state,roots,per_run_parent=True)
+ for old,(inode,raw) in original.items():
+ index = next(i for i,root in enumerate(roots) if old.is_relative_to(root))
+ new = targets[index]/old.relative_to(roots[index])
+ self.assertEqual((new.stat().st_ino,new.read_bytes()),(inode,raw))
+ self.assertFalse(any(p.endswith(('service.env','credential')) for p in opened))
+
+ def test_g05_unknown_mpu_scope_or_digest_drift_refuses_before_any_move(self):
+ for fault in ('scope','digest','missing-digest','changed-content','byte-cap'):
+ with self.subTest(fault=fault),tempfile.TemporaryDirectory() as directory:
+ state,roots,node,request = self.mpu_partial_scene(directory)
+ with self.root_stats():
+ before = retained.run(request,node)
+ if fault == 'scope': request['mpuKeys'] = ['objects-0255.bin']
+ elif fault == 'digest': request['expectedMpuSceneSha256'] = 'f'*64
+ elif fault == 'missing-digest': request.pop('expectedMpuSceneSha256')
+ elif fault == 'changed-content': next((roots[0]/'data/.rustfs.sys/multipart').rglob('part.1')).write_bytes(b'changed MPU')
+ with patch.object(retained,'rename_new') as move, \
+ patch.object(mpu_scene,'MAX_BYTES',1 if fault == 'byte-cap' else mpu_scene.MAX_BYTES), \
+ self.assertRaises(ValueError):
+ retained.run({**request,'action':'retained-scene-isolate','expectedBaseline':before['baseline']},node)
+ move.assert_not_called()
+
+
+class IncompleteFleetIsolationTests(unittest.TestCase):
+ def test_main_does_not_treat_an_archived_namespace_as_restored_or_already_clean(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory); candidate = root / "candidate.json"; candidate.write_text("{}")
+ plan = {"kind": "advanced-mrf-fixture", "nodes": ["vm000"], "runId": "gh-12345-1-h2"}
+ states = {"state": {"exists": False}, "roots": [{"exists": False}] * 4,
+ "retained": {"state": True, "roots": [False, True, True, True]}}
+ result = {"cleanup": "ISOLATED", "originalSceneRestored": False}
+ argv = ["recover", "--failed-run-id", "12345", "--artifact", str(root), "--candidate", str(candidate),
+ "--evidence", str(root / "evidence")]
+ with patch("sys.argv", argv), patch.object(recovery, "recovery_plan", return_value=[plan]), \
+ patch.object(Fleet, "remote", return_value=states) as remote, \
+ patch.object(recovery, "isolate_incomplete_scene", return_value=result) as isolate:
+ recovery.main()
+ self.assertEqual([call.args[1] for call in remote.call_args_list], ["namespace-state"])
+ isolate.assert_called_once()
+ summary = json.loads((root / "evidence/recovery-summary.json").read_text())
+ self.assertEqual(summary["cleanup"], "ISOLATED")
+ self.assertFalse(summary["canonicalPassClaimed"])
+
+ def test_all_preflights_precede_moves_and_original_failure_is_not_restored(self):
+ for failure in (None, "audit-vm003", "move-vm001"):
+ with self.subTest(failure=failure), tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ case = "heal.checkpoint-and-mrf-corruption"
+ folder = root / "artifact/run" / case / "evidence" / case / "DB/H2/mrf-checksum"
+ folder.mkdir(parents=True)
+ nodes = [f"vm00{i}" for i in range(4)]
+ plan = {"nodes": nodes, "runId": "gh-12345-1-h2", "bucket": "rv-heal-0123456789abcdef", "binarySha256": "b" * 64}
+ for index, node in enumerate(nodes):
+ (folder / f"{index:05d}-reserved.json").write_text(json.dumps({"node": node, "reserved": True,
+ "owner": {**{key: plan[key] for key in ("runId", "bucket", "binarySha256")}, "runtimeProfile": "mrf-crash"}}))
+ events = []
+ def remote(node, action, **kwargs):
+ events.append((node, action))
+ if (failure == "audit-" + node and action.endswith("audit")) or (failure == "move-" + node and action.endswith("isolate")):
+ raise ValueError("injected failure")
+ return {"baseline": {"roots": [None, {}, {}, {}]}, "inspectionOnly": action.endswith("audit"),
+ "mutated": action.endswith("isolate"), "credentialContentsRead": False,
+ "activeNamespaceIsolated": True, "originalSceneRestored": False, "originalEvidenceComplete": False,
+ "backupRestorationUsed": False, "canonicalPassClaimed": False}
+ fleet = types.SimpleNamespace(remote=remote, record=lambda label, value: value)
+ if failure:
+ with self.assertRaises(ValueError): recovery.isolate_incomplete_scene(fleet, plan, root)
+ else:
+ result = recovery.isolate_incomplete_scene(fleet, plan, root)
+ self.assertEqual(result["cleanup"], "ISOLATED")
+ self.assertFalse(result["originalSceneRestored"])
+ self.assertFalse(result["originalVerdictRewritten"])
+ self.assertEqual(events[:4], [(node, "retained-scene-audit") for node in nodes])
+ if failure == "audit-vm003": self.assertFalse(any(action.endswith("isolate") for _, action in events))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test_validation_heal_schema.py b/test_validation_heal_schema.py
new file mode 100644
index 00000000..43c4526d
--- /dev/null
+++ b/test_validation_heal_schema.py
@@ -0,0 +1,183 @@
+import base64
+import copy
+import hashlib
+import json
+from pathlib import Path
+import sys
+import tempfile
+import types
+import unittest
+from unittest.mock import patch
+
+from scripts.validation_heal import schema, schema_faults, heal_fault, fleet_node
+from scripts.validation_heal.cursor import verify_all_parts, verify_part_manifest
+from scripts.validation_heal.matrix import AUTOMATIC_CASES, select
+
+
+class SchemaRefusalTests(unittest.TestCase):
+ def format(self, foreign=False, width=4):
+ prefix = "4" if foreign else "3"
+ members = [f"{prefix}2345678-1234-4234-8234-{index:012x}" for index in range(width)]
+ return {"version": "1", "format": "xl", "id": ("2" if foreign else "1") + "2345678-1234-4234-8234-123456789abc",
+ "xl": {"version": "3", "this": members[0], "sets": [members], "distributionAlgo": "SIPMOD+PARITY"}}
+
+ def donor(self):
+ raw = b" \n" + json.dumps(self.format(True), indent=2).encode() + b"\n"
+ return {"readOnly": True, "binarySha256": "candidate", "rawBase64": base64.b64encode(raw).decode(),
+ "sha256": hashlib.sha256(raw).hexdigest(), "value": self.format(True)}
+
+ def test_foreign_disk_uses_actual_donor_uuid_and_keeps_original_deployment_and_set(self):
+ original = self.format(width=16)
+ raw, changed, detail = schema_faults.derive(json.dumps(original).encode(), "foreign-disk", self.donor())
+ self.assertEqual(json.loads(raw), changed)
+ self.assertEqual(changed["xl"]["this"], self.donor()["value"]["xl"]["this"])
+ changed["xl"]["this"] = original["xl"]["this"]
+ self.assertEqual(changed, original)
+ self.assertTrue(detail["derivedIdentityFixture"])
+
+ def test_foreign_set_copies_exact_donor_bytes_even_when_topology_width_differs(self):
+ donor = self.donor()
+ raw, changed, detail = schema_faults.derive(json.dumps(self.format(width=16)).encode(), "foreign-set", donor)
+ self.assertEqual(raw, base64.b64decode(donor["rawBase64"]))
+ self.assertEqual(changed, donor["value"])
+ self.assertFalse(detail["derivedIdentityFixture"])
+ self.assertEqual(len(changed["xl"]["sets"][0]), 4)
+
+ def test_future_format_changes_only_reviewed_semantic_version(self):
+ original = self.format()
+ raw, changed, detail = schema_faults.derive(json.dumps(original).encode(), "future-format")
+ self.assertEqual(json.loads(raw)["version"], "999")
+ self.assertTrue(detail["otherSemanticFieldsUnchanged"])
+ changed["version"] = "1"
+ self.assertEqual(changed, original)
+
+ def test_foreign_provenance_rejects_shared_ids_changed_bytes_and_unreviewed_schema(self):
+ for kind in ("sha", "value", "read-only", "shared-deployment", "shared-member", "schema", "geometry"):
+ donor = self.donor()
+ if kind == "sha": donor["sha256"] = "0" * 64
+ elif kind == "value": donor["value"]["id"] = self.format()["id"]
+ elif kind == "read-only": donor["readOnly"] = False
+ else:
+ value = donor["value"]
+ if kind == "shared-deployment": value["id"] = self.format()["id"]
+ if kind == "shared-member": value["xl"]["sets"][0][1] = self.format()["xl"]["sets"][0][1]
+ if kind == "schema": value["xl"]["version"] = "4"
+ if kind == "geometry": value["xl"]["sets"][0].pop()
+ raw = json.dumps(value).encode()
+ donor.update(rawBase64=base64.b64encode(raw).decode(), sha256=hashlib.sha256(raw).hexdigest())
+ with self.subTest(kind=kind), self.assertRaises(ValueError):
+ schema_faults.derive(json.dumps(self.format()).encode(), "foreign-set", donor)
+
+ def test_future_object_schema_changes_exact_major_and_preserves_every_other_byte(self):
+ original = b"XL2 \x01\x00\x03\x00original-msgpack-and-payload"
+ changed = heal_fault.mutation(original, "future-xl-schema")
+ self.assertEqual(changed, original[:4] + b"\x02\x00" + original[6:])
+ for value, offset in ((b"XL2 \x02\x00\x03\x00", None), (b"bad!\x01\x00\x03\x00", None), (original, 5), (b"XL2 ", None)):
+ with self.subTest(value=value, offset=offset), self.assertRaises(ValueError):
+ heal_fault.mutation(value, "future-xl-schema", offset)
+ with self.assertRaisesRegex(ValueError, "exact object metadata"):
+ heal_fault.apply_fault(0, {"bucketPrefix": "bucket"}, "bucket/key/data/part.1", "C09-future-0", "future-xl-schema", "a" * 64)
+
+ @unittest.skipUnless(sys.platform == "linux", "actual Linux FD/mount guards run in CI")
+ def test_format_install_uses_one_empty_root_preserves_archive_and_rejects_other_disk(self):
+ with tempfile.TemporaryDirectory() as folder:
+ root = Path(folder)
+ (root / "data").mkdir()
+ saved = root / "quarantine/old-disk/.rustfs.sys"
+ saved.mkdir(parents=True)
+ original = json.dumps(self.format()).encode()
+ (saved / "format.json").write_bytes(original)
+ replacement = {"disk": 0, "originalArchiveUnchanged": True, "originalStillAtDataRoot": False,
+ "originalFormatSha256": hashlib.sha256(original).hexdigest()}
+ owner = {"runId": "run", "binarySha256": "candidate"}
+ with heal_fault.directory(root) as fd:
+ for kind in ("wrong-disk", "occupied", "wrong-candidate", "archive"):
+ request = {"disk": 0, "branch": "foreign-set", "donor": self.donor()}
+ current = copy.deepcopy(replacement)
+ if kind == "wrong-disk": request["disk"] = 1
+ if kind == "occupied": (root / "data/occupied").write_bytes(b"live")
+ if kind == "wrong-candidate": request["donor"]["binarySha256"] = "other"
+ if kind == "archive": current["originalArchiveUnchanged"] = False
+ with self.subTest(kind=kind), self.assertRaises(ValueError): schema_faults.install(fd, current, owner, request, heal_fault)
+ (root / "data/occupied").unlink(missing_ok=True)
+ proof = schema_faults.install(fd, replacement, owner, {"disk": 0, "branch": "future-format"}, heal_fault)
+ self.assertEqual((saved / "format.json").read_bytes(), original)
+ self.assertEqual(hashlib.sha256((root / "data/.rustfs.sys/format.json").read_bytes()).hexdigest(), proof["injectedSha256"])
+ self.assertFalse(proof["sharedFormatTouched"])
+ self.assertFalse(proof["credentialsRead"])
+
+ def scene(self):
+ before = {"node": {"formats": [{"disk": i, "sha256": f"format-{i}"} for i in range(4)], "files": []}}
+ after = copy.deepcopy(before)
+ targets, receipts = [], []
+ for i in range(4):
+ relative = "bucket/key/xl.meta"
+ original = {"disk": i, "path": relative, "sha256": f"original-{i}", "size": 91,
+ "metadata": {"physicalBitrotVerified": False}}
+ before["node"]["files"].extend([original, {"disk": i, "path": "bucket/key/data/part.1", "sha256": f"part-{i}", "size": 100}])
+ changed = {"sha256": f"future-{i}", "size": 91}
+ after["node"]["files"].extend([{**original, **changed, "metadataError": {"exactWitnessedFault": True,
+ "validMetadataClaimed": False, "message": "xl file version err"}}, copy.deepcopy(before["node"]["files"][-1])])
+ del after["node"]["files"][-2]["metadata"]
+ targets.append({"node": "node", "disk": i, "relativePath": relative})
+ receipts.append({"kind": "future-xl-schema", "relativePath": relative,
+ "before": {"sha256": original["sha256"], "size": 91}, "fault": changed})
+ return before, after, targets, receipts
+
+ def test_negative_scene_cannot_hide_unknown_record_rewrite_absence_or_payload_mutation(self):
+ before, after, targets, receipts = self.scene()
+ self.assertEqual(schema.metadata_scene(before, after, targets, receipts)["unknownRecordsRetainedExactly"], 4)
+ for kind in ("absent", "rewrite", "valid", "control", "format", "parser", "duplicate", "length"):
+ bad, bad_receipts = copy.deepcopy(after), copy.deepcopy(receipts)
+ if kind == "absent": bad["node"]["files"].pop(0)
+ if kind == "rewrite": bad["node"]["files"][0]["sha256"] = "rewritten"
+ if kind == "valid": bad["node"]["files"][0]["metadata"] = {}
+ if kind == "control": bad["node"]["files"][1]["sha256"] = "changed-payload"
+ if kind == "format": bad["node"]["formats"][0]["sha256"] = "changed"
+ if kind == "parser": bad["node"]["files"][0]["metadataError"]["message"] = "other corrupt metadata"
+ if kind == "duplicate": bad["node"]["files"].append(bad["node"]["files"][0])
+ if kind == "length": bad_receipts[0]["fault"]["size"] += 1
+ with self.subTest(kind=kind), self.assertRaises(ValueError): schema.metadata_scene(before, bad, targets, bad_receipts)
+
+ def test_per_disk_fault_hash_does_not_accept_another_drives_metadata(self):
+ entry = {"disk": 1, "path": "bucket/key/xl.meta", "sha256": "second", "size": 8}
+ faults = {"0:bucket/key/xl.meta": {"sha256": "first", "size": 8}, "1:bucket/key/xl.meta": {"sha256": "second", "size": 8}}
+ with patch.object(fleet_node, "command", return_value=types.SimpleNamespace(returncode=1, stderr="xl file version err")):
+ observed = fleet_node.inspect_metadata(Path("tool"), Path("path"), "bucket", "key", entry, faults)
+ self.assertFalse(observed["metadataError"]["validMetadataClaimed"])
+ entry["sha256"] = "first"
+ with self.assertRaises(ValueError): fleet_node.inspect_metadata(Path("tool"), Path("path"), "bucket", "key", entry, faults)
+
+ def test_unknown_get_requires_explicit_server_refusal_without_payload_or_absence(self):
+ observed = {"acknowledged": False, "resultUnknown": False,
+ "response": {"ResponseMetadata": {"HTTPStatusCode": 500}, "Error": {"Code": "InternalError"}}}
+ self.assertFalse(schema.unsupported_get(observed)["unknownTreatedAsMissing"])
+ for kind in ("transport", "payload", "absence", "success"):
+ bad = copy.deepcopy(observed)
+ if kind == "transport": bad["resultUnknown"] = True
+ if kind == "payload": bad["payload"] = {"sha256": "bytes"}
+ if kind == "absence": bad["response"] = {"ResponseMetadata": {"HTTPStatusCode": 404}, "Error": {"Code": "NoSuchKey"}}
+ if kind == "success": bad["acknowledged"] = True
+ with self.subTest(kind=kind), self.assertRaises(ValueError): schema.unsupported_get(bad)
+
+ def test_full_128_object_streaming_manifest_keeps_cursor_gap_requirement_separate(self):
+ cohorts = [{"cohort": [{"node": "node", "disk": i, "relativePath": f"bucket/key{n}/part.1",
+ "versionId": "null", "partNumber": 1, "sha256": f"hash-{n}-{i}", "size": 81} for i in range(4)]} for n in range(128)]
+ authority = {(item["disk"], item["relativePath"]): item for cohort in cohorts for item in cohort["cohort"]}
+ def remote(node, action, *, parts):
+ self.assertLessEqual(len(parts), 128)
+ return {"complete": True, "parts": [{**part, "sha256": authority[part["disk"], part["relativePath"]]["sha256"],
+ "physicalBitrotVerified": True, "physicalSize": 81} for part in parts]}
+ fleet = types.SimpleNamespace(nodes=["node"], remote=remote, record=lambda label, value: value)
+ self.assertEqual(verify_part_manifest(fleet, cohorts, "proof", count=128)["allPartsStreamingVerified"], 512)
+ with self.assertRaises(ValueError): verify_all_parts(fleet, cohorts, "proof", count=128)
+ with self.assertRaises(ValueError): verify_part_manifest(fleet, cohorts[:-1], "proof", count=128)
+
+ def test_preserves_four_source_branches_and_disables_unrelated_automatic_heal(self):
+ self.assertEqual(select("C09")[0]["variants"], list(schema.BRANCHES))
+ self.assertIn(schema.CASE_ID, AUTOMATIC_CASES)
+ self.assertEqual(fleet_node.runtime_config("schema-refusal")["RUSTFS_HEAL_AUTO_HEAL_ENABLE"], "false")
+ self.assertEqual(schema.CONTRACT["candidateCommit"], "072b57b37559fbe626aeda8abf61617fdcbdd926")
+
+
+if __name__ == "__main__": unittest.main()
diff --git a/test_validation_heal_scopes.py b/test_validation_heal_scopes.py
new file mode 100644
index 00000000..d55d42ce
--- /dev/null
+++ b/test_validation_heal_scopes.py
@@ -0,0 +1,187 @@
+import copy
+from pathlib import Path
+import tempfile
+import unittest
+import uuid
+from unittest.mock import patch
+
+from scripts.validation_heal import scopes
+from scripts.validation_heal.healthy import validate_topology, Fleet
+from scripts.validation_heal.fleet_node import runtime_config
+from scripts.validation_heal.shards import map_target, files
+from scripts.validation_heal.matrix import select, AUTOMATIC_CASES
+
+
+class ScopeTests(unittest.TestCase):
+ primary, secondary = "rv-heal-0123456789abcdef", "rv-heal-0123456789abcdef-other"
+
+ def snapshot(self):
+ sets = [[f"disk-{group}-{slot}" for slot in range(8)] for group in range(2)]
+ snapshot = {f"node{node + 1}": {"formats": [], "files": []} for node in range(4)}
+ objects = [(self.primary, "a/target.bin", 0), (self.primary, "a/child/neighbor.bin", 1),
+ (self.primary, "ab/control.bin", 1), (self.primary, "b/control.bin", 0),
+ (self.secondary, "a/target.bin", 1)]
+ for node, observation in enumerate(snapshot.values()):
+ for disk in range(4):
+ group, slot = disk // 2, (disk % 2) * 4 + node
+ observation["formats"].append({"disk": disk, "value": {"format": "xl", "id": "pool",
+ "xl": {"sets": sets, "this": sets[group][slot]}}})
+ for index, (bucket, key, selected_set) in enumerate(objects):
+ if selected_set != group: continue
+ data_dir = str(uuid.UUID(int=index + 1))
+ metadata = {"bucket": bucket, "key": key, "versions": [{"versionId": None, "isLatest": True,
+ "deleted": False, "dataDir": data_dir, "size": scopes.SIZE, "inline": None, "usesLegacyChecksum": False,
+ "role": "data" if slot < 4 else "parity", "parts": [{"number": 1, "size": scopes.SIZE, "actualSize": scopes.SIZE}],
+ "erasure": {"index": slot + 1, "data_blocks": 4, "parity_blocks": 4, "block_size": 1048576,
+ "distribution": list(range(1, 9)), "algorithm": "rs-vandermonde"}}]}
+ observation["files"].extend([
+ {"disk": disk, "path": f"{bucket}/{key}/xl.meta", "size": 600, "sha256": f"meta-{index}-{slot}", "metadata": metadata},
+ {"disk": disk, "path": f"{bucket}/{key}/{data_dir}/part.1", "size": 400, "sha256": f"part-{index}-{slot}"}])
+ return snapshot
+
+ def expected(self):
+ return {self.primary: [{"key": key} for key in ("a/target.bin", "a/child/neighbor.bin", "ab/control.bin", "b/control.bin")],
+ self.secondary: [{"key": "a/target.bin"}]}
+
+ def mapping(self):
+ return scopes.object_mapping(self.snapshot(), self.expected(), "H2")
+
+ def receipt(self, branch):
+ mapped = self.mapping()
+ selected = scopes.selection(branch, self.primary, mapped)
+ _, options = scopes.request_spec(branch, self.primary)
+ objects = [{"identity": {"kind": "object", "bucket": bucket, "object": key, "versionId": "null",
+ "poolIndex": options["pool"], "setIndex": options["set"], "bucketIncarnationId": "incarnation-" + bucket},
+ "disposition": {"state": "repaired"}} for bucket, key in sorted(selected)]
+ counts = {"processed": len(selected), "healed": len(selected), "unchanged": 0,
+ "failed": 0, "skipped": 0, "unknown": 0, "attemptFailures": 0, "overflowed": False}
+ current = {"summary": "finished", "outcome": {"execution": {"state": "completed"}, "coverage": "complete",
+ "objectsTruncated": False, "objects": objects, "counters": counts}}
+ return current, mapped, selected, options
+
+ def test_five_source_branches_and_real_two_set_runtime_are_retained(self):
+ self.assertEqual(select(scopes.CASE_ID)[0]["variants"], list(scopes.BRANCHES))
+ self.assertIn(scopes.CASE_ID, AUTOMATIC_CASES)
+ config = runtime_config("scope-isolation")
+ self.assertEqual(config["RUSTFS_ERASURE_SET_DRIVE_COUNT"], "8")
+ self.assertEqual(config["RUSTFS_SCANNER_ENABLED"], "false")
+ self.assertEqual(config["RUSTFS_HEAL_AUTO_HEAL_ENABLE"], "false")
+ self.assertEqual(config["RUSTFS_HEAL_MRF_ENABLE"], "false")
+
+ def test_existing_single_set_oracle_rejects_a_multi_set_baseline_without_explicit_binding(self):
+ snapshot = self.snapshot()
+ with self.assertRaises(ValueError): validate_topology(snapshot, "H2")
+ with self.assertRaises(ValueError): map_target(snapshot, self.primary, "a/target.bin", "data", 1)
+ proof = validate_topology(snapshot, "H2", expected_sets=2)
+ self.assertEqual(proof["setCount"], 2)
+ self.assertEqual(len(proof["diskUUIDs"]), 16)
+ for group in proof["actualSets"]: self.assertEqual(len(group), 8)
+
+ def test_every_set_must_span_all_four_original_hosts(self):
+ snapshot = self.snapshot()
+ records = [record for observation in snapshot.values() for record in observation["formats"]]
+ sets = [[record["value"]["xl"]["this"] for record in records[:8]],
+ [record["value"]["xl"]["this"] for record in records[8:]]]
+ for record in records: record["value"]["xl"]["sets"] = sets
+ with self.assertRaisesRegex(ValueError, "span every"): validate_topology(snapshot, "H2", expected_sets=2)
+
+ def test_mapping_uses_actual_members_and_role_distribution_without_projecting_format_values(self):
+ snapshot = self.snapshot()
+ before = copy.deepcopy(snapshot)
+ mapped = scopes.object_mapping(snapshot, self.expected(), "H2")
+ self.assertEqual(snapshot, before)
+ self.assertEqual(mapped[(self.primary, "a/target.bin")]["setIndex"], 0)
+ other = mapped[(self.primary, "ab/control.bin")]
+ self.assertEqual(other["setIndex"], 1)
+ self.assertEqual({member["node"] for member in other["cohort"]}, set(snapshot))
+ self.assertEqual(len(other["cohort"]), 8)
+ self.assertTrue(all(member["setSlot"] + 1 == member["index"] for member in other["cohort"]))
+ self.assertEqual(len(scopes.fault_targets(self.primary, self.secondary, mapped, "H2")), 5)
+ with self.assertRaises(ValueError): map_target(snapshot, self.primary, "ab/control.bin", "data", 1, multi_set=True, set_index=0)
+
+ def test_orphan_payload_on_an_unselected_set_is_not_a_complete_baseline(self):
+ snapshot = self.snapshot()
+ part = next(entry for entry in snapshot["node1"]["files"] if "a/target.bin/" in entry["path"] and entry["path"].endswith("part.1"))
+ snapshot["node1"]["files"].append({**part, "disk": 2})
+ with self.assertRaisesRegex(ValueError, "selected and unselected"):
+ scopes.object_mapping(snapshot, self.expected(), "H2")
+
+ def test_each_selector_uses_exact_bucket_key_prefix_and_physical_set(self):
+ mapped = self.mapping()
+ self.assertEqual(len(scopes.selection("root", self.primary, mapped)), 5)
+ self.assertEqual(len(scopes.selection("bucket", self.primary, mapped)), 4)
+ prefix = scopes.selection("prefix", self.primary, mapped)
+ self.assertEqual(prefix, {(self.primary, "a/target.bin"), (self.primary, "a/child/neighbor.bin")})
+ self.assertEqual(scopes.selection("object", self.primary, mapped), {(self.primary, "a/target.bin")})
+ self.assertEqual(scopes.selection("pool-set", self.primary, mapped), {(self.primary, "a/target.bin"), (self.primary, "b/control.bin")})
+ self.assertFalse(scopes.request_spec("object", self.primary)[1]["recursive"])
+ self.assertIsNone(scopes.request_spec("root", self.primary)[1]["pool"])
+ self.assertEqual(scopes.request_spec("pool-set", self.primary)[1]["set"], 0)
+
+ def test_nullable_unrestricted_receipts_keep_raw_fields_and_independent_physical_bindings(self):
+ current, mapped, selected, options = self.receipt("root")
+ original = copy.deepcopy(current)
+ proof = scopes.responsibility(current, mapped, selected, selected, options)
+ self.assertEqual(current, original)
+ self.assertTrue(proof["rawIdentitiesNotRewritten"])
+ self.assertEqual({row["physicalSetIndex"] for row in proof["physicalScopeBindings"]}, {0, 1})
+ self.assertTrue(all(row["rawSelectorScope"] == [None, None] for row in proof["physicalScopeBindings"]))
+ item = current["outcome"]["objects"][0]["identity"]
+ item.update(poolIndex=0, setIndex=mapped[(item["bucket"], item["object"])]["setIndex"])
+ scopes.responsibility(current, mapped, selected, selected, options)
+
+ def test_explicit_scope_nulls_wrong_sets_extra_objects_unknowns_and_false_counts_fail(self):
+ for kind in ("null", "set", "bool", "missing", "duplicate", "kind", "version", "incarnation", "unknown", "counter", "coverage"):
+ with self.subTest(kind=kind):
+ current, mapped, selected, options = self.receipt("pool-set")
+ item = current["outcome"]["objects"][0]
+ if kind == "null": item["identity"].update(poolIndex=None, setIndex=None)
+ elif kind == "set": item["identity"]["setIndex"] = 1
+ elif kind == "bool": item["identity"]["setIndex"] = False
+ elif kind == "missing": current["outcome"]["objects"].pop()
+ elif kind == "duplicate": current["outcome"]["objects"][-1] = copy.deepcopy(item)
+ elif kind in {"kind", "version", "incarnation"}:
+ item["identity"][{"kind": "kind", "version": "versionId", "incarnation": "bucketIncarnationId"}[kind]] = "" if kind == "incarnation" else "foreign"
+ elif kind == "unknown": item["disposition"]["state"] = "unknown"
+ elif kind == "counter": current["outcome"]["counters"]["healed"] += 1
+ else: current["outcome"]["coverage"] = "partial"
+ with self.assertRaises(ValueError): scopes.responsibility(current, mapped, selected, selected, options)
+ current, mapped, selected, options = self.receipt("root")
+ current["outcome"]["objects"][0]["identity"].update(poolIndex=None, setIndex=0)
+ with self.assertRaises(ValueError): scopes.responsibility(current, mapped, selected, selected, options)
+
+ def test_outside_control_faults_must_still_be_missing_before_backup_rescue(self):
+ original = self.snapshot()
+ mapped = self.mapping()
+ targets = list(mapped.values())
+ selected = scopes.selection("prefix", self.primary, mapped)
+ after = copy.deepcopy(original)
+ removed = {(target["node"], target["disk"], target["relativePath"]) for target in targets if (target["bucket"], target["key"]) not in selected}
+ for name, observation in after.items():
+ observation["files"] = [entry for entry in observation["files"] if (name, entry["disk"], entry["path"]) not in removed]
+ proof = scopes.physical_selection(original, after, targets, selected)
+ self.assertEqual(proof["selectedReconstructedParts"], 2)
+ self.assertEqual(proof["unselectedStillMissingParts"], 3)
+ self.assertTrue(proof["beforeAnyTargetGETOrBackupRescue"])
+ wrong = copy.deepcopy(after)
+ identity = next(iter(removed))
+ wrong[identity[0]]["files"].append(files(original)[identity])
+ with self.assertRaisesRegex(ValueError, "unselected control was repaired"):
+ scopes.physical_selection(original, wrong, targets, selected)
+ bad = copy.deepcopy(after)
+ metadata = next(entry for entry in bad["node1"]["files"] if "ab/control.bin/xl.meta" in entry["path"])
+ metadata["sha256"] = "changed"
+ with self.assertRaises(ValueError): scopes.physical_selection(original, bad, targets, selected)
+
+ def test_scope_adapter_uploads_a_compilable_complete_node_program(self):
+ import types
+ def invoke(argv, **params):
+ compile(params["input"], "remote-scoped-node", "exec")
+ return types.SimpleNamespace(returncode=0, stdout='{"checked":true}', stderr="")
+ with tempfile.TemporaryDirectory() as name, patch("scripts.validation_heal.healthy.subprocess.run", side_effect=invoke):
+ fleet = Fleet(["n0", "n1", "n2", "n3"], Path(name), "gh-1234-1-h2", runtime_profile="scope-isolation")
+ self.assertTrue(fleet.remote("n0", "process-metrics")["checked"])
+ self.assertTrue(fleet.remote("n0", "start")["checked"])
+
+
+if __name__ == "__main__": unittest.main()
diff --git a/test_validation_heal_shard_boundaries.py b/test_validation_heal_shard_boundaries.py
new file mode 100644
index 00000000..77c06099
--- /dev/null
+++ b/test_validation_heal_shard_boundaries.py
@@ -0,0 +1,155 @@
+"""Default-class C02/C03 boundaries retain the full original DB and strict witnesses."""
+
+from contextlib import ExitStack
+import copy
+import json
+import hashlib
+from types import SimpleNamespace
+import tempfile
+import unittest
+from pathlib import Path
+from unittest.mock import patch
+
+import test_validation_heal as physical_fixtures
+import test_validation_heal_network_fixtures as contract_fixtures
+from scripts.validation_heal import shard_boundaries as controls, shards
+
+
+class ShardBoundaryControlsTest(unittest.TestCase):
+ def inputs(self, data=2):
+ manifest, candidate, target, processes = contract_fixtures.NetworkBoundaryFixturesTest().parameters()
+ target['logical']['erasure'].update(data_blocks=data, parity_blocks=data, distribution=list(range(1, data * 2 + 1)))
+ for value in processes.values():
+ value['storageClassEnvironment']['RUSTFS_STORAGE_CLASS_STANDARD'] = None
+ original = [{'key': f'objects/{i:04d}.bin'} for i in range(128)]
+ recorder = SimpleNamespace(bytes_written=100 * 1024**2,
+ probe=SimpleNamespace(binding={'maxInventoryBytes': 512 * 1024**2, 'maxInventoryObjects': 256}))
+ return manifest, candidate, target, processes, recorder, original
+
+ def test_actual_default_class_geometry_preserves128_and_all_required_boundaries(self):
+ for data, inline in ((2, 262144), (6, 262146)):
+ with self.subTest(data=data):
+ plan = controls.parameters(*self.inputs(data))
+ self.assertEqual(plan['actualLogicalInlineLimit'], inline)
+ self.assertEqual(plan['groups']['inline-minus-exact-plus'], [inline - 1, inline, inline + 1])
+ self.assertEqual(plan['groups']['stripe-minus-exact-plus'], [1048575, 1048576, 1048577])
+ self.assertEqual(plan['groups']['multipart-minimum-minus-exact-plus'], [5242879, 5242880, 5242881])
+ self.assertEqual(plan['originalDefaultDBCount'], 128)
+ self.assertFalse(plan['faultBranchMatrixChanged'])
+ self.assertFalse(plan['completedMultipartProtocolClaimed'])
+
+ def test_source_configuration_original_controls_and_resource_refusals_precede_writes(self):
+ for kind in ('unknown-source', 'source-hash', 'class', 'inline-override', 'missing-original', 'bytes', 'count'):
+ values = self.inputs()
+ manifest, candidate, target, processes, recorder, original = values
+ if kind == 'unknown-source':
+ candidate['source_sha'] = 'f' * 40
+ manifest['sizeContract']['candidateCommit'] = candidate['source_sha']
+ elif kind == 'source-hash': manifest['sizeContract']['sourceFiles']['crates/ecstore/src/set_disk/mod.rs'] = 'f' * 64
+ elif kind == 'class': processes['node0']['storageClassEnvironment']['RUSTFS_STORAGE_CLASS_STANDARD'] = 'EC:8'
+ elif kind == 'inline-override': processes['node0']['storageClassEnvironment']['RUSTFS_STORAGE_CLASS_INLINE_BLOCK'] = '1MiB'
+ elif kind == 'missing-original': original.pop()
+ elif kind == 'bytes': recorder.probe.binding['maxInventoryBytes'] = recorder.bytes_written
+ else: recorder.probe.binding['maxInventoryObjects'] = 128
+ with self.subTest(kind=kind), self.assertRaises(ValueError): controls.parameters(*values)
+
+ def test_seed_is_public_put_with_independent_payloads_and_disjoint_keys(self):
+ plan = controls.parameters(*self.inputs())
+ calls = []
+ def call(operation, params, body):
+ calls.append((operation, params, body))
+ return {'ETag': 'ack'}
+ expected = controls.seed_controls(SimpleNamespace(call=call), 'owned', plan)
+ self.assertEqual(len(expected), plan['supplementalObjectCount'])
+ self.assertEqual(sum(len(body) for _, _, body in calls), plan['supplementalPayloadBytes'])
+ for entry, (operation, params, body) in zip(expected, calls):
+ self.assertEqual(operation, 'put_object')
+ self.assertEqual(params['Key'], entry['key'])
+ self.assertTrue(entry['key'].startswith('boundaries/'))
+ self.assertEqual(entry['payload'], {'size': len(body), 'sha256': hashlib.sha256(body).hexdigest()})
+
+ def scene(self):
+ original = physical_fixtures.HealShardOracleTest().snapshot()
+ bucket = 'rv-heal-0123456789abcdef'
+ size = 1048577
+ key = f'boundaries/0000-{size}.bin'
+ for row in original['node1']['files']:
+ row['path'] = row['path'].replace('objects/0008.bin', key)
+ if 'metadata' in row:
+ row['metadata']['key'] = key
+ version = row['metadata']['versions'][0]
+ version['size'] = size
+ version['parts'][0].update(size=size, actualSize=size)
+ plan = {'groups': {'stripe-plus': [size]}, 'supplementalObjectCount': 1,
+ 'actualLogicalInlineLimit': 262144, 'actualDataBlocks': 2, 'actualECBlockSize': 1048576}
+ expected = [{'key': key, 'versionId': 'null', 'isLatest': True, 'deleteMarker': False, 'payload': {'size': size}}]
+ return original, bucket, expected, plan
+
+ def test_real_decoded_full_set_mapping_rejects_missing_parts_changed_shapes_and_ack(self):
+ snapshot, bucket, expected, plan = self.scene()
+ targets = controls.map_controls(snapshot, bucket, expected, plan)
+ self.assertEqual((targets[0]['shape'], len(targets[0]['target']['cohort'])), ('external', 4))
+ for kind in ('missing-part', 'size', 'geometry', 'key', 'version', 'duplicate'):
+ value, ack, parameters = copy.deepcopy(snapshot), copy.deepcopy(expected), copy.deepcopy(plan)
+ if kind == 'missing-part': value['node1']['files'] = [row for row in value['node1']['files'] if not row['path'].endswith('part.1')]
+ elif kind == 'size': ack[0]['payload']['size'] -= 1
+ elif kind == 'geometry': parameters['actualDataBlocks'] = 6
+ elif kind == 'key': ack[0]['key'] += '-other'
+ elif kind == 'version': ack[0]['versionId'] = 'other'
+ else: ack += copy.deepcopy(ack)
+ with self.subTest(kind=kind), self.assertRaises(ValueError): controls.map_controls(value, bucket, ack, parameters)
+
+ def test_original_byte_proof_refuses_different_surviving_shard(self):
+ snapshot, bucket, expected, plan = self.scene()
+ targets = controls.map_controls(snapshot, bucket, expected, plan)
+ fleet = SimpleNamespace(record=lambda label, value: value)
+ def remote(node, action, **params):
+ shard = next(row for row in targets[0]['target']['cohort'] if row['disk'] == params['disk'])
+ return {'sha256': shard['sha256'], 'physicalSize': shard['size']}
+ fleet.remote = remote
+ self.assertTrue(controls.prove_controls(fleet, targets, 'proof')[0]['physical']['sameRoleOriginalShardBytesRestored'])
+ fleet.remote = lambda *args, **kwargs: {'sha256': 'changed', 'physicalSize': 82}
+ with self.assertRaises(ValueError): controls.prove_controls(fleet, targets, 'proof')
+
+
+ def test_real_c02_and_c03_callers_require_calibrated_controls_before_any_fault(self):
+ class ReachedControls(BaseException):
+ pass
+ for case in ("heal.data-shard-bitrot", "heal.parity-shard-bitrot"):
+ with self.subTest(case=case), tempfile.TemporaryDirectory() as directory, ExitStack() as stack:
+ root = Path(directory)
+ candidate = root / "candidate.json"
+ candidate.write_text(json.dumps({"source_sha": "1c7fc818dd858247c45b39542abd8291b1e6e31b", "package_sha256": "a" * 64}))
+ rc = root / "rc"
+ rc.write_bytes(b"client")
+ args = SimpleNamespace(evidence=root, candidate=candidate, rc_bin=rc, tool_directory=root,
+ testing_commit="b" * 40, binary_sha="c" * 64, case_id=case)
+ records = {}
+ def record(label, value):
+ records[label] = value
+ return value
+ fleet = SimpleNamespace(record=record, deploy=lambda *args: None,
+ snapshot=lambda label: {"calibration": label}, cleanup=lambda: True)
+ stack.enter_context(patch.dict('os.environ', {'GITHUB_RUN_ID': '123', 'GITHUB_RUN_ATTEMPT': '1'}))
+ stack.enter_context(patch.object(shards, 'Fleet', return_value=fleet))
+ stack.enter_context(patch.object(shards, 'Probe', return_value=None))
+ stack.enter_context(patch.object(shards, 'Recorder', return_value="recorder"))
+ stack.enter_context(patch.object(shards, 'verify_inspector', return_value="exact-manifest"))
+ stack.enter_context(patch.object(shards, 'validate_topology', return_value={}))
+ def seed(recorder, bucket, fixture, count, sizes, **kwargs):
+ return [{'key': f'objects/{i:04d}.bin'} for i in range(128)] if fixture == 'DB' else []
+ stack.enter_context(patch.object(shards, 'seed', side_effect=seed))
+ stack.enter_context(patch.object(shards, 'map_target', return_value="actual-target"))
+ plan = stack.enter_context(patch.object(controls, 'parameters', return_value="source-size-plan"))
+ seeded = stack.enter_context(patch.object(controls, 'seed_controls', side_effect=ReachedControls))
+ with self.assertRaises(ReachedControls): shards.run_topology(args, 'H1', ['vm000'])
+ self.assertEqual(plan.call_args.args[:3], ("exact-manifest", json.loads(candidate.read_text()), "actual-target"))
+ self.assertEqual(plan.call_args.args[3], {"calibration": "DB-size-calibration-live"})
+ self.assertEqual(plan.call_args.args[4], "recorder")
+ self.assertEqual(len(plan.call_args.args[5]), 128)
+ self.assertEqual(seeded.call_args.args[2], "source-size-plan")
+ self.assertEqual(records["DB-source-size-plan"], "source-size-plan")
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/test_validation_heal_status_keepalive.py b/test_validation_heal_status_keepalive.py
new file mode 100644
index 00000000..b9055602
--- /dev/null
+++ b/test_validation_heal_status_keepalive.py
@@ -0,0 +1,215 @@
+import http.server
+import copy
+import hashlib
+import json
+import os
+from pathlib import Path
+import socket
+import sys
+import threading
+import tempfile
+import time
+import unittest
+from unittest.mock import patch
+
+from datetime import datetime, timedelta, timezone
+from scripts.validation_heal.status_keepalive import KeepAliveStatusProbe
+from scripts.validation_heal.heal_probe import observation_phase, ADMIN_RESPONSE_LIMIT
+
+
+def binding():
+ return {"schemaVersion":1,"runId":"gh-12345-1-h1","leaseExpiresAt":(datetime.now(timezone.utc)+timedelta(hours=1)).isoformat(),
+ "jumpHostOnly":True,"exclusiveHealService":True,"candidateCommit":"a"*40,"harnessCommit":"b"*40,
+ "candidatePackageSha256":"c"*64,"candidateBinarySha256":"d"*64,"rcBinarySha256":"e"*64,
+ "bucketPrefix":"rv-heal-0123456789abcdef","defaultEndpoint":"node1","endpoints":{"node1":"http://127.0.0.1:9400"},
+ "maxInventoryObjects":512,"maxInventoryBytes":512*1024**2}
+
+TOKEN = '00000000-0000-4000-8000-000000000001'
+
+
+class Handler(http.server.BaseHTTPRequestHandler):
+ protocol_version = 'HTTP/1.1'
+
+ def log_message(self, *args):
+ pass
+
+ def do_POST(self):
+ self.server.peers.append(self.client_address)
+ self.server.requests.append((self.path, dict(self.headers)))
+ self.rfile.read(int(self.headers.get('Content-Length', '0')))
+ if self.server.mode == 'disconnect':
+ self.connection.shutdown(socket.SHUT_RDWR)
+ self.connection.close()
+ return
+ body = json.dumps({'summary': 'running', 'clientToken': TOKEN}).encode()
+ if self.server.mode == 'oversize':
+ body = b'x' * (ADMIN_RESPONSE_LIMIT + 1)
+ status = 302 if self.server.mode == 'redirect' else 503 if self.server.mode == 'error' else 200
+ self.send_response(status)
+ self.send_header('Content-Length', str(len(body) + int(self.server.mode == 'truncated')))
+ if self.server.mode in ('truncated', 'close'):
+ self.send_header('Connection', 'close')
+ self.send_header('Location', 'http://not-followed.invalid/')
+ self.end_headers()
+ self.wfile.write(body)
+
+
+class KeepaliveTests(unittest.TestCase):
+ def setUp(self):
+ self.server = http.server.ThreadingHTTPServer(('127.0.0.1', 0), Handler)
+ self.server.peers = []; self.server.requests = []; self.server.mode = 'ok'
+ self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)
+ self.thread.start()
+ lab = binding(); lab['endpoints'][lab['defaultEndpoint']] = f'http://127.0.0.1:{self.server.server_port}'
+ self.probe = KeepAliveStatusProbe(lab)
+
+ def tearDown(self):
+ self.server.shutdown(); self.server.server_close(); self.thread.join()
+
+ def call(self, query=None, body=b''):
+ return self.probe.admin('/rustfs/admin/v3/heal/rv-heal-0123456789abcdef', [('clientToken', TOKEN)] if query is None else query,
+ body, auth='anonymous')
+
+ def test_real_socket_reused_and_full_evidence_retained(self):
+ with self.probe.status_session(TOKEN, "/rustfs/admin/v3/heal/rv-heal-0123456789abcdef") as state:
+ a = self.call(); b = self.call()
+ self.assertEqual(state['requests'], 2)
+ self.assertEqual(len(set(self.server.peers)), 1)
+ self.assertLess(a['callFinishedMonotonicNs'], b['callStartedMonotonicNs'])
+ self.assertEqual(a['clockDomain'], b['clockDomain'])
+ self.assertEqual(b['response']['summary'], 'running')
+ self.assertIn('Content-Length', b['responseHeaders'])
+ self.assertEqual((state['connectionAttempts'], state['connectionsOpened']), (1, 1))
+ first, second = state['observations']
+ self.assertTrue(first['transport']['newConnection'])
+ self.assertFalse(second['transport']['newConnection'])
+ self.assertEqual(first['transport']['connectionId'], second['transport']['connectionId'])
+ self.assertEqual(first['transport']['localAddress'], list(self.server.peers[0]))
+ self.assertEqual(first['transport']['peerAddress'], ['127.0.0.1', self.server.server_port])
+ self.assertIn('connectionEstablishment', first['phases'])
+ self.assertNotIn('connectionEstablishment', second['phases'])
+ for sample, measured in ((a, first), (b, second)):
+ self.assertEqual(sample['callStartedMonotonicNs'], measured['callStartedMonotonicNs'])
+ self.assertEqual(sample['callFinishedMonotonicNs'], measured['callFinishedMonotonicNs'])
+ for name in ('requestWrite', 'responseHeaders', 'responseBody', 'responseParsing'):
+ phase = measured['phases'][name]
+ self.assertTrue(phase['completed'])
+ self.assertLessEqual(sample['callStartedMonotonicNs'], phase['startedMonotonicNs'])
+ self.assertLessEqual(phase['finishedMonotonicNs'], sample['callFinishedMonotonicNs'])
+ self.assertIsNone(self.probe._status)
+
+ def test_successful_server_close_records_a_new_connection_for_the_next_request(self):
+ self.server.mode = 'close'
+ with self.probe.status_session(TOKEN, "/rustfs/admin/v3/heal/rv-heal-0123456789abcdef") as state:
+ self.call(); self.call()
+ self.assertEqual((state['requests'], state['connectionAttempts'], state['connectionsOpened']), (2, 2, 2))
+ first, second = [row['transport'] for row in state['observations']]
+ self.assertNotEqual(first['connectionId'], second['connectionId'])
+ self.assertTrue(first['newConnection'] and second['newConnection'])
+ self.assertTrue(first['serverWillClose'] and second['serverWillClose'])
+ self.assertFalse(state['poisoned'])
+
+ def test_connection_failure_is_retained_without_reconnect_or_false_write_receipt(self):
+ with self.probe.status_session(TOKEN, "/rustfs/admin/v3/heal/rv-heal-0123456789abcdef") as state, \
+ patch('http.client.HTTPConnection.connect', side_effect=OSError('local connect failure')) as connect:
+ with self.assertRaises(OSError): self.call()
+ self.assertEqual((state['connectionAttempts'], state['connectionsOpened']), (1, 0))
+ measured = state['observations'][0]
+ self.assertFalse(measured['requestWriteCompleted'])
+ self.assertFalse(measured['responseObserved'])
+ self.assertFalse(measured['phases']['connectionEstablishment']['completed'])
+ self.assertEqual(measured['failureType'], 'OSError')
+ with self.assertRaisesRegex(ValueError, 'poisoned'): self.call()
+ connect.assert_called_once()
+ self.assertEqual(state['failureType'], 'OSError')
+ self.assertEqual(measured['failureType'], 'OSError')
+ self.assertEqual(self.server.requests, [])
+
+ def test_observation_budget_fails_before_an_additional_request(self):
+ with patch('scripts.validation_heal.status_keepalive.MAX_STATUS_OBSERVATIONS', 2), self.probe.status_session(TOKEN, "/rustfs/admin/v3/heal/rv-heal-0123456789abcdef") as state:
+ self.call(); self.call()
+ with self.assertRaisesRegex(ValueError, 'budget exhausted'): self.call()
+ self.assertEqual(len(state['observations']), 2)
+ self.assertEqual(state['requests'], 2)
+ self.assertTrue(state['poisoned'])
+ self.assertEqual(len(self.server.requests), 2)
+
+ def test_one_observation_cannot_hide_two_http_requests(self):
+ with self.probe.status_session(TOKEN, "/rustfs/admin/v3/heal/rv-heal-0123456789abcdef") as state:
+ with self.assertRaisesRegex(ValueError, 'already invoked'):
+ with self.probe.status_observation():
+ self.call(); self.call()
+ self.assertEqual(len(state['observations']), 1)
+ self.assertEqual(state['requests'], 1)
+ self.assertTrue(state['poisoned'])
+ self.assertEqual(len(self.server.requests), 1)
+
+ def test_poll_measurement_rejects_foreign_clock_token_session_or_sequence(self):
+ with self.probe.status_session(TOKEN, "/rustfs/admin/v3/heal/rv-heal-0123456789abcdef"):
+ sample = self.call()
+ for field, value in [('clockDomain', 'foreign'), ('query', [('clientToken', 'foreign')]),
+ ('statusObservation', {'sessionId': 'foreign', 'sequence': 1}),
+ ('statusObservation', {**sample['statusObservation'], 'sequence': True}),
+ ('statusObservation', {**sample['statusObservation'], 'sequence': 2})]:
+ altered = copy.deepcopy(sample); altered[field] = value
+ with self.assertRaises(ValueError): self.probe.observation_for(altered)
+ with self.probe.status_session(TOKEN, "/rustfs/admin/v3/heal/rv-heal-0123456789abcdef"):
+ self.call()
+ with self.assertRaises(ValueError): self.probe.observation_for(sample)
+
+
+
+ def test_signed_headers_preserved(self):
+ with patch.dict('os.environ', HEAL_ACCESS_KEY='test-access', HEAL_SECRET_KEY='test-secret'):
+ with self.probe.status_session(TOKEN, "/rustfs/admin/v3/heal/rv-heal-0123456789abcdef"):
+ self.probe.admin('/rustfs/admin/v3/heal/rv-heal-0123456789abcdef', [('clientToken', TOKEN)], b'')
+ headers = {k.lower(): v for k, v in self.server.requests[0][1].items()}
+ self.assertIn('AWS4-HMAC-SHA256', headers['authorization'])
+ self.assertIn('x-amz-content-sha256', headers)
+
+ def test_control_or_different_token_not_sent(self):
+ for query, body in [([], b'{}'), ([('clientToken', TOKEN), ('forceStop', 'true')], b''),
+ ([('clientToken', 'different')], b''), ([('clientToken', TOKEN)] * 2, b'')]:
+ with self.probe.status_session(TOKEN, "/rustfs/admin/v3/heal/rv-heal-0123456789abcdef") as state:
+ with self.assertRaises(ValueError): self.call(query, body)
+ self.assertTrue(state['poisoned'])
+ self.assertEqual(self.server.requests, [])
+
+ def test_disconnect_redirect_and_oversize_poison_without_retry(self):
+ for mode in ('disconnect', 'redirect', 'oversize', 'truncated'):
+ self.server.mode = mode
+ before = len(self.server.requests)
+ with self.probe.status_session(TOKEN, "/rustfs/admin/v3/heal/rv-heal-0123456789abcdef") as state:
+ with self.assertRaises(Exception): self.call()
+ self.assertTrue(state['poisoned'])
+ with self.assertRaisesRegex(ValueError, 'poisoned'): self.call()
+ self.assertEqual(len(self.server.requests), before + 1)
+
+ def test_http_error_kept_as_observation_not_retried(self):
+ self.server.mode = 'error'
+ with self.probe.status_session(TOKEN, "/rustfs/admin/v3/heal/rv-heal-0123456789abcdef"):
+ self.assertEqual(self.call()['httpStatus'], 503)
+ self.assertEqual(len(self.server.requests), 1)
+
+ def test_other_thread_cannot_issue_a_status_request(self):
+ with self.probe.status_session(TOKEN, "/rustfs/admin/v3/heal/rv-heal-0123456789abcdef"):
+ with patch('scripts.validation_heal.status_keepalive.threading.get_ident', return_value=-1):
+ with self.assertRaisesRegex(ValueError, 'thread changed'): self.call()
+ self.assertEqual(self.server.requests, [])
+
+ def test_default_transport_unchanged_and_nested_session_rejected(self):
+ with patch('scripts.validation_heal.status_keepalive.Probe._open_admin', return_value='default') as original:
+ self.assertEqual(self.probe._open_admin(object()), 'default')
+ original.assert_called_once()
+ with self.probe.status_session(TOKEN, "/rustfs/admin/v3/heal/rv-heal-0123456789abcdef"):
+ with self.assertRaisesRegex(ValueError, 'nested'):
+ with self.probe.status_session(TOKEN, "/rustfs/admin/v3/heal/rv-heal-0123456789abcdef"): pass
+
+ def test_outside_status_session_has_no_timing_schema_or_sample_budget(self):
+ sample = self.call()
+ self.assertNotIn('statusObservation', sample)
+ self.assertIsNone(getattr(self.probe, '_status', None))
+ self.assertEqual(len(self.server.requests), 1)
+
+
+if __name__ == '__main__': unittest.main()
diff --git a/test_validation_heal_timeout_retention.py b/test_validation_heal_timeout_retention.py
new file mode 100644
index 00000000..e4b98769
--- /dev/null
+++ b/test_validation_heal_timeout_retention.py
@@ -0,0 +1,938 @@
+"""Original live I05 evidence selects preservation only, before any contender."""
+
+import hashlib
+import json
+import os
+from pathlib import Path
+import shutil
+import tempfile
+from types import SimpleNamespace
+import unittest
+from unittest.mock import patch
+import zipfile
+
+from scripts.validation_heal import timeout_retention as controller, retained_scene as retained
+from scripts.validation_heal import race_retention, recover_environment as recovery
+from scripts.validation_heal.heal_probe import canonical, payload
+from scripts.validation_heal.lock_timeout import CONTENDER_BYTES
+import test_validation_heal_race_retention as race_tests
+
+
+FIXTURE = Path(__file__).parent / 'testdata/validation/i05-pre-contender-39711742431.zip'
+DRAIN_FIXTURE = Path(__file__).parent / 'testdata/validation/i05-drain-budget-39746002648.zip'
+DESCRIPTOR_FIXTURE = Path(__file__).parent / 'testdata/validation/i05-descriptor-drain-39754893467.zip'
+FILTER_STARTUP_FIXTURE = Path(__file__).parent / 'testdata/validation/i05-filter-startup-39761856012.zip'
+H2_FILTER_STARTUP_FIXTURE = Path(__file__).parent / 'testdata/validation/i05-h2-filter-startup-39764496504.zip'
+
+
+class TimeoutRetentionTests(unittest.TestCase):
+ def h2_startup_archive(self, directory):
+ root = Path(directory) / 'artifact'
+ archive_root = root / 'rustfs-validation-39764496504-1'
+ with zipfile.ZipFile(H2_FILTER_STARTUP_FIXTURE) as archive:
+ archive.extractall(archive_root)
+ return root, json.loads((archive_root / 'candidate.json').read_text()), archive_root
+
+ def test_exact_h2_startup_failure_retains_all_peers_after_actual_h1_cleanup(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root, candidate, archive = self.h2_startup_archive(directory)
+ before = {p: p.read_bytes() for p in archive.rglob('*') if p.is_file()}
+ with patch.object(recovery, 'Fleet') as fleet:
+ plan, = recovery.recovery_plan(root, 39764496504, candidate)
+ fleet.assert_not_called()
+ self.assertEqual(plan['runId'], 'gh-39764496504-1-h2')
+ self.assertEqual(plan['nodes'], ['vm000', 'vm001', 'vm002', 'vm003'])
+ self.assertEqual(plan['harnessCommit'], controller.H2_FILTER_STARTUP_HELPER)
+ self.assertEqual(plan['ackFilesSha256'], controller.H2_FILTER_STARTUP_ARCHIVE_SHA256)
+ race_retention.validate(plan)
+ self.assertEqual(before, {p: p.read_bytes() for p in archive.rglob('*') if p.is_file()})
+
+ def test_h2_startup_refuses_unreviewed_run_effects_cleanup_peers_and_archive_drift(self):
+ variants = ('run', 'candidate', 'helper', 'h1-cleanup', 'h1-result', 'scene',
+ 'missing-peer', 'workload', 'extra-case', 'format', 'symlink')
+ for variant in variants:
+ with self.subTest(variant=variant), tempfile.TemporaryDirectory() as directory:
+ root, candidate, archive = self.h2_startup_archive(directory)
+ evidence = archive / 'run' / controller.CASE / 'evidence' / controller.CASE
+ failed_run = 39764496504
+ if variant == 'run': failed_run += 1
+ elif variant == 'candidate': candidate['package_sha256'] = '0' * 64
+ elif variant == 'helper':
+ path = archive / 'run' / controller.CASE / 'cases' / (controller.CASE + '.json')
+ value = json.loads(path.read_text())
+ value['provenance']['testing_commit'] = controller.FILTER_STARTUP_HELPER
+ path.write_text(json.dumps(value))
+ elif variant in ('h1-cleanup', 'h1-result'):
+ name = '00040-cleanup.json' if variant == 'h1-cleanup' else '00042-diagnostic-result.json'
+ (evidence / 'H1' / name).write_text('{}')
+ elif variant == 'missing-peer': (evidence / 'H2/00009-reserved.json').unlink()
+ elif variant == 'workload':
+ (evidence / 'H2/fixture').mkdir()
+ (evidence / 'H2/fixture/000001-request.json').write_text('{}')
+ elif variant == 'extra-case':
+ (archive / 'run' / controller.CASE / 'cases/other.json').write_text('{}')
+ else:
+ path = evidence / 'H2/00014-original-failure-scene.json'
+ if variant == 'symlink':
+ copy = Path(directory) / 'scene.json'
+ copy.write_bytes(path.read_bytes())
+ path.unlink()
+ path.symlink_to(copy)
+ else:
+ value = json.loads(path.read_text())
+ if variant == 'scene': value['vm002']['files'].append({'path': 'unknown'})
+ else: value['vm002']['formats'][0]['sha256'] = '0' * 64
+ path.write_text(json.dumps(value))
+ with patch.object(recovery, 'Fleet') as fleet, self.assertRaises(ValueError):
+ recovery.recovery_plan(root, failed_run, candidate)
+ fleet.assert_not_called()
+
+ def startup_archive(self, directory):
+ root = Path(directory) / 'artifact'
+ archive_root = root / 'rustfs-validation-39761856012-1'
+ with zipfile.ZipFile(FILTER_STARTUP_FIXTURE) as archive:
+ archive.extractall(archive_root)
+ candidate = json.loads((archive_root / 'candidate.json').read_text())
+ return root, candidate, archive_root
+
+ def test_exact_filter_startup_failure_preserves_empty_stopped_scene_only(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root, candidate, archive = self.startup_archive(directory)
+ before = {p: p.read_bytes() for p in archive.rglob('*') if p.is_file()}
+ with patch.object(recovery, 'Fleet') as fleet:
+ plan, = recovery.recovery_plan(root, 39761856012, candidate)
+ fleet.assert_not_called()
+ self.assertEqual(plan['runId'], 'gh-39761856012-1-h1')
+ self.assertEqual(plan['kind'], 'unresolved-i05-timeout')
+ self.assertEqual(plan['harnessCommit'], controller.FILTER_STARTUP_HELPER)
+ self.assertEqual(plan['ackFilesSha256'], controller.FILTER_STARTUP_ARCHIVE_SHA256)
+ self.assertEqual(before, {p: p.read_bytes() for p in archive.rglob('*') if p.is_file()})
+
+ def test_startup_isolation_refuses_changed_or_additional_effects_before_fleet(self):
+ for variant in ('run', 'candidate', 'missing', 'changed', 'request', 'H2', 'symlink'):
+ with self.subTest(variant=variant), tempfile.TemporaryDirectory() as directory:
+ root, candidate, archive = self.startup_archive(directory)
+ folder = archive / 'run' / controller.CASE / 'evidence' / controller.CASE / 'H1'
+ failed_run = 39761856012
+ if variant == 'run': failed_run += 1
+ elif variant == 'candidate': candidate['package_sha256'] = '0' * 64
+ elif variant == 'missing': (folder / '00008-original-failure-scene.json').unlink()
+ elif variant == 'changed': (folder / '00010-diagnostic-result.json').write_text('{}')
+ elif variant == 'request':
+ (folder / 'fixture').mkdir()
+ (folder / 'fixture/000001-request.json').write_text('{}')
+ elif variant == 'H2': (folder.parent / 'H2').mkdir()
+ else:
+ target = folder / '00008-original-failure-scene.json'
+ copy = Path(directory) / 'scene.json'
+ copy.write_bytes(target.read_bytes())
+ target.unlink()
+ target.symlink_to(copy)
+ with patch.object(recovery, 'Fleet') as fleet, self.assertRaises(ValueError):
+ recovery.recovery_plan(root, failed_run, candidate)
+ fleet.assert_not_called()
+
+ def archive(self, directory):
+ root = Path(directory) / 'artifact'
+ with zipfile.ZipFile(FIXTURE) as archive:
+ archive.extractall(root)
+ artifact = root / 'rustfs-validation-39711742431-1'
+ candidate = json.loads((artifact / 'candidate.json').read_text())
+ folder = artifact / 'run' / controller.CASE / 'evidence' / controller.CASE / 'H1'
+ return root, candidate, folder
+
+ def drain_archive(self, directory, run=39746002648):
+ root = Path(directory) / 'drain-artifact'
+ with zipfile.ZipFile(DRAIN_FIXTURE if run == 39746002648 else DESCRIPTOR_FIXTURE) as archive:
+ archive.extractall(root)
+ artifact = root / f'rustfs-validation-{run}-1'
+ candidate = json.loads((artifact / 'candidate.json').read_text())
+ folder = artifact / 'run' / controller.CASE / 'evidence' / controller.CASE / 'H1'
+ return root, candidate, folder
+
+ def add_contender_archive(self, root, candidate, folder):
+ def read(path): return json.loads(path.read_text())
+ def write(path, value): path.write_text(json.dumps(value, sort_keys=True, separators=(',', ':')))
+
+ result_path = folder.parents[2] / 'cases' / f'{controller.CASE}.json'
+ result = read(result_path)
+ result['provenance']['testing_commit'] = controller.CONTENDER_HELPER
+ write(result_path, result)
+ binding_path = folder / '00001-binding.json'
+ binding = read(binding_path)
+ binding['harnessCommit'] = controller.CONTENDER_HELPER
+ write(binding_path, binding)
+ binding_hash = hashlib.sha256(canonical(binding).encode()).hexdigest()
+
+ summary_path = folder.parent / 'healthy-summary.json'
+ summary = read(summary_path)
+ row = dict(summary['diagnostics'][0])
+ row['failure'] = controller.FAILURES[controller.CONTENDER_HELPER]
+ summary['diagnostics'] = [row]
+ write(summary_path, summary)
+ diagnostic_path = folder / '00021-diagnostic-result.json'
+ diagnostic = read(diagnostic_path)
+ diagnostic['failure'] = controller.FAILURES[controller.CONTENDER_HELPER]
+ write(diagnostic_path, diagnostic)
+
+ renamed = (
+ ('00018-quiescent.json', '00022-quiescent.json'),
+ ('00019-original-failure-scene.json', '00023-original-failure-scene.json'),
+ ('00020-original-failure-logs.json', '00024-original-failure-logs.json'),
+ ('00021-diagnostic-result.json', '00025-diagnostic-result.json'),
+ )
+ for old, new in renamed: (folder / old).rename(folder / new)
+
+ first = read(folder / '00017-I05-caller-window.json')
+ first_after = first['afterCursor']
+ middle_after = first_after + '-middle'
+ last_after = middle_after + '-last'
+ for name, before, after in (
+ ('00019-I05-caller-window.json', first_after, middle_after),
+ ('00021-I05-caller-window.json', middle_after, last_after)):
+ write(folder / name, {'node': 'vm000', 'unit': 'rustfs-validation-heal-' +
+ binding['runId'] + '.service', 'complete': True, 'beforeCursor': before,
+ 'afterCursor': after, 'hasMore': False, 'sourceMessageLoss': [], 'journal': ''})
+ write(folder / '00018-I05-caller-before-disconnect-socket.json', {'readOnly': True})
+ write(folder / '00020-I05-caller-admission-resources.json', {'readOnly': True})
+
+ scope_path = folder / 'timeout/scope.json'
+ scope = read(scope_path)
+ scope['bindingSha256'] = binding_hash
+ write(scope_path, scope)
+ run_id, bucket, key = scope['runId'], binding['bucketPrefix'], scope['entry']['key']
+ caller_path = folder / 'timeout/caller'
+ for name in ('request.json', 'request-written.json', 'disconnected.json'):
+ request_path = caller_path / 'transport' / name
+ caller_request = read(request_path)
+ caller_request['harnessCommit'] = controller.CONTENDER_HELPER
+ caller_request['bindingSha256'] = binding_hash
+ write(request_path, caller_request)
+ caller_request_bytes = (caller_path / 'transport/request.json').read_bytes()
+ for name in ('request-written.json', 'disconnected.json'):
+ request_path = caller_path / 'transport' / name
+ caller_request = read(request_path)
+ caller_request['requestSha256'] = hashlib.sha256(caller_request_bytes).hexdigest()
+ write(request_path, caller_request)
+ caller_request = read(caller_path / 'transport/request.json')
+ domain = caller_request['clockDomain']
+ contender = folder / 'timeout/contender'
+ contender.mkdir()
+ body = payload(f'{run_id}/I05-timeout-contender', CONTENDER_BYTES)
+ params = {'Bucket': bucket, 'Key': key, 'Metadata': {'heal-fixture': 'I05-timeout-contender'}}
+ request = {'schemaVersion': 2, 'runId': run_id, 'sequence': 1, 'operation': 'put_object',
+ 'params': params, 'identity': None, 'expectHttp': 503, 'clockDomain': domain,
+ 'preparedMonotonicNs': 1000000, 'preparedAt': '2026-10-22T00:00:00+00:00',
+ 'casePassClaimed': False,
+ 'payload': {'size': len(body), 'sha256': hashlib.sha256(body).hexdigest()}}
+ request_bytes = json.dumps(request, sort_keys=True, separators=(',', ':')).encode()
+ (contender / '000001-request.json').write_bytes(request_bytes)
+ request_id = '30000000-0000-4000-8000-000000000001'
+ response = {'Error': {'Code': 'ServiceUnavailable', 'Message': 'retry'},
+ 'ResponseMetadata': {'RequestId': request_id, 'HostId': '', 'HTTPStatusCode': 503,
+ 'HTTPHeaders': {'retry-after': '1', 'x-amz-request-id': request_id}, 'RetryAttempts': 0}}
+ ack = {'schemaVersion': 2, 'runId': run_id, 'sequence': 1, 'requestFile': '000001-request.json',
+ 'requestSha256': hashlib.sha256(request_bytes).hexdigest(), 'operationCheck': 'PASS',
+ 'casePassClaimed': False, 'clockDomain': domain,
+ 'collectorCallStartedMonotonicNs': 1001000, 'collectorCallFinishedMonotonicNs': 2001000,
+ 'recordedMonotonicNs': 2002000, 'observation': {'endpoint': 'node1', 'operation': 'put_object',
+ 'params': params, 'payload': request['payload'], 'clockDomain': domain,
+ 'clientCallInvoked': True, 'acknowledged': False, 'resultUnknown': False,
+ 'clientCleanupError': None, 'callStartedMonotonicNs': 1002000,
+ 'callFinishedMonotonicNs': 2000000, 'response': response}}
+ write(contender / '000001-ack.json', ack)
+
+ admission = {'node': 'vm000', 'attempt': {'request_id': '20000000-0000-4000-8000-000000000001',
+ 'lock_attempt_id': '20000000-0000-4000-8000-000000000002', 'owner': ':::9000',
+ 'namespace': 'pool-0', 'admitted': True, 'observedAcquiredWithoutDrop': True}}
+ write(caller_path / 'admission.json', admission)
+ rows = []
+ def lock_row(mode, op, obj, attempt, state, **extra):
+ return {'target': 'rustfs_ecstore::object_lock_diag', 'request_id': request_id,
+ 'event': 'object_namespace_lock', 'component': 'ecstore', 'subsystem': 'object_lock',
+ 'op': op, 'mode': mode, 'bucket': '.rustfs.sys' if mode == 'read' else bucket,
+ 'object': obj, 'namespace': 'pool-0', 'owner': ':::9000', 'lock_attempt_id': attempt,
+ 'requested_version_id': None, 'timeout_ms': 1000, 'acquire_ms': 1001 if state == 'failed' else 0,
+ 'state': state, 'level': 'ERROR' if state == 'failed' else 'TRACE',
+ 'span': {'name': 'put_object_with_old_current_size'},
+ 'spans': [{'name': 'http-request', 'request_id': request_id, 'method': 'PUT',
+ 'uri': f'/{bucket}/{key}'}], **extra}
+ read_attempt = '20000000-0000-4000-8000-000000000003'
+ for state in ('acquiring', 'acquired', 'guard_dropped'):
+ rows.append(lock_row('read', 'get_object', f'buckets/{bucket}/.bucket-incarnation', read_attempt, state))
+ write_attempt = '20000000-0000-4000-8000-000000000004'
+ rows.append(lock_row('write', 'put_object_commit', key, write_attempt, 'acquiring'))
+ rows.append(lock_row('write', 'put_object_commit', key, write_attempt, 'failed', failure='timeout'))
+ write(folder / '00021-I05-caller-window.json', {'node': 'vm000', 'unit': 'rustfs-validation-heal-' +
+ binding['runId'] + '.service', 'complete': True, 'beforeCursor': middle_after,
+ 'afterCursor': last_after, 'hasMore': False, 'sourceMessageLoss': [],
+ 'journal': '\n'.join(json.dumps(row, sort_keys=True, separators=(',', ':')) for row in rows)})
+ return contender
+
+ def add_h2_failure_archive(self, root, candidate, h1_folder, *, window_rounds=1):
+ self.add_contender_archive(root, candidate, h1_folder)
+ evidence = h1_folder.parent
+ h2_folder = evidence / 'H2'
+ shutil.copytree(h1_folder, h2_folder)
+
+ def read(path): return json.loads(path.read_text())
+ def write(path, value): path.write_text(json.dumps(value, sort_keys=True, separators=(',', ':')))
+ old_binding = read(h2_folder / '00001-binding.json')
+ old_run, old_bucket = old_binding['runId'], old_binding['bucketPrefix']
+ run_id = old_run[:-2] + 'h2'
+ bucket = 'rv-heal-' + hashlib.sha256(run_id.encode()).hexdigest()[:16]
+
+ def replace(value):
+ if isinstance(value, dict): return {replace(key): replace(item) for key, item in value.items()}
+ if isinstance(value, list): return [replace(item) for item in value]
+ if isinstance(value, str): return value.replace(old_run, run_id).replace(old_bucket, bucket)
+ return value
+
+ for path in h2_folder.rglob('*.json'):
+ write(path, replace(read(path)))
+ nodes = ['vm000', 'vm001', 'vm002', 'vm003']
+ binding = read(h2_folder / '00001-binding.json')
+ binding.update(runId=run_id, bucketPrefix=bucket, harnessCommit=controller.H2_CONTENDER_HELPER,
+ endpoints={f'node{i}': f'http://rustfs-node{i}:9000' for i in range(1, 5)})
+ binding_hash = hashlib.sha256(canonical(binding).encode()).hexdigest()
+ write(h2_folder / '00001-binding.json', binding)
+
+ requests = {}
+ for sequence in range(1, 10):
+ request_path = h2_folder / f'fixture/{sequence:06d}-request.json'
+ ack_path = h2_folder / f'fixture/{sequence:06d}-ack.json'
+ request, ack = read(request_path), read(ack_path)
+ request['runId'] = ack['runId'] = run_id
+ request['params']['Bucket'] = ack['observation']['params']['Bucket'] = bucket
+ if sequence >= 3 and sequence != 6:
+ index, generation = (0, sequence - 3) if sequence <= 6 else (1, sequence - 7)
+ size = 32 * 1024**2 if index == 0 else 1024
+ body = payload(f'{bucket}/objects/{index:04d}.bin/{generation}', size)
+ item = {'size': len(body), 'sha256': hashlib.sha256(body).hexdigest()}
+ request['payload'] = ack['observation']['payload'] = item
+ etag = hashlib.md5(body, usedforsecurity=False).hexdigest()
+ ack['observation']['response']['ETag'] = etag
+ requests[ack['observation']['response']['VersionId']] = (item, etag)
+ raw = json.dumps(request, sort_keys=True, separators=(',', ':')).encode()
+ request_path.write_bytes(raw)
+ ack['requestSha256'] = hashlib.sha256(raw).hexdigest()
+ write(ack_path, ack)
+
+ for name in ('00010-original-ACK-bound-S3.json', '00013-I05-timeout-original-S3.json'):
+ path = h2_folder / name
+ value = read(path)
+ value['bucket'] = bucket
+ for entry in value['entries']:
+ if not entry['deleteMarker']:
+ entry['payload'], etag = requests[entry['versionId']]
+ entry['properties']['ETag'] = etag
+ write(path, value)
+
+ scope_path = h2_folder / 'timeout/scope.json'
+ scope = read(scope_path)
+ scope.update(runId=run_id, candidateCommit=controller.CANDIDATE, bindingSha256=binding_hash)
+ scope_entry = next(entry for entry in read(h2_folder / '00010-original-ACK-bound-S3.json')['entries']
+ if entry['key'] == scope['entry']['key']
+ and entry['versionId'] == scope['entry']['versionId'])
+ scope['entry']['payload'] = requests[scope_entry['versionId']][0]
+ write(scope_path, scope)
+ caller_dir = h2_folder / 'timeout/caller'
+ transport = caller_dir / 'transport'
+ request_path = transport / 'request.json'
+ caller_request = read(request_path)
+ caller_request.update(runId=run_id, harnessCommit=controller.H2_CONTENDER_HELPER,
+ candidateCommit=controller.CANDIDATE, bucket=bucket,
+ bindingSha256=binding_hash,
+ requestTarget=f"/{bucket}/{scope_entry['key']}?versionId={scope_entry['versionId']}")
+ caller_request['params']['Bucket'] = bucket
+ raw = json.dumps(caller_request, sort_keys=True, separators=(',', ':')).encode()
+ request_path.write_bytes(raw)
+ for name in ('request-written.json', 'disconnected.json'):
+ path = transport / name
+ value = read(path)
+ value.update(runId=run_id, harnessCommit=controller.H2_CONTENDER_HELPER,
+ candidateCommit=controller.CANDIDATE, bucket=bucket,
+ bindingSha256=binding_hash,
+ requestTarget=caller_request['requestTarget'],
+ requestSha256=hashlib.sha256(raw).hexdigest())
+ write(path, value)
+
+ contender = h2_folder / 'timeout/contender'
+ contender_request_path = contender / '000001-request.json'
+ contender_ack_path = contender / '000001-ack.json'
+ contender_request = read(contender_request_path)
+ contender_ack = read(contender_ack_path)
+ contender_body = payload(f'{run_id}/I05-timeout-contender', CONTENDER_BYTES)
+ contender_request.update(runId=run_id, payload={'size': len(contender_body),
+ 'sha256': hashlib.sha256(contender_body).hexdigest()})
+ contender_request['params']['Bucket'] = bucket
+ raw = json.dumps(contender_request, sort_keys=True, separators=(',', ':')).encode()
+ contender_request_path.write_bytes(raw)
+ contender_ack.update(runId=run_id, requestSha256=hashlib.sha256(raw).hexdigest())
+ contender_ack['observation']['params']['Bucket'] = bucket
+ contender_ack['observation']['payload'] = contender_request['payload']
+ write(contender_ack_path, contender_ack)
+
+ records = {}
+ for path in sorted(h2_folder.glob('*.json')):
+ match = path.name.split('-', 1)
+ label = match[1][:-5]
+ records.setdefault(label, []).append(read(path))
+ for path in h2_folder.glob('*.json'):
+ path.unlink()
+
+ def clone(value): return json.loads(json.dumps(value))
+ def by_node(value, node):
+ result = clone(value)
+ if isinstance(result, dict): result['node'] = node
+ return result
+ old_reservation = records['reserved'][0]
+ old_owner = old_reservation['owner']
+ owners = {}
+ reservations = []
+ uploads = []
+ for node in nodes:
+ owner = clone(old_owner)
+ owner.update(runId=run_id, bucket=bucket, hostname=node,
+ unit=f'rustfs-validation-heal-{run_id}.service')
+ owners[node] = owner
+ reservation = clone(old_reservation)
+ reservation.update(node=node, owner=owner)
+ reservations.append(reservation)
+ uploads.append(by_node(records['inspector-upload'][0], node))
+
+ baseline_resources = records['I05-caller-baseline-resources'][0]
+ resources = [by_node(baseline_resources, node) for node in nodes]
+ for value, node in zip(resources, nodes, strict=True):
+ value['identity']['binarySha256'] = binding['candidateBinarySha256']
+ value['identity']['settings'] = controller.runtime_config('lock-timeout')
+ original_scene = records['original-failure-scene'][0]['vm000']
+ scenes = {}
+ for node in nodes:
+ scenes[node] = clone(original_scene)
+ scenes[node].update(hostname=node, unit=owners[node]['unit'],
+ binarySha256=binding['candidateBinarySha256'])
+ physical = records['I05-timeout-original-physical'][0]
+ old_physical = physical['vm000']
+ snapshots = {node: clone(old_physical) for node in nodes}
+ started = {node: {'started': True} for node in nodes}
+ deployed = records['deployed-topology'][0]
+ deployed = {node: clone(deployed['vm000']) for node in nodes}
+ preflight = records['preflight'][0]
+ preflight = {node: {**clone(preflight['vm000']), 'hostname': node} for node in nodes}
+ checkpoint = records['I05-caller-checkpoint'][0]
+ checkpoints = [by_node(checkpoint, node) for node in nodes]
+ cursor_by_node = {node: f'{node}-checkpoint' for node in nodes}
+ windows = []
+ source_windows = records['I05-caller-window']
+ for stage in range(3):
+ for round_index in range(window_rounds):
+ for node in nodes:
+ source = source_windows[stage]
+ window = by_node(source, node)
+ window['unit'] = owners[node]['unit']
+ window['beforeCursor'] = cursor_by_node[node]
+ cursor_by_node[node] = f'{node}-window-{stage}-{round_index + 1}'
+ window['afterCursor'] = cursor_by_node[node]
+ if node != 'vm000' or stage != 2 or round_index != 0: window['journal'] = ''
+ windows.append(window)
+ admission_resources = [clone(value) for value in resources]
+ for value in admission_resources:
+ value['identity']['binarySha256'] = binding['candidateBinarySha256']
+ value['identity']['settings'] = controller.runtime_config('lock-timeout')
+ quiescent_source = records['quiescent'][0].get('vm000', {'stopped': True})
+ quiescent = {node: clone(quiescent_source) for node in nodes}
+ logs = [by_node(records['original-failure-logs'][0], node) for node in nodes]
+
+ attestation = records['integrity-fleet-attestation'][0]
+ attestation['nodes'] = nodes
+ attestation['allParticipatingNodesUseCandidate'] = True
+ actual = {'topology': 'H2', 'nodes': 4, 'independentDrives': 16}
+ diagnostic_result = {'topology': 'H2', 'diagnosticComplete': False, 'cleanup': 'FAIL',
+ 'wholeCasePassClaimed': False, 'canonicalCasePassClaimed': False,
+ 'failure': controller.FAILURES[controller.H2_CONTENDER_HELPER], 'namespacesRetained': True}
+ labels = controller.h2_labels((window_rounds, window_rounds, window_rounds))
+ by_label = {
+ 'binding': [binding], 'inspector-build': records['inspector-build'],
+ 'preflight': [preflight], 'integrity-fleet-attestation': [attestation],
+ 'reserved': reservations, 'inspector-upload': uploads, 'started': [started],
+ 'deployed-topology': [deployed], 'actual-topology': [actual],
+ 'original-ACK-bound-S3': records['original-ACK-bound-S3'],
+ 'original-ACK-proof': records['original-ACK-proof'],
+ 'I05-timeout-original-physical': [snapshots], 'I05-timeout-original-S3': records['I05-timeout-original-S3'],
+ 'I05-timeout-baseline-physical-after-public-read': [clone(snapshots)],
+ 'I05-caller-baseline-resources': resources, 'I05-caller-checkpoint': checkpoints,
+ 'I05-caller-window': windows, 'I05-caller-before-disconnect-socket': records['I05-caller-before-disconnect-socket'],
+ 'I05-caller-admission-resources': admission_resources, 'quiescent': [quiescent],
+ 'original-failure-scene': [scenes], 'original-failure-logs': logs,
+ 'diagnostic-result': [diagnostic_result],
+ }
+ indexes = {}
+ for label in labels:
+ index = indexes.get(label, 0)
+ value = by_label[label][index]
+ indexes[label] = index + 1
+ write(h2_folder / f'{sum(indexes.values()):05d}-{label}.json', value)
+
+ shutil.rmtree(h1_folder)
+ h1_folder.mkdir()
+ write(h1_folder / '00001-diagnostic-result.json', {'topology': 'H1', 'diagnosticComplete': True,
+ 'cleanup': 'PASS', 'wholeCasePassClaimed': False, 'canonicalCasePassClaimed': False})
+ case_path = root / 'rustfs-validation-39711742431-1' / 'run' / controller.CASE / 'cases' / f'{controller.CASE}.json'
+ case_result = read(case_path)
+ case_result['provenance']['testing_commit'] = controller.H2_CONTENDER_HELPER
+ write(case_path, case_result)
+ summary_path = evidence / 'healthy-summary.json'
+ summary = read(summary_path)
+ summary['diagnostics'] = [
+ {'topology': 'H1', 'diagnosticComplete': True, 'cleanup': 'PASS',
+ 'wholeCasePassClaimed': False, 'canonicalCasePassClaimed': False},
+ diagnostic_result]
+ summary.update(diagnosticComplete=False, diagnosticBranchesPassed=False, cleanup='FAIL', complete=False,
+ wholeCasePassClaimed=False, canonicalCasePassClaimed=False)
+ write(summary_path, summary)
+ return h2_folder
+
+ def test_original_live_failure_yields_exact_pre_contender_preservation_plan(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root, candidate, _ = self.archive(directory)
+ plans = recovery.recovery_plan(root, 39711742431, candidate)
+ self.assertEqual(len(plans), 1)
+ plan = plans[0]
+ self.assertEqual(plan['kind'], 'unresolved-i05-timeout')
+ self.assertEqual(plan['runId'], 'gh-39711742431-1-h1')
+ self.assertEqual(plan['nodes'], ['vm000'])
+ self.assertEqual(plan['ackFilesSha256'], 'fc8576efabec0e2163f8b434d0971f307b3334b01c3afae90459e2280722b70c')
+ self.assertEqual(plan['businessSceneSha256'], {'vm000':
+ '6c9e808ff0c6d03cb5dfd5546bd588c3d3e1bcee3c167877dec21c4399fc1e6d'})
+
+ def test_new_helper_failed_refusal_yields_exact_contender_preservation_plan(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root, candidate, folder = self.archive(directory)
+ self.add_contender_archive(root, candidate, folder)
+ plan, = recovery.recovery_plan(root, 39711742431, candidate)
+ self.assertEqual(plan['kind'], 'unresolved-i05-timeout')
+ self.assertEqual(plan['harnessCommit'], controller.CONTENDER_HELPER)
+ self.assertEqual(plan['nodes'], ['vm000'])
+ self.assertEqual(plan['businessSceneSha256'], {'vm000':
+ '6c9e808ff0c6d03cb5dfd5546bd588c3d3e1bcee3c167877dec21c4399fc1e6d'})
+
+ def test_drain_budget_failure_yields_exact_bounded_preservation_plan(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root, candidate, _ = self.drain_archive(directory)
+ plan, = recovery.recovery_plan(root, 39746002648, candidate)
+ self.assertEqual(plan['kind'], 'unresolved-i05-timeout')
+ self.assertEqual(plan['runId'], 'gh-39746002648-1-h1')
+ self.assertEqual(plan['harnessCommit'], controller.DRAIN_BUDGET_HELPER)
+ self.assertEqual(plan['nodes'], ['vm000'])
+ self.assertEqual(plan['ackFilesSha256'], '21e9471c9e9133186b88fb664f8a4deff4c93044761a9c399915a0b1db9d6f32')
+ self.assertEqual(plan['businessSceneSha256'], {'vm000':
+ 'c438f5bf295f56ddd168e9e840b41281863c3da06c0973eb9abead95edd63e89'})
+
+ def test_descriptor_failure_preserves_exact_scene_without_leak_or_pass_claim(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root, candidate, folder = self.drain_archive(directory, 39754893467)
+ plan, = recovery.recovery_plan(root, 39754893467, candidate)
+ self.assertEqual(plan['harnessCommit'], controller.DESCRIPTOR_HELPER)
+ self.assertEqual(plan['kind'], 'unresolved-i05-timeout')
+ self.assertEqual(plan['ackFilesSha256'],
+ 'f114c1f2f4d8ddbbc5c98549e1e32f8da61241b4ccf3123bf89f9f3aed88879d')
+ self.assertEqual(plan['businessSceneSha256'], {'vm000':
+ 'f921b69ea51622f89e9ad4a78de8c890caf0b7ffefa0b401457d6f9796fc7d31'})
+ result = json.loads((folder.parents[2] / 'cases' / f'{controller.CASE}.json').read_text())
+ self.assertEqual((result['verdict'], result['cleanup_status']), ('FAIL', 'FAIL'))
+
+ def test_drain_window_boundary_follows_labels_after_variable_caller_wait(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root, candidate, folder = self.drain_archive(directory, 39754893467)
+ empty = folder / '00023-I05-caller-window.json'
+ value = json.loads(empty.read_text())
+ self.assertEqual(value['journal'], '')
+ self.assertEqual(value['beforeCursor'], value['afterCursor'])
+ empty.unlink()
+ for path in sorted(folder.glob('*.json')):
+ index = int(path.name[:5])
+ if index > 23:
+ path.rename(path.with_name(f'{index - 1:05d}' + path.name[5:]))
+ plan, = recovery.recovery_plan(root, 39754893467, candidate)
+ self.assertEqual(plan['runId'], 'gh-39754893467-1-h1')
+
+ def test_descriptor_failure_refuses_corrupt_census_and_unbounded_effects(self):
+ variants = ('missing-census', 'count', 'inode', 'duplicate-fd', 'foreign-tcp', 'tcp-join',
+ 'namespace', 'process', 'leak-claim', 'raw-path', 'vanished', 'socket-match',
+ 'success', 'unknown', 'extra-action', 'physical-drift')
+ for variant in variants:
+ with self.subTest(variant=variant), tempfile.TemporaryDirectory() as directory:
+ root, candidate, folder = self.drain_archive(directory, 39754893467)
+ path = next(folder.glob('*-I05-caller-drain-resources.json'))
+ value = json.loads(path.read_text())
+ census = value['descriptorCensus']
+ if variant == 'missing-census': value.pop('descriptorCensus')
+ elif variant == 'count': census['fileDescriptors'] += 1
+ elif variant == 'inode': next(row for row in census['descriptors'] if row['kind'] == 'socket')['inode'] += 1
+ elif variant == 'duplicate-fd': census['descriptors'][-1]['descriptor'] = 0
+ elif variant == 'foreign-tcp': census['ownedTcpConnections'][0]['inode'] = 999999999
+ elif variant == 'tcp-join': census['ownedTcpConnections'][0]['descriptors'] = [0]
+ elif variant == 'namespace': value['networkNamespace'] = 'net:[1]'
+ elif variant == 'process': value['identity']['startTicks'] += 1
+ elif variant == 'leak-claim': census['noLeakProven'] = True
+ elif variant == 'raw-path': census['descriptors'][0]['target'] = '/private/path'
+ elif variant == 'vanished': census['vanishedDescriptors'] = 1
+ elif variant == 'socket-match':
+ path = next(folder.glob('*-I05-caller-drain-socket.json'))
+ value = json.loads(path.read_text())
+ value['samples'][0]['ownedTcpConnections'][0]['descriptors'] = [0]
+ elif variant in ('success', 'unknown'):
+ path = folder / 'timeout/contender/000001-ack.json'
+ value = json.loads(path.read_text())
+ if variant == 'success': value['observation']['response']['ResponseMetadata']['HTTPStatusCode'] = 200
+ else: value['observation']['resultUnknown'] = True
+ elif variant == 'extra-action': (folder / 'timeout/contender/000002-request.json').write_text('{}')
+ else:
+ path = next(folder.glob('*-original-failure-scene.json'))
+ value = json.loads(path.read_text())
+ value['vm000']['formats'][0]['sha256'] = '0' * 64
+ path.write_text(json.dumps(value))
+ with patch.object(recovery, 'Fleet') as fleet, self.assertRaises((ValueError, KeyError)):
+ recovery.recovery_plan(root, 39754893467, candidate)
+ fleet.assert_not_called()
+
+ def test_drain_budget_failure_refuses_unknown_mutation_or_incomplete_sampling(self):
+ for variant in ('success', 'unknown', 'extra-action', 'missing-sample', 'physical-drift'):
+ with self.subTest(variant=variant), tempfile.TemporaryDirectory() as directory:
+ root, candidate, folder = self.drain_archive(directory)
+ if variant == 'success':
+ ack = json.loads((folder / 'timeout/contender/000001-ack.json').read_text())
+ ack['observation']['response']['ResponseMetadata']['HTTPStatusCode'] = 200
+ (folder / 'timeout/contender/000001-ack.json').write_text(json.dumps(ack))
+ elif variant == 'unknown':
+ ack = json.loads((folder / 'timeout/contender/000001-ack.json').read_text())
+ ack['observation']['resultUnknown'] = True
+ (folder / 'timeout/contender/000001-ack.json').write_text(json.dumps(ack))
+ elif variant == 'extra-action':
+ (folder / 'timeout/contender/000002-request.json').write_text('{}')
+ elif variant == 'missing-sample':
+ next(folder.glob('*-I05-caller-drain-resources.json')).unlink()
+ else:
+ scene = json.loads((folder / '00058-original-failure-scene.json').read_text())
+ scene['vm000']['formats'][0]['sha256'] = '0' * 64
+ (folder / '00058-original-failure-scene.json').write_text(json.dumps(scene))
+ with patch.object(recovery, 'Fleet') as fleet, self.assertRaises((ValueError, KeyError)):
+ recovery.recovery_plan(root, 39746002648, candidate)
+ fleet.assert_not_called()
+
+ def test_h2_failure_after_clean_h1_yields_exact_four_node_plan(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root, candidate, h1_folder = self.archive(directory)
+ self.add_h2_failure_archive(root, candidate, h1_folder, window_rounds=2)
+ plan, = recovery.recovery_plan(root, 39711742431, candidate)
+ self.assertEqual(plan['kind'], 'unresolved-i05-timeout')
+ self.assertEqual(plan['runId'], 'gh-39711742431-1-h2')
+ self.assertEqual(plan['harnessCommit'], controller.H2_CONTENDER_HELPER)
+ self.assertEqual(plan['nodes'], ['vm000', 'vm001', 'vm002', 'vm003'])
+ self.assertEqual(set(plan['owners']), set(plan['businessSceneSha256']))
+
+ def test_h2_retention_refuses_partial_peer_or_unclean_h1_evidence(self):
+ for variant in ('missing-reservation', 'missing-peer-window', 'missing-peer-log', 'h1-dirty', 'peer-contender'):
+ with self.subTest(variant=variant), tempfile.TemporaryDirectory() as directory:
+ root, candidate, h1_folder = self.archive(directory)
+ h2_folder = self.add_h2_failure_archive(root, candidate, h1_folder)
+ if variant == 'missing-reservation':
+ (h2_folder / '00005-reserved.json').unlink()
+ elif variant == 'missing-peer-window':
+ window = next(path for path in h2_folder.glob('*-I05-caller-window.json')
+ if json.loads(path.read_text()).get('node') == 'vm002')
+ window.unlink()
+ elif variant == 'missing-peer-log':
+ log = next(path for path in h2_folder.glob('*-original-failure-logs.json')
+ if json.loads(path.read_text()).get('node') == 'vm002')
+ log.unlink()
+ elif variant == 'h1-dirty':
+ summary_path = h1_folder.parent / 'healthy-summary.json'
+ summary = json.loads(summary_path.read_text())
+ summary['diagnostics'][0]['cleanup'] = 'FAIL'
+ summary_path.write_text(json.dumps(summary))
+ else:
+ window = next(path for path in h2_folder.glob('*-I05-caller-window.json')
+ if json.loads(path.read_text()).get('node') == 'vm001')
+ value = json.loads(window.read_text())
+ value['journal'] = json.dumps({'request_id': '30000000-0000-4000-8000-000000000001'})
+ window.write_text(json.dumps(value))
+ with patch.object(recovery, 'Fleet') as fleet, self.assertRaises((ValueError, KeyError)):
+ recovery.recovery_plan(root, 39711742431, candidate)
+ fleet.assert_not_called()
+
+ def test_new_helper_refuses_unknown_success_or_corrupt_lock_lifecycle(self):
+ for variant in ('unknown', 'success', 'acquired', 'read-lifecycle', 'extra-action'):
+ with self.subTest(variant=variant), tempfile.TemporaryDirectory() as directory:
+ root, candidate, folder = self.archive(directory)
+ contender = self.add_contender_archive(root, candidate, folder)
+ ack_path = contender / '000001-ack.json'
+ ack = json.loads(ack_path.read_text())
+ if variant == 'unknown': ack['observation']['resultUnknown'] = True
+ elif variant == 'success':
+ ack['observation']['response']['ResponseMetadata']['HTTPStatusCode'] = 200
+ elif variant == 'extra-action':
+ (contender / '000002-request.json').write_text('{}')
+ elif variant == 'acquired':
+ window_path = folder / '00021-I05-caller-window.json'
+ window = json.loads(window_path.read_text())
+ events = [json.loads(line) for line in window['journal'].splitlines()]
+ events[-1]['state'] = 'acquired'
+ window['journal'] = '\n'.join(json.dumps(event) for event in events)
+ window_path.write_text(json.dumps(window))
+ elif variant == 'read-lifecycle':
+ window_path = folder / '00021-I05-caller-window.json'
+ window = json.loads(window_path.read_text())
+ events = [json.loads(line) for line in window['journal'].splitlines()]
+ events[2]['object'] = 'buckets/foreign/.bucket-incarnation'
+ window['journal'] = '\n'.join(json.dumps(event) for event in events)
+ window_path.write_text(json.dumps(window))
+ ack_path.write_text(json.dumps(ack))
+ with patch.object(recovery, 'Fleet') as fleet, self.assertRaises((ValueError, KeyError)):
+ recovery.recovery_plan(root, 39711742431, candidate)
+ fleet.assert_not_called()
+
+ def test_tampered_or_unresolved_evidence_refuses_before_fleet_creation(self):
+ variants = ('candidate', 'helper', 'missing-ack', 'ack-hash', 'ack-unknown', 'ack-operation',
+ 'request-mutation', 'payload', 'marker', 'owner', 'physical', 'live', 'other-node',
+ 'contender', 'extra-fixture', 'other-case', 'H2', 'failure', 'open-socket', 'GET-mutation',
+ 'GET-version', 'GET-hash', 'GET-retry', 'extra-transport', 'inspector', 'extra-action',
+ 'metadata', 'tags')
+ for variant in variants:
+ with self.subTest(variant=variant), tempfile.TemporaryDirectory() as directory:
+ root, candidate, folder = self.archive(directory)
+ def change(name, mutate):
+ path = folder / name; value = json.loads(path.read_text()); mutate(value)
+ path.write_text(json.dumps(value))
+ if variant == 'candidate': candidate['source_sha'] = '1' * 40
+ elif variant == 'helper': change('00001-binding.json', lambda v: v.update(harnessCommit='2' * 40))
+ elif variant == 'missing-ack': (folder / 'fixture/000009-ack.json').unlink()
+ elif variant.startswith('ack-'):
+ field, value = {'ack-hash': ('requestSha256', '0' * 64),
+ 'ack-unknown': ('observation', {'resultUnknown': True}),
+ 'ack-operation': ('observation', {'operation': 'complete_multipart_upload'})}[variant]
+ change('fixture/000003-ack.json', lambda v: v.update({field: value}))
+ elif variant in ('request-mutation', 'payload'):
+ change('fixture/000003-request.json', lambda v: v.update(
+ operation='delete_object') if variant == 'request-mutation' else v.update(payload={'size': 1}))
+ request = folder / 'fixture/000003-request.json'
+ change('fixture/000003-ack.json', lambda v: v.update(requestSha256=hashlib.sha256(request.read_bytes()).hexdigest()))
+ elif variant == 'marker':
+ change('fixture/000006-ack.json', lambda v: v['observation']['response'].update(DeleteMarker=False))
+ elif variant == 'owner': change('00005-reserved.json', lambda v: v['owner'].update(runtimeProfile='manual'))
+ elif variant in ('physical', 'live', 'other-node'):
+ change('00019-original-failure-scene.json', lambda v:
+ v['vm000']['files'][0].update(sha256='0' * 64) if variant == 'physical' else
+ v['vm000'].update(pid=7) if variant == 'live' else v.update(vm001=v['vm000']))
+ elif variant == 'contender': (folder / 'timeout/contender').mkdir()
+ elif variant == 'extra-fixture': (folder / 'fixture/000010-request.json').write_text('{}')
+ elif variant == 'other-case':
+ (folder.parents[2] / 'cases/other.json').write_text('{}')
+ elif variant == 'H2': (folder.parent / 'H2').mkdir()
+ elif variant == 'failure': change('00021-diagnostic-result.json', lambda v: v.update(failure={}))
+ elif variant in ('open-socket', 'GET-retry'):
+ change('timeout/caller/transport/disconnected.json', lambda v: v.update(
+ clientSocketClosed=False) if variant == 'open-socket' else v.update(connectionAttempts=2))
+ elif variant in ('GET-mutation', 'GET-version'):
+ change('timeout/caller/transport/request.json', lambda v: v.update(operation='put_object')
+ if variant == 'GET-mutation' else v['params'].update(VersionId='foreign'))
+ elif variant == 'GET-hash':
+ change('timeout/caller/transport/request-written.json', lambda v: v.update(requestSha256='0' * 64))
+ elif variant == 'extra-transport': (folder / 'timeout/caller/transport/ack.json').write_text('{}')
+ elif variant == 'inspector': change('00002-inspector-build.json', lambda v: v.update(binarySha256='0' * 64))
+ elif variant in ('metadata', 'tags'):
+ def mutate(value):
+ if variant == 'metadata': value['entries'][0]['properties']['Metadata']['nonce'] = 'foreign'
+ else: value['entries'][0]['tags'] = []
+ for name in ('00010-original-ACK-bound-S3.json', '00013-I05-timeout-original-S3.json'):
+ change(name, mutate)
+ else: (folder / '00022-another-action.json').write_text('{}')
+ with patch.object(recovery, 'Fleet') as fleet, self.assertRaises((ValueError, KeyError)):
+ recovery.recovery_plan(root, 39711742431, candidate)
+ fleet.assert_not_called()
+
+ def test_main_routes_preservation_before_generic_cleanup_and_never_deploys(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root, candidate, _ = self.archive(directory)
+ plan = recovery.recovery_plan(root, 39711742431, candidate)[0]
+ path = Path(directory) / 'candidate.json'; path.write_text(json.dumps(candidate))
+ evidence = Path(directory) / 'recovery'
+ fleet = SimpleNamespace(nodes=plan['nodes'], run_id=plan['runId'], record=lambda label, value: value)
+ proof = {'runId': plan['runId'], 'cleanup': 'ISOLATED', 'canonicalPassClaimed': False}
+ with patch('sys.argv', ['recover', '--failed-run-id', '39711742431', '--artifact', str(root),
+ '--candidate', str(path), '--evidence', str(evidence)]), \
+ patch.object(recovery, 'Fleet', return_value=fleet), \
+ patch.object(recovery, 'isolate_race_scene', return_value=proof) as isolate:
+ recovery.main()
+ isolate.assert_called_once_with(fleet, plan)
+ summary = json.loads((evidence / 'recovery-summary.json').read_text())
+ self.assertIn('ISOLATED', json.dumps(summary))
+
+ def scene(self, directory):
+ helper, state, roots, node, request, scene = race_tests.RaceRetentionTests().scene(directory)
+ owner = request['expectedOwner']; owner['runtimeProfile'] = 'lock-timeout'
+ for folder in (*roots, state):
+ (folder / 'owner.json').write_text(json.dumps(owner))
+ for root in roots:
+ for path in (root / 'quarantine').iterdir(): path.unlink()
+ return helper, state, roots, node, request, scene
+
+ def test_all_five_interrupted_moves_preserve_metadata_content_and_unread_credentials(self):
+ for interrupt_after in range(1, 6):
+ with self.subTest(interrupt_after=interrupt_after), tempfile.TemporaryDirectory() as directory:
+ helper, state, roots, node, request, _ = self.scene(directory)
+ original = {path: (path.stat(), path.read_bytes()) for folder in (*roots, state)
+ for path in folder.rglob('*') if path.is_file()}
+ opened, moves = [], 0
+ original_open = os.open
+ def record(path, *args, **kwargs):
+ opened.append(str(path)); return original_open(path, *args, **kwargs)
+ def interrupted(source, target):
+ nonlocal moves
+ helper.local_rename(source, target)
+ if target.is_dir():
+ moves += 1
+ if moves == interrupt_after: raise RuntimeError('lost connection after rename')
+ with helper.root_stats(), patch.object(os, 'open', side_effect=record):
+ before = retained.run(request, node)
+ change = {**request, 'action': 'retained-scene-isolate', 'expectedBaseline': before['baseline']}
+ with patch.object(retained, 'rename_new', side_effect=interrupted), self.assertRaises(RuntimeError):
+ retained.run(change, node)
+ with patch.object(retained, 'rename_new', side_effect=helper.local_rename):
+ proof = retained.run(change, node)
+ self.assertTrue(proof['activeNamespaceIsolated'])
+ self.assertEqual(proof['baseline'], before['baseline'])
+ self.assertEqual(proof['locations'], ['retained'] * 5)
+ self.assertEqual(len(proof['privateArchiveParents']), 5)
+ self.assertFalse(retained.run(change, node)['mutated'])
+ retained_state, retained_roots = retained.destinations(state, roots, per_run_parent=True)
+ for old, (info, raw) in original.items():
+ source = next(folder for folder in (*roots, state) if old.is_relative_to(folder))
+ dest = retained_state if source == state else retained_roots[roots.index(source)]
+ moved = dest / old.relative_to(source)
+ actual = moved.stat()
+ self.assertEqual(moved.read_bytes(), raw)
+ for field in ('st_ino', 'st_dev', 'st_mode', 'st_uid', 'st_gid', 'st_size', 'st_mtime_ns'):
+ self.assertEqual(getattr(actual, field), getattr(info, field))
+ self.assertFalse(any(path.endswith('service.env') or path.endswith('/credential') for path in opened))
+
+ def test_i05_does_not_open_other_profiles_quarantine_or_multipart_files(self):
+ with tempfile.TemporaryDirectory() as directory:
+ helper, state, roots, node, request, _ = self.scene(directory)
+ extra = roots[0] / 'quarantine/G01-target.original'
+ extra.write_bytes(b'foreign backup must stay opaque')
+ opened = []; original_open = os.open
+ def record(path, *args, **kwargs):
+ opened.append(str(path)); return original_open(path, *args, **kwargs)
+ with helper.root_stats(), patch.object(os, 'open', side_effect=record): retained.run(request, node)
+ self.assertNotIn(str(extra), opened)
+
+ def test_plan_profile_cannot_be_reused_for_another_isolation_kind(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root, candidate, _ = self.archive(directory)
+ plan = recovery.recovery_plan(root, 39711742431, candidate)[0]
+ plan['kind'] = 'unresolved-g01-race'
+ with self.assertRaises(ValueError): race_retention.validate(plan)
+
+ def successor_archive(self, directory):
+ root, candidate, folder = self.archive(directory)
+ for path in folder.glob('*-reserved.json'):
+ value = json.loads(path.read_text())
+ value['owner']['runtimeProfile'] = 'lock-successor'
+ path.write_text(json.dumps(value))
+ result_path = next(folder.glob('*-diagnostic-result.json'))
+ result = json.loads(result_path.read_text())
+ result['branch'] = 'queued-successor'
+ result_path.write_text(json.dumps(result))
+ summary_path = folder.parent / 'healthy-summary.json'
+ summary = json.loads(summary_path.read_text())
+ summary.update(diagnosticBranch='queued-successor', diagnostics=[result],
+ wholeCasePassClaimed=False)
+ summary_path.write_text(json.dumps(summary))
+ receipt = folder / 'queued-successor/successor'
+ receipt.mkdir(parents=True)
+ (receipt / '000001-request.json').write_text('{"operation":"put_object"}')
+ (receipt / '000001-ack.json').write_text('{"resultUnknown":true,"acknowledged":false}')
+ binding = json.loads(next(folder.glob('*-binding.json')).read_text())
+ scene = json.loads(next(folder.glob('*-original-failure-scene.json')).read_text())
+ seal = controller.seal_successor_scene(folder, binding, scene)
+ (folder / 'successor-retention.json').write_text(json.dumps(seal))
+ return root, candidate, folder
+
+ def test_successor_unknown_receipt_selects_only_stopped_scene_isolation(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root, candidate, folder = self.successor_archive(directory)
+ original = {p: p.read_bytes() for p in root.rglob('*') if p.is_file()}
+ with patch.object(recovery, 'Fleet') as fleet:
+ plan, = recovery.recovery_plan(root, 39711742431, candidate)
+ fleet.assert_not_called()
+ self.assertEqual(plan['kind'], 'unresolved-i05-successor')
+ self.assertEqual(plan['owners']['vm000']['runtimeProfile'], 'lock-successor')
+ race_retention.validate(plan)
+ self.assertEqual(original, {p: p.read_bytes() for p in root.rglob('*') if p.is_file()})
+ candidate_path = Path(directory) / 'candidate.json'
+ candidate_path.write_text(json.dumps(candidate))
+ evidence = Path(directory) / 'recovery'
+ fake = SimpleNamespace(nodes=plan['nodes'], run_id=plan['runId'], record=lambda label, value: value)
+ proof = {'runId': plan['runId'], 'cleanup': 'ISOLATED', 'canonicalPassClaimed': False}
+ with patch('sys.argv', ['recover', '--failed-run-id', '39711742431', '--artifact', str(root),
+ '--candidate', str(candidate_path), '--evidence', str(evidence)]), \
+ patch.object(recovery, 'Fleet', return_value=fake), \
+ patch.object(recovery, 'isolate_race_scene', return_value=proof) as isolate:
+ recovery.main()
+ isolate.assert_called_once_with(fake, plan)
+
+ def test_successor_seal_refuses_changed_or_missing_original_evidence_and_foreign_profile(self):
+ for variant in ('receipt', 'missing', 'extra', 'symlink', 'scene', 'candidate', 'kind', 'profile', 'dirty-predecessor'):
+ with self.subTest(variant=variant), tempfile.TemporaryDirectory() as directory:
+ root, candidate, folder = self.successor_archive(directory)
+ receipt = folder / 'queued-successor/successor/000001-ack.json'
+ if variant == 'receipt': receipt.write_text('{}')
+ elif variant == 'missing': receipt.unlink()
+ elif variant == 'extra': (folder / 'unrecorded-write.json').write_text('{}')
+ elif variant == 'symlink':
+ target = Path(directory) / 'receipt.json'; target.write_bytes(receipt.read_bytes())
+ receipt.unlink(); receipt.symlink_to(target)
+ elif variant == 'candidate': candidate['package_sha256'] = '0' * 64
+ elif variant == 'dirty-predecessor':
+ path = folder.parent / 'healthy-summary.json'
+ summary = json.loads(path.read_text()); summary['diagnostics'] *= 2
+ path.write_text(json.dumps(summary))
+ else:
+ path = folder / 'successor-retention.json'
+ seal = json.loads(path.read_text())
+ if variant == 'scene': seal['plan']['businessSceneSha256']['vm000'] = '0' * 64
+ elif variant == 'kind': seal['plan']['kind'] = 'unresolved-i05-timeout'
+ else: seal['plan']['owners']['vm000']['runtimeProfile'] = 'manual'
+ path.write_text(json.dumps(seal))
+ with patch.object(recovery, 'Fleet') as fleet, self.assertRaises(ValueError):
+ recovery.recovery_plan(root, 39711742431, candidate)
+ fleet.assert_not_called()
+
+ def test_successor_isolation_preserves_all_five_original_trees(self):
+ with tempfile.TemporaryDirectory() as directory:
+ helper, state, roots, node, request, _ = self.scene(directory)
+ request['expectedOwner']['runtimeProfile'] = 'lock-successor'
+ for folder in (*roots, state):
+ (folder / 'owner.json').write_text(json.dumps(request['expectedOwner']))
+ original = {p: (p.stat().st_ino, p.read_bytes()) for folder in (*roots, state)
+ for p in folder.rglob('*') if p.is_file()}
+ with helper.root_stats():
+ before = retained.run(request, node)
+ change = {**request, 'action': 'retained-scene-isolate', 'expectedBaseline': before['baseline']}
+ with patch.object(retained, 'rename_new', side_effect=helper.local_rename):
+ proof = retained.run(change, node)
+ self.assertFalse(retained.run(change, node)['mutated'])
+ self.assertEqual(proof['locations'], ['retained'] * 5)
+ self.assertFalse(proof['originalSceneRestored'] or proof['backupRestorationUsed'] or proof['canonicalPassClaimed'])
+ target_state, target_roots = retained.destinations(state, roots, per_run_parent=True)
+ for path, expected in original.items():
+ source = next(folder for folder in (*roots, state) if path.is_relative_to(folder))
+ target = target_state if source == state else target_roots[roots.index(source)]
+ moved = target / path.relative_to(source)
+ self.assertEqual((moved.stat().st_ino, moved.read_bytes()), expected)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/test_validation_heal_unformatted.py b/test_validation_heal_unformatted.py
new file mode 100644
index 00000000..a13aeb9e
--- /dev/null
+++ b/test_validation_heal_unformatted.py
@@ -0,0 +1,154 @@
+import base64
+import copy
+import hashlib
+import json
+from pathlib import Path
+import tempfile
+from types import SimpleNamespace
+import unittest
+from unittest.mock import patch
+
+from scripts.validation_heal import replace_drive, unformatted
+from scripts.validation_heal.matrix import select, AUTOMATIC_CASES
+
+
+class UnformattedReplacementTests(unittest.TestCase):
+ original_id = "12345678-1234-4234-8234-123456789abc"
+ foreign_id = "22345678-1234-4234-8234-123456789abc"
+ original_members = [f"32345678-1234-4234-8234-123456789ab{index}" for index in range(4)]
+ foreign_members = [f"42345678-1234-4234-8234-123456789ab{index}" for index in range(4)]
+
+ def format(self, foreign=False):
+ members = self.foreign_members if foreign else self.original_members
+ return {"version": "1", "format": "xl", "id": self.foreign_id if foreign else self.original_id,
+ "xl": {"version": "3", "this": members[0], "sets": [members], "distributionAlgo": "SIPMOD+PARITY"}}
+
+ def scene(self):
+ before = {"node": {"formats": [{"disk": index, "sha256": "original-" + str(index), "value": {"id": self.original_id}}
+ for index in range(4)],
+ "files": [{"disk": index, "path": "bucket/object/part.1", "sha256": "part-" + str(index), "size": 17}
+ for index in range(4)]}}
+ after = copy.deepcopy(before)
+ after["node"]["files"] = after["node"]["files"][1:]
+ after["node"]["formats"][0] = {"disk": 0, "present": False, "unformattedReplacement": True}
+ return before, after
+
+ def test_negative_scene_cannot_hide_survivor_write_or_foreign_reidentification(self):
+ before, after = self.scene()
+ self.assertFalse(unformatted.negative_scene(before, after, ("node", 0), "blank-unformatted", None)["productRepairClaimed"])
+ foreign = {"sha256": "foreign-digest", "value": self.format(True)}
+ after["node"]["formats"][0] = {"disk": 0, "sha256": foreign["sha256"], "value": foreign["value"]}
+ unformatted.negative_scene(before, after, ("node", 0), "foreign-format", foreign)
+ for kind in ("survivor", "target", "format", "foreign"):
+ bad = copy.deepcopy(after)
+ if kind == "survivor": bad["node"]["files"][0]["sha256"] = "changed"
+ elif kind == "target": bad["node"]["files"].append(before["node"]["files"][0])
+ elif kind == "format": bad["node"]["formats"][1]["value"]["id"] = "changed"
+ else: bad["node"]["formats"][0]["value"]["id"] = self.original_id
+ with self.subTest(kind=kind), self.assertRaises(ValueError):
+ unformatted.negative_scene(before, bad, ("node", 0), "foreign-format", foreign)
+
+ def admin(self):
+ return {"httpStatus": 200, "response": {"info": {"servers": [{"drives": [
+ {"pool_index": 0, "set_index": 0, "disk_index": index, "endpoint": "endpoint-" + str(index),
+ "uuid": self.original_members[index] if index else "", "state": "ok" if index else "unformatted disk",
+ "runtimeState": "online" if index else "offline"} for index in range(4)]}]}}}
+
+ def test_structured_refusal_requires_exact_slot_and_each_other_original_member(self):
+ target = {"setSlot": 0, "diskUUID": self.original_members[0],
+ "cohort": [{"setSlot": index, "diskUUID": value} for index, value in enumerate(self.original_members)]}
+ value = self.admin()
+ self.assertFalse(unformatted.refusal(value, target, "endpoint-0", None)["unreadyMemberHealthyClaimed"])
+ for field, changed in (("state", "ok"), ("state", "unknown"), ("runtimeState", "online"), ("endpoint", "other"), ("uuid", "invented")):
+ bad = self.admin(); bad["response"]["info"]["servers"][0]["drives"][0][field] = changed
+ with self.subTest(field=field), self.assertRaises(ValueError): unformatted.refusal(bad, target, "endpoint-0", None)
+ bad = self.admin(); bad["response"]["info"]["servers"][0]["drives"][2]["uuid"] = self.foreign_members[2]
+ with self.assertRaises(ValueError): unformatted.refusal(bad, target, "endpoint-0", None)
+
+ def archive(self, root):
+ state = root / "state"; state.mkdir()
+ drive = root / "drive"; (drive / "quarantine" / "old-disk" / ".rustfs.sys").mkdir(parents=True)
+ data = drive / "data"; data.mkdir()
+ backup = drive / "quarantine" / "old-disk"
+ (backup / ".rustfs.sys" / "format.json").write_text(json.dumps(self.format()))
+ value = {"disk": 0, "dataPath": str(data), "backupPath": str(backup), "originalStillAtDataRoot": False,
+ "originalArchiveUnchanged": True, "original": {"kind": "original"}}
+ return state, drive, data, backup, value
+
+ def test_import_copies_released_donor_bytes_and_rejects_original_or_shared_members(self):
+ with tempfile.TemporaryDirectory() as folder:
+ state, drive, data, backup, value = self.archive(Path(folder))
+ with patch.object(replace_drive, "attest", return_value=value):
+ for donor in (self.format(), {**self.format(True), "xl": self.format()["xl"]},
+ {**self.format(True), "version": "future"}):
+ raw = json.dumps(donor).encode()
+ with self.assertRaises(ValueError): replace_drive.install_foreign(state, [drive], base64.b64encode(raw).decode(), hashlib.sha256(raw).hexdigest())
+ self.assertFalse(list(data.iterdir()))
+ raw = b' ' + json.dumps(self.format(True), indent=2).encode() + b'\n'
+ proof = replace_drive.install_foreign(state, [drive], base64.b64encode(raw).decode(), hashlib.sha256(raw).hexdigest())
+ self.assertEqual((data / ".rustfs.sys/format.json").read_bytes(), raw)
+ self.assertFalse(proof["syntheticFormatOrUUIDUsed"])
+ self.assertEqual(json.loads((backup / ".rustfs.sys/format.json").read_text()), self.format())
+ with self.assertRaises(ValueError): replace_drive.install_foreign(state, [drive], base64.b64encode(raw).decode(), hashlib.sha256(raw).hexdigest())
+
+ def test_negative_recovery_preserves_rejected_scene_and_cannot_claim_reconstruction(self):
+ with tempfile.TemporaryDirectory() as folder:
+ state, drive, data, backup, value = self.archive(Path(folder))
+ (state / "disk-replacement.json").write_text("intent")
+ (data / "foreign.bin").write_bytes(b"exact-rejected-scene")
+ def tree(path):
+ return {"kind": "rejected" if (path / "foreign.bin").exists() else "original"}
+ with patch.object(replace_drive, "attest", return_value=value), patch.object(replace_drive, "tree_identity", side_effect=tree):
+ proof = replace_drive.restore_original(state, [drive])
+ self.assertEqual(json.loads((data / ".rustfs.sys/format.json").read_text()), self.format())
+ self.assertEqual((drive / "quarantine/rejected-root/foreign.bin").read_bytes(), b"exact-rejected-scene")
+ self.assertFalse(proof["productRepairClaimed"])
+ self.assertTrue(proof["backupRestorationUsed"])
+ self.assertFalse((state / "disk-replacement.json").exists())
+ self.assertFalse(backup.exists())
+
+ def test_both_source_branches_have_an_automatic_entry(self):
+ self.assertEqual(select("B04")[0]["variants"], list(unformatted.BRANCHES))
+ self.assertIn(unformatted.CASE_ID, AUTOMATIC_CASES)
+
+ def test_second_admission_block_is_blocked_and_does_not_seed_fixture(self):
+ class BlockedFleet:
+ instances = []
+ def __init__(self, *args, **kwargs):
+ self.reserved = []
+ self.records = []
+ self.instances.append(self)
+ def record(self, label, value):
+ self.records.append((label, value))
+ return value
+ def deploy(self, *args, **kwargs):
+ raise unformatted.ReplacementAdmissionBlocked({
+ "vm000": {"admitted": False, "mutated": False, "readOnly": True}})
+ def cleanup(self): return True
+ with tempfile.TemporaryDirectory() as folder:
+ root = Path(folder)
+ candidate = root / "candidate.json"
+ candidate.write_text(json.dumps({"source_sha": "072b57b37559fbe626aeda8abf61617fdcbdd926",
+ "package_sha256": "b" * 64, "package_url": "https://example.invalid/rustfs.deb"}))
+ evidence = root / "evidence"
+ evidence.mkdir()
+ rc = root / "rc"
+ rc.write_bytes(b"rc")
+ args = SimpleNamespace(evidence=evidence, candidate=candidate, binary_sha="c" * 64,
+ testing_commit="d" * 40, tool_directory=root, rc_bin=rc)
+ with patch.dict("os.environ", {"GITHUB_RUN_ID": "12345", "GITHUB_RUN_ATTEMPT": "1"}), \
+ patch.object(unformatted, "Fleet", BlockedFleet), \
+ patch.object(unformatted, "verify_inspector", return_value={"verified": True}):
+ result = unformatted.run_branch(args, "H1", ["vm000"], "blank-unformatted", None)
+ self.assertEqual(result["verdict"], "BLOCKED")
+ self.assertTrue(result["replacementAdmissionBlocked"])
+ self.assertTrue(result["deploymentSkipped"])
+ self.assertTrue(result["fixtureSeedSkipped"])
+ self.assertEqual(result["cleanup"], "PASS")
+ self.assertEqual(result["admissionEvidence"]["vm000"]["admitted"], False)
+ self.assertEqual(len(BlockedFleet.instances), 1)
+ self.assertEqual(BlockedFleet.instances[0].reserved, [])
+ self.assertFalse((evidence / "H1-blank-unformatted" / "fixture").exists())
+
+
+if __name__ == "__main__": unittest.main()
diff --git a/test_validation_heal_version_race_adapter.py b/test_validation_heal_version_race_adapter.py
new file mode 100644
index 00000000..0db47fd4
--- /dev/null
+++ b/test_validation_heal_version_race_adapter.py
@@ -0,0 +1,148 @@
+"""Real H1/H2 oracle adaptation, source metadata fault shape and negative probes."""
+import copy
+import hashlib
+from pathlib import Path
+from types import SimpleNamespace
+import tempfile
+import unittest
+from unittest.mock import Mock,patch
+
+import test_validation_heal_version_race_physical as fixtures
+from scripts.validation_heal import version_race_adapter as adapter,version_race_physical as oracle
+from scripts.validation_heal import version_race_fixture as model,version_race_runner as runner,race
+from scripts.validation_heal.heal_workload import Recorder
+
+
+def fleet_scene(entries,width=4):
+ snapshot,metadata=fixtures.physical(entries)
+ slots=[str(i) for i in range(width)];distribution=list(range(2,width+1))+[1]
+ result={f'node{n}':{'formats':[],'files':[]} for n in range(1,2 if width==4 else 5)}
+ for slot in range(width):
+ node=f'node{slot%4+1}' if width==16 else 'node1';disk=slot//4 if width==16 else slot
+ value=result[node]
+ value['formats'].append({'disk':disk,'value':{'format':'xl','id':'pool','xl':{'this':slots[slot],'sets':[slots]}}})
+ for row in snapshot['files']:
+ if row['disk']!=0:continue
+ current={**row,'disk':disk};current['sha256']=hashlib.sha256((row['sha256']+str(slot)).encode()).hexdigest()
+ if row['path'].endswith('/xl.meta'):
+ key=row['path'][len(fixtures.BUCKET)+1:].removesuffix('/xl.meta')
+ decoded=copy.deepcopy(next(v['decoded'] for v in metadata['rows'] if v['disk']==0 and v['key']==key))
+ for version in decoded['versions']:
+ if not version['deleted']:
+ version['erasure'].update(index=distribution[slot],distribution=distribution,
+ data_blocks=2 if width==4 else 12,parity_blocks=2 if width==4 else 4)
+ current['metadata']=decoded
+ value['files'].append(current)
+ return result
+
+
+class VersionAdapterTests(unittest.TestCase):
+ def test_h1_and_interleaved_h2_map_real_set_slots_and_all_coding_indices(self):
+ entries=[fixtures.entry(1,latest=True)]
+ for width in (4,16):
+ with self.subTest(width=width):
+ scene=fleet_scene(entries,width)
+ normalized,metadata=adapter.view(scene,fixtures.BUCKET,[fixtures.KEY],fixtures.INSPECTOR_SHA)
+ proof=oracle.key_state(normalized,metadata,fixtures.BUCKET,fixtures.KEY,entries,
+ inspector_sha=fixtures.INSPECTOR_SHA,disk_count=width)
+ self.assertEqual(set(proof['versions'][fixtures.uid(1)]['coding']),set(range(1,width+1)))
+ self.assertTrue(proof['allLiveVersionsFullSet'])
+ self.assertEqual(proof['allLiveVersionsFourShards'],width==4)
+ if width==16:
+ self.assertEqual([row['disk'] for row in metadata['rows'][:4]],[0,4,8,12])
+
+ def test_h2_missing_peer_geometry_duplicate_slot_and_wrong_inspector_are_refused(self):
+ entries=[fixtures.entry(1,latest=True)]
+ for fault in ('peer','slot','geometry','inspector','unknown-key','decode'):
+ with self.subTest(fault=fault):
+ scene=fleet_scene(entries,16)
+ if fault=='peer':scene.pop('node4')
+ elif fault=='slot':scene['node1']['formats'][1]['value']['xl']['this']='0'
+ elif fault=='unknown-key':scene['node1']['files'][0]['path']=fixtures.BUCKET+'/foreign/xl.meta'
+ elif fault=='decode':next(v for v in scene['node1']['files'] if 'metadata' in v).pop('metadata')
+ elif fault=='geometry':next(v for v in scene['node1']['files'] if 'metadata' in v)['metadata']['versions'][0]['erasure']['data_blocks']=8
+ with self.assertRaises(ValueError):
+ normalized,metadata=adapter.view(scene,fixtures.BUCKET,[fixtures.KEY],fixtures.INSPECTOR_SHA)
+ oracle.key_state(normalized,metadata,fixtures.BUCKET,fixtures.KEY,entries,
+ inspector_sha='b'*64 if fault=='inspector' else fixtures.INSPECTOR_SHA,disk_count=16)
+
+ def test_complete_g04_fault_plan_contains_257_metadata_members_on_original_fourth_drive(self):
+ keys={f'objects-{i:04d}.bin' for i in range(257)}
+ entries=[fixtures.entry(i+1,key=key,latest=True) for i,key in enumerate(sorted(keys))]
+ for width in (4,16):
+ scene=fleet_scene(entries,width)
+ ledger=SimpleNamespace(bucket=fixtures.BUCKET,bound=True,poisoned=None,keys=keys,entries=lambda:entries)
+ targets=adapter.metadata_targets(scene,fixtures.BUCKET,ledger,'create-marker',fixtures.INSPECTOR_SHA)
+ self.assertEqual(len(targets),257)
+ self.assertEqual({(v['node'],v['disk']) for v in targets},{('node1',3)})
+ self.assertTrue(all(v['relativePath'].endswith('/xl.meta') for v in targets))
+ self.assertEqual(targets[-1]['key'],model.TARGET)
+ wrong=copy.deepcopy(scene);wrong['node1']['files'].pop()
+ with self.assertRaises((ValueError,KeyError)):
+ adapter.metadata_targets(wrong,fixtures.BUCKET,ledger,'create-marker',fixtures.INSPECTOR_SHA)
+
+ def test_metadata_reconstruction_changes_only_one_metadata_file(self):
+ entries=[fixtures.entry(1,latest=True)];before=fleet_scene(entries)
+ row=next(v for v in before['node1']['files'] if v['disk']==3 and 'metadata' in v)
+ target={'node':'node1','disk':3,'relativePath':row['path'],'key':fixtures.KEY}
+ after=copy.deepcopy(before);selected=next(v for v in after['node1']['files'] if v['disk']==3 and 'metadata' in v)
+ selected['sha256']='f'*64
+ self.assertTrue(adapter.reconstruction(before,after,fixtures.BUCKET,target)['allDataShardsUnchanged'])
+ for fault in ('data','other-meta','missing','format'):
+ wrong=copy.deepcopy(after)
+ if fault=='format':wrong['node1']['formats'][0]['sha256']='changed'
+ elif fault=='missing':wrong['node1']['files'].remove(next(v for v in wrong['node1']['files'] if v['disk']==3 and 'metadata' in v))
+ else:next(v for v in wrong['node1']['files'] if ('metadata' not in v if fault=='data' else v['disk']==0 and 'metadata' in v))['sha256']='e'*64
+ with self.subTest(fault=fault),self.assertRaises(ValueError):adapter.reconstruction(before,wrong,fixtures.BUCKET,target)
+
+ def test_source_partial_window_requires_repaired_control_32_pending_and_missing_selected_metadata(self):
+ keys=sorted({f'objects-{i:04d}.bin' for i in range(257)})
+ entries=[fixtures.entry(i+1,key=key,latest=True) for i,key in enumerate(keys)]
+ before=fleet_scene(entries);snapshot=copy.deepcopy(before)
+ targets=[{'node':'node1','disk':3,'key':key,'relativePath':f'{fixtures.BUCKET}/{key}/xl.meta'} for key in keys]
+ snapshot['node1']['files']=[row for row in snapshot['node1']['files'] if not
+ (row['disk']==3 and row['path'].endswith('/xl.meta') and row['path']!=targets[0]['relativePath'])]
+ fleet=SimpleNamespace(record=lambda label,value:value,snapshot=lambda label:snapshot)
+ status={'httpStatus':200,'response':{'summary':'running','settings':race.MARKER_REFERENCE['effective'],
+ 'outcome':{'counters':{'processed':1}}}}
+ with patch.object(runner.race,'observe',return_value=status):
+ self.assertEqual(runner.partial(None,fleet,'path','token',race.MARKER_REFERENCE,before,targets,fixtures.BUCKET),snapshot)
+ fleet.snapshot=lambda label:before
+ with self.assertRaises(race.WindowNotObserved):runner.partial(None,fleet,'path','token',race.MARKER_REFERENCE,before,targets,fixtures.BUCKET)
+
+ def test_h2_deleted_generation_is_not_live_but_its_unreferenced_bytes_are_reported(self):
+ before_entries=[fixtures.entry(1),fixtures.entry(2,latest=True)]
+ after_entries=[fixtures.entry(1,latest=True)]
+ before=fleet_scene(before_entries,16);after=fleet_scene(after_entries,16)
+ # Retained bytes stay original even when metadata latest is promoted.
+ for node,value in after.items():
+ for row in value['files']:
+ if row['path'].endswith('/part.1'):
+ row['sha256']=next(v['sha256'] for v in before[node]['files'] if v['disk']==row['disk'] and v['path']==row['path'])
+ value['files'] += [copy.deepcopy(v) for v in before[node]['files'] if fixtures.uid(1002) in v['path']]
+ proof=adapter.transition(before,after,fixtures.BUCKET,before_entries,after_entries,fixtures.INSPECTOR_SHA)
+ key=proof['keys'][fixtures.KEY]
+ self.assertEqual(key['deletedVersions'],[fixtures.uid(2)])
+ self.assertEqual(len(key['unreferencedDeletedGenerationFiles']),16)
+ self.assertFalse(key['garbageCollectorResponsibilityProved'])
+ bad=copy.deepcopy(after)
+ next(v for v in bad['node4']['files'] if 'metadata' in v)['metadata']['versions'].append(
+ copy.deepcopy(next(v for v in before['node4']['files'] if 'metadata' in v)['metadata']['versions'][-1]))
+ with self.assertRaises(ValueError):adapter.transition(before,bad,fixtures.BUCKET,before_entries,after_entries,fixtures.INSPECTOR_SHA)
+
+ def test_negative_diagnostics_drain_read_failures_but_stop_on_evidence_io(self):
+ expected=[{'params':{'Bucket':fixtures.BUCKET,'Key':fixtures.KEY,'VersionId':fixtures.uid(i)},
+ 'httpStatus':404,'errorCode':'NoSuchVersion'} for i in (1,2)]
+ with tempfile.TemporaryDirectory() as tmp:
+ recorder=SimpleNamespace(output=Path(tmp),call=Mock(side_effect=[ValueError('wrong original 403'),
+ {'ResponseMetadata':{'HTTPStatusCode':404},'Error':{'Code':'NoSuchVersion'}}]))
+ proof=runner.negative_checks(recorder,expected)
+ self.assertEqual([v['state'] for v in proof['results']],['FAIL','PASS'])
+ recorder.call.reset_mock()
+ recorder.call.side_effect=ValueError('wrong original 403')
+ with patch.object(runner,'save',side_effect=OSError('fsync failed')):
+ with self.assertRaises(OSError):runner.negative_checks(recorder,expected)
+ self.assertEqual(recorder.call.call_count,1)
+
+
+if __name__=='__main__':unittest.main()
diff --git a/test_validation_heal_version_race_fixture.py b/test_validation_heal_version_race_fixture.py
new file mode 100644
index 00000000..8b3530fe
--- /dev/null
+++ b/test_validation_heal_version_race_fixture.py
@@ -0,0 +1,247 @@
+import copy
+import hashlib
+import json
+from pathlib import Path
+import tempfile
+from types import SimpleNamespace
+import unittest
+from unittest.mock import Mock,patch
+
+from scripts.validation_heal import version_race_fixture as f
+from scripts.validation_heal.heal_workload import Recorder,RecordedCallError
+from test_validation_heal_status_keepalive import binding as transport_binding
+
+BUCKET='rv-heal-test-001'
+
+
+def binding():
+ return {**transport_binding(), 'bucketPrefix': BUCKET}
+
+
+def ack(code=200,**values):return {'ResponseMetadata':{'HTTPStatusCode':code},**values}
+
+
+class SeedRecorder:
+ def __init__(self,versioned=True):
+ self.probe=SimpleNamespace(binding={'bucketPrefix':BUCKET});self.calls=[];self.index=0;self.versioned=versioned
+ def call(self,operation,params,body=None,**kwargs):
+ self.calls.append((operation,copy.deepcopy(params)));self.index+=1
+ if operation=='put_object':return ack(VersionId=f'v-{self.index}' if self.versioned else 'null',ETag='"'+hashlib.md5(body).hexdigest()+'"')
+ if operation=='delete_object':return ack(204,VersionId=f'm-{self.index}',DeleteMarker=True)
+ return ack()
+
+
+def fixture(branch='new-version-vs-historical-heal'):
+ recorder=SeedRecorder(branch!='unversioned-delete');ledger=f.seed_fixture(recorder,BUCKET,branch,controls=3,size=17)
+ inventory={'bucket':BUCKET,'entries':ledger.entries()}
+ for row in inventory['entries']:
+ if not row['deleteMarker']:row['properties']['StorageClass']='STANDARD'
+ ledger.bind_baseline(inventory)
+ return recorder,ledger
+
+
+class VersionFixtureTests(unittest.TestCase):
+ def test_full_g04_population_uses_real_durable_recorder_and_four_bound_batch_receipts(self):
+ for branch in f.BRANCHES[4:]:
+ with self.subTest(branch=branch),tempfile.TemporaryDirectory() as tmp:
+ sdk=SeedRecorder(branch!='unversioned-delete')
+ def invoke(operation,params,body=None):
+ if operation=='delete_objects':response=ack(Deleted=copy.deepcopy(params['Delete']['Objects']))
+ elif operation=='delete_object' and ('VersionId' in params or branch=='unversioned-delete'):
+ response=ack(204,**({'VersionId':params['VersionId'],'DeleteMarker':True} if 'VersionId' in params else {}))
+ else:response=sdk.call(operation,params,body)
+ return {'acknowledged':True,'resultUnknown':False,'operation':operation,
+ 'params':copy.deepcopy(params),'response':response}
+ recorder=Recorder(SimpleNamespace(binding=binding(),s3=invoke),Path(tmp)/'journal')
+ ledger=f.seed_fixture(recorder,BUCKET,branch,size=17)
+ self.assertEqual(ledger.keys,{f'objects-{i:04d}.bin' for i in range(257)})
+ self.assertEqual(len(ledger.records),257 if branch=='unversioned-delete' else 260 if branch in ('delete-marker','batch-version-delete') else 259)
+ inventory={'bucket':BUCKET,'entries':ledger.entries()};ledger.bind_baseline(inventory)
+ order=ledger.history[f.TARGET][:];plan=f.mutation_plan(ledger,branch,size=17)
+ f.submit(recorder,ledger,plan[0])
+ ledger.verify_inventory({'bucket':BUCKET,'entries':ledger.entries()})
+ if branch=='batch-version-delete':
+ self.assertEqual(ledger.history[f.TARGET],[order[1]])
+ self.assertEqual(ledger.history['objects-0002.bin'],[])
+ self.assertEqual(len(ledger.negative_reads()),4)
+ receipt=json.loads((recorder.output/f'{recorder.sequence:06d}-ack.json').read_text())
+ self.assertEqual([v['state'] for v in receipt['batchDelete']['items']],['ACK']*4)
+ self.assertEqual(len(list(recorder.output.glob('*-request.json'))),recorder.sequence)
+ self.assertEqual(len(list(recorder.output.glob('*-ack.json'))),recorder.sequence)
+
+ def test_all_eight_branches_have_exact_bounded_fixtures_and_plans(self):
+ for branch in f.BRANCHES:
+ recorder,ledger=fixture(branch);plan=f.mutation_plan(ledger,branch,size=17)
+ self.assertTrue(plan);self.assertFalse(ledger.poisoned)
+ puts=[v for v in recorder.calls if v[0]=='put_object']
+ self.assertEqual(len(puts),4 if branch=='unversioned-delete' else 6)
+ self.assertEqual(sum(v[0]=='put_bucket_versioning' for v in recorder.calls),branch!='unversioned-delete')
+ self.assertTrue(all(v['params']['Bucket']==BUCKET for v in plan))
+
+ def test_controls_reference_new_payloads_and_real_version_ack_ids(self):
+ _,ledger=fixture();plan=f.mutation_plan(ledger,f.BRANCHES[0],size=17)
+ self.assertEqual(len(plan),3);self.assertEqual([v['referenceKey'] for v in plan],[f'objects-{i:04d}.bin' for i in range(3)])
+ before=ledger.entries()
+ for i,step in enumerate(plan):
+ body=f.payload(step['nonce'],step['size']);ledger.apply(step['operation'],step['params'],ack(VersionId=f'new-{i}',ETag=f'etag-{i}'),body)
+ after=ledger.entries();self.assertEqual(len(after),len(before)+3)
+ target=[v for v in after if v['key']==f.TARGET]
+ self.assertEqual([v['versionId'] for v in target if v['isLatest']],['new-2'])
+ self.assertTrue(all(v['properties']['StorageClass']=='STANDARD' for v in target))
+ self.assertTrue(all('VersionId' in v['params'] for v in ledger.exact_reads()))
+ with self.assertRaises(ValueError):f.mutation_plan(ledger,f.BRANCHES[0],size=17)
+
+ def test_reference_size_or_unbound_fixture_cannot_produce_race_plan(self):
+ _,ledger=fixture()
+ with self.assertRaisesRegex(ValueError,'reference payload'):f.mutation_plan(ledger,f.BRANCHES[0],size=19)
+ ledger.bound=False
+ with self.assertRaisesRegex(ValueError,'bound healthy'):f.mutation_plan(ledger,f.BRANCHES[0],size=17)
+
+ def test_latest_is_serial_ack_order_not_lexical_id_or_timestamp(self):
+ _,ledger=fixture();plan=f.mutation_plan(ledger,f.BRANCHES[0],size=17)
+ for vid,step in zip(('zzz','aaa','bbb'),plan):
+ ledger.apply('put_object',step['params'],ack(VersionId=vid,ETag='e'),f.payload(step['nonce'],17))
+ self.assertEqual([r['versionId'] for r in ledger.entries() if r['key']==f.TARGET and r['isLatest']],['bbb'])
+
+ def test_delete_current_exposes_previous_version_without_resurrection(self):
+ _,ledger=fixture('delete-current-version-vs-deep');original=ledger.history[f.TARGET][:]
+ step=f.mutation_plan(ledger,'delete-current-version-vs-deep')[0]
+ self.assertEqual(step['params']['VersionId'],original[-1])
+ ledger.apply(step['operation'],step['params'],ack(204,VersionId=original[-1]))
+ self.assertEqual([r['versionId'] for r in ledger.entries() if r['key']==f.TARGET and r['isLatest']],[original[-2]])
+ self.assertEqual(ledger.negative_reads(),[dict(params={'Bucket':BUCKET,'Key':f.TARGET,'VersionId':original[-1]},httpStatus=404,errorCode='NoSuchVersion')])
+
+ def test_delete_old_does_not_create_marker_or_change_latest(self):
+ _,ledger=fixture('delete-old-version-vs-deep');original=ledger.history[f.TARGET][:]
+ step=f.mutation_plan(ledger,'delete-old-version-vs-deep')[0]
+ self.assertEqual(step['params']['VersionId'],original[0])
+ ledger.apply(step['operation'],step['params'],ack(204))
+ rows=[r for r in ledger.entries() if r['key']==f.TARGET]
+ self.assertTrue(all(not v['deleteMarker'] for v in rows));self.assertEqual([r['versionId'] for r in rows if r['isLatest']],[original[-1]])
+
+ def test_marker_create_and_exact_marker_delete_have_different_expectations(self):
+ _,ledger=fixture('create-marker');step=f.mutation_plan(ledger,'create-marker')[0]
+ ledger.apply(step['operation'],step['params'],ack(204,VersionId='marker',DeleteMarker=True))
+ latest=[r for r in ledger.entries() if r['key']==f.TARGET and r['isLatest']]
+ self.assertEqual(latest[0]['versionId'],'marker');self.assertTrue(latest[0]['deleteMarker'])
+ negatives=ledger.negative_reads();self.assertEqual([r['httpStatus'] for r in negatives],[405,404])
+ _,other=fixture('delete-marker');old=other.history[f.TARGET][:]
+ step=f.mutation_plan(other,'delete-marker')[0]
+ self.assertEqual(step['params']['VersionId'],old[-1])
+ other.apply(step['operation'],step['params'],ack(204,VersionId=old[-1],DeleteMarker=True))
+ self.assertEqual([r['versionId'] for r in other.entries() if r['key']==f.TARGET and r['isLatest']],[old[-2]])
+ self.assertFalse(any(r['deleteMarker'] for r in other.entries()))
+
+ def test_never_versioned_delete_is_not_marker_creation(self):
+ _,ledger=fixture('unversioned-delete');step=f.mutation_plan(ledger,'unversioned-delete')[0]
+ self.assertNotIn('VersionId',step['params']);ledger.apply(step['operation'],step['params'],ack(204))
+ self.assertFalse(any(v['key']==f.TARGET for v in ledger.entries()))
+ self.assertEqual(ledger.negative_reads()[0]['errorCode'],'NoSuchKey')
+ _,ledger=fixture('unversioned-delete')
+ with self.assertRaises(ValueError):ledger.apply('delete_object',step['params'],ack(204,DeleteMarker=True,VersionId='marker'))
+
+ def test_successful_batch_removes_only_frozen_key_version_pairs(self):
+ _,ledger=fixture('batch-version-delete');step=f.mutation_plan(ledger,'batch-version-delete')[0]
+ pairs=step['params']['Delete']['Objects'];self.assertEqual(len(pairs),4);self.assertIs(step['params']['Delete']['Quiet'],False)
+ middle=ledger.history[f.TARGET][1]
+ ledger.apply('delete_objects',step['params'],ack(Deleted=pairs))
+ self.assertEqual(ledger.history[f.TARGET],[middle]);self.assertEqual(ledger.history['objects-0002.bin'],[])
+ self.assertEqual(len(ledger.deleted),4);self.assertEqual(len(ledger.records),3)
+
+ def test_partial_batch_retains_known_effects_and_never_accepts_final_inventory(self):
+ _,ledger=fixture('batch-version-delete');step=f.mutation_plan(ledger,'batch-version-delete')[0];items=step['params']['Delete']['Objects']
+ response=ack(Deleted=[items[0]],Errors=[{**v,'Code':'InternalError'} for v in items[1:]])
+ with self.assertRaisesRegex(ValueError,'rejected/unknown'):ledger.apply('delete_objects',step['params'],response)
+ self.assertNotIn((items[0]['Key'],items[0]['VersionId']),ledger.records)
+ self.assertTrue(all((v['Key'],v['VersionId']) in ledger.records for v in items[1:]))
+ with self.assertRaisesRegex(ValueError,'unresolved'):ledger.verify_inventory({'bucket':BUCKET,'entries':ledger.entries()})
+
+ def test_duplicate_batch_receipt_is_unknown_not_two_deletes(self):
+ _,ledger=fixture('batch-version-delete');step=f.mutation_plan(ledger,'batch-version-delete')[0];items=step['params']['Delete']['Objects']
+ with self.assertRaises(ValueError):ledger.apply('delete_objects',step['params'],ack(Deleted=[items[0],items[0],*items[1:]]))
+ self.assertIn((items[0]['Key'],items[0]['VersionId']),ledger.records)
+ self.assertEqual(len(ledger.deleted),3)
+
+ def test_real_recorder_partial_exception_keeps_successful_batch_members(self):
+ _,ledger=fixture('batch-version-delete');step=f.mutation_plan(ledger,'batch-version-delete')[0];items=step['params']['Delete']['Objects']
+ observed={'acknowledged':True,'resultUnknown':False,'response':ack(Deleted=[items[0]],Errors=[{**v,'Code':'AccessDenied'} for v in items[1:]])}
+ with tempfile.TemporaryDirectory() as tmp:
+ probe=SimpleNamespace(binding=binding(),s3=Mock(return_value=observed));recorder=Recorder(probe,Path(tmp)/'ledger')
+ with self.assertRaises(RecordedCallError) as caught:f.submit(recorder,ledger,step)
+ self.assertEqual(caught.exception.observation,observed)
+ self.assertNotIn((items[0]['Key'],items[0]['VersionId']),ledger.records)
+ self.assertEqual(len(ledger.deleted),1);self.assertTrue(ledger.poisoned)
+ probe.s3.assert_called_once()
+
+ def test_ack_with_client_cleanup_error_is_retained_but_not_accepted(self):
+ _,ledger=fixture('delete-current-version-vs-deep');step=f.mutation_plan(ledger,'delete-current-version-vs-deep')[0]
+ observed={'acknowledged':True,'resultUnknown':False,'response':ack(204,VersionId=step['params']['VersionId']),
+ 'clientCleanupError':{'message':'close failed'}}
+ with tempfile.TemporaryDirectory() as tmp:
+ recorder=Recorder(SimpleNamespace(binding=binding(),s3=Mock(return_value=observed)),Path(tmp)/'ledger')
+ with self.assertRaises(RecordedCallError):f.submit(recorder,ledger,step)
+ self.assertNotIn((f.TARGET,step['params']['VersionId']),ledger.records)
+ self.assertEqual(ledger.events[-1]['state'],'COLLECTOR_ERROR')
+
+ def test_missing_or_reused_version_ack_cannot_hide_old_or_deleted_version(self):
+ for vid in (None,'null',''):
+ _,ledger=fixture();step=f.mutation_plan(ledger,f.BRANCHES[0],size=17)[0];before=len(ledger.records)
+ with self.assertRaises(ValueError):ledger.apply('put_object',step['params'],ack(VersionId=vid,ETag='e'),b'new')
+ self.assertEqual(len(ledger.records),before)
+ _,ledger=fixture('delete-current-version-vs-deep');step=f.mutation_plan(ledger,'delete-current-version-vs-deep')[0];vid=step['params']['VersionId']
+ ledger.apply('delete_object',step['params'],ack(204))
+ params=f.put_step(BUCKET,f.TARGET,'new',3)['params']
+ with self.assertRaisesRegex(ValueError,'reused'):ledger.apply('put_object',params,ack(VersionId=vid,ETag='e'),b'new')
+
+ def test_inventory_rejects_missing_historical_bytes_wrong_latest_and_metadata(self):
+ _,ledger=fixture();good={'bucket':BUCKET,'entries':ledger.entries()};ledger.verify_inventory(good)
+ for kind in ('missing','duplicate','latest','payload','metadata','tags','foreign'):
+ bad=copy.deepcopy(good)
+ if kind=='missing':bad['entries'].pop()
+ if kind=='duplicate':bad['entries'].append(bad['entries'][0])
+ if kind=='latest':bad['entries'][0]['isLatest']=not bad['entries'][0]['isLatest']
+ if kind=='payload':bad['entries'][0]['payload']['sha256']='0'*64
+ if kind=='metadata':bad['entries'][0]['properties']['Metadata']={}
+ if kind=='tags':bad['entries'][0]['tags']=[]
+ if kind=='foreign':bad['bucket']='other'
+ with self.subTest(kind=kind),self.assertRaises(ValueError):ledger.verify_inventory(bad)
+ with self.assertRaisesRegex(ValueError,'rebound'):ledger.bind_baseline(good)
+
+ def test_marker_rejection_cannot_be_replaced_by_access_denied_or_other_id(self):
+ expected=dict(httpStatus=405,errorCode='MethodNotAllowed',deleteMarker=True,versionId='marker')
+ response=ack(405,Error={'Code':'MethodNotAllowed'});response['ResponseMetadata']['HTTPHeaders']={'X-Amz-Delete-Marker':'true','X-Amz-Version-Id':'marker'}
+ f.check_negative(response,expected)
+ for field,value in (('X-Amz-Version-Id','other'),('X-Amz-Delete-Marker','false')):
+ bad=copy.deepcopy(response);bad['ResponseMetadata']['HTTPHeaders'][field]=value
+ with self.assertRaises(ValueError):f.check_negative(bad,expected)
+ with self.assertRaises(ValueError):f.check_negative(ack(403,Error={'Code':'AccessDenied'}),expected)
+
+ def test_unknown_never_disappears_after_successful_list(self):
+ _,ledger=fixture();step=f.mutation_plan(ledger,f.BRANCHES[0],size=17)[0]
+ recorder=Mock();recorder.call.side_effect=TimeoutError('unknown')
+ with self.assertRaises(TimeoutError):f.submit(recorder,ledger,step)
+ self.assertEqual(ledger.events[-1]['state'],'UNKNOWN')
+ with self.assertRaises(ValueError):ledger.verify_inventory({'bucket':BUCKET,'entries':ledger.entries()})
+ with self.assertRaises(ValueError):ledger.apply('put_object',step['params'],ack(VersionId='new',ETag='e'),b'new')
+
+ def test_mismatched_failed_receipt_poison_and_http_rejection_are_distinct(self):
+ for variant in ('foreign','rejected','missing-http'):
+ _,ledger=fixture();step=f.mutation_plan(ledger,f.BRANCHES[0],size=17)[0]
+ params=copy.deepcopy(step['params'])
+ if variant=='foreign':params['Key']='foreign'
+ observed={'acknowledged':False,'resultUnknown':False,
+ 'response':ack(403) if variant=='rejected' else None}
+ recorder=Mock();recorder.call.side_effect=RecordedCallError('failed','put_object',params,observed,'a'*64)
+ with self.assertRaises(ValueError):f.submit(recorder,ledger,step)
+ self.assertEqual(ledger.events[-1]['state'],'REJECTED' if variant=='rejected' else 'UNKNOWN')
+ with self.assertRaises(ValueError):ledger.verify_inventory({'bucket':BUCKET,'entries':ledger.entries()})
+
+ def test_seed_scope_and_budget_fail_before_any_call(self):
+ recorder=SeedRecorder()
+ for kwargs in ({'controls':2},{'size':64*1024**2},{'controls':True}):
+ with self.assertRaises(ValueError):f.seed_fixture(recorder,BUCKET,f.BRANCHES[0],**kwargs)
+ with self.assertRaises(ValueError):f.seed_fixture(recorder,'foreign-bucket',f.BRANCHES[0])
+ self.assertFalse(recorder.calls)
+
+
+if __name__=='__main__':unittest.main()
diff --git a/test_validation_heal_version_race_physical.py b/test_validation_heal_version_race_physical.py
new file mode 100644
index 00000000..d1a6a028
--- /dev/null
+++ b/test_validation_heal_version_race_physical.py
@@ -0,0 +1,233 @@
+import copy
+import hashlib
+import unittest
+import uuid
+from types import SimpleNamespace
+
+from functools import partial
+from scripts.validation_heal import version_race_physical as actual
+from scripts.validation_heal import version_race_fixture as fixture
+
+INSPECTOR_SHA='a'*64
+p=SimpleNamespace(INSPECTOR_SHA=INSPECTOR_SHA,
+ key_state=partial(actual.key_state,inspector_sha=INSPECTOR_SHA),
+ transition=partial(actual.transition,inspector_sha=INSPECTOR_SHA),
+ file_index=actual.file_index)
+
+BUCKET='rv-heal-version-physical'
+KEY='objects-0256.bin'
+CONTROL='objects-0000.bin'
+
+
+def uid(n):return str(uuid.UUID(int=n))
+
+
+def entry(n,*,key=KEY,latest=False,marker=False,payload='old'):
+ value=dict(key=key,versionId=uid(n),isLatest=latest,deleteMarker=marker)
+ if not marker:value['payload']={'size':263291,'sha256':hashlib.sha256(payload.encode()).hexdigest()}
+ return value
+
+
+def physical(entries):
+ snapshot={'formats':['unchanged'],'files':[]};metadata={'inspectorSha256':p.INSPECTOR_SHA,'rows':[]}
+ for key in sorted({v['key'] for v in entries}):
+ rows=[v for v in entries if v['key']==key]
+ for disk in range(4):
+ digest=hashlib.sha256(repr(rows).encode()).hexdigest()
+ snapshot['files'].append(dict(disk=disk,path=f'{BUCKET}/{key}/xl.meta',size=300,sha256=digest))
+ versions=[]
+ # Non-self-inverse permutation catches confusion between physical
+ # drive order and coding index order.
+ distribution=[3,1,4,2] if key==KEY else [1,2,3,4]
+ for row in rows:
+ version=dict(versionId=row['versionId'],isLatest=row['isLatest'],deleted=row['deleteMarker'],inline=None,
+ dataDir=None,size=0,parts=[],erasure={})
+ if not row['deleteMarker']:
+ directory=uid(uuid.UUID(row['versionId']).int+1000);index=distribution[disk]
+ version.update(dataDir=directory,size=row['payload']['size'],parts=[dict(number=1,size=row['payload']['size'])],
+ erasure=dict(index=index,data_blocks=2,parity_blocks=2,block_size=1048576,distribution=distribution))
+ digest=hashlib.sha256(f"{row['payload']['sha256']}/{index}".encode()).hexdigest()
+ snapshot['files'].append(dict(disk=disk,path=f'{BUCKET}/{key}/{directory}/part.1',size=131681,sha256=digest))
+ versions.append(version)
+ meta=next(v for v in snapshot['files'] if v['disk']==disk and v['path']==f'{BUCKET}/{key}/xl.meta')
+ metadata['rows'].append(dict(disk=disk,key=key,metadataSha256=meta['sha256'],
+ decoded=dict(bucket=BUCKET,key=key,versions=versions)))
+ return snapshot,metadata
+
+
+class VersionPhysicalTests(unittest.TestCase):
+ def test_auto_testing_inspector_identity_is_mandatory_and_not_inferred_from_observation(self):
+ entries=[entry(1,latest=True)];snapshot,metadata=physical(entries)
+ with self.assertRaises(TypeError):actual.key_state(snapshot,metadata,BUCKET,KEY,entries)
+ for digest in ('b'*64,'',True,None):
+ with self.subTest(digest=digest),self.assertRaises(ValueError):
+ actual.key_state(snapshot,metadata,BUCKET,KEY,entries,inspector_sha=digest)
+ bad=copy.deepcopy(metadata);bad['inspectorSha256']='b'*64
+ with self.assertRaises(ValueError):p.key_state(snapshot,bad,BUCKET,KEY,entries)
+
+
+
+
+ def test_each_historical_version_requires_all_four_coding_indices(self):
+ entries=[entry(1),entry(2,latest=True)];snapshot,metadata=physical(entries)
+ proof=p.key_state(snapshot,metadata,BUCKET,KEY,entries)
+ self.assertEqual(set(proof['versions']),{uid(1),uid(2)})
+ self.assertEqual(set(proof['versions'][uid(1)]['coding']),{1,2,3,4})
+ self.assertFalse(proof['casePassClaimed'])
+ snapshot['files']=[v for v in snapshot['files'] if not (v['disk']==3 and uid(1001) in v['path'])]
+ with self.assertRaisesRegex(ValueError,'lacks a physical shard'):p.key_state(snapshot,metadata,BUCKET,KEY,entries)
+
+ def test_exact_marker_population_and_current_flags_on_every_drive(self):
+ entries=[entry(1),entry(2,latest=True,marker=True)];snapshot,metadata=physical(entries)
+ p.key_state(snapshot,metadata,BUCKET,KEY,entries)
+ for kind in ('missing-marker','resurrect','latest','marker-data','duplicate','different-key','missing-decode'):
+ bad=copy.deepcopy(metadata);rows=bad['rows'][0]['decoded']['versions']
+ if kind=='missing-marker':rows.pop()
+ if kind=='resurrect':rows[1]['deleted']=False
+ if kind=='latest':rows[0]['isLatest']=True
+ if kind=='marker-data':rows[1]['dataDir']=uid(999)
+ if kind=='duplicate':rows.append(copy.deepcopy(rows[0]))
+ if kind=='different-key':bad['rows'][0]['decoded']['key']='other'
+ if kind=='missing-decode':bad['rows'].pop()
+ with self.subTest(kind=kind),self.assertRaises(ValueError):p.key_state(snapshot,bad,BUCKET,KEY,entries)
+
+ def test_metadata_sha_and_geometry_are_not_optional(self):
+ entries=[entry(1,latest=True)];snapshot,metadata=physical(entries)
+ for kind in ('sha','inspector','index','distribution','bool-index','inline','parts','generation','size','bool-disk'):
+ bad=copy.deepcopy(metadata);v=bad['rows'][0]['decoded']['versions'][0]
+ if kind=='sha':bad['rows'][0]['metadataSha256']='0'*64
+ if kind=='inspector':bad['inspectorSha256']='0'*64
+ if kind=='index':v['erasure']['index']=1
+ if kind=='distribution':v['erasure']['distribution']=[3,1,1,2]
+ if kind=='bool-index':v['erasure']['index']=True
+ if kind=='inline':v['inline']={'length':123}
+ if kind=='parts':v['parts'][0]['size']=1
+ if kind=='generation':v['dataDir']=uid(999)
+ if kind=='size':v['size']=1
+ if kind=='bool-disk':bad['rows'][0]['disk']=False
+ with self.subTest(kind=kind),self.assertRaises(ValueError):p.key_state(snapshot,bad,BUCKET,KEY,entries)
+
+ def test_deleted_old_version_never_returns_but_gc_bytes_are_separate(self):
+ old=[entry(1),entry(2,latest=True)];new=[entry(2,latest=True)]
+ before,bm=physical(old);after,am=physical(new)
+ orphan=next(v for v in before['files'] if uid(1001) in v['path']);after['files'].append(orphan)
+ proof=p.transition(before,after,bm,am,BUCKET,KEY,old,new)
+ self.assertEqual(proof['deletedVersions'],[uid(1)]);self.assertEqual(proof['unreferencedDeletedGenerationFiles'],[orphan])
+ self.assertFalse(proof['deletedGenerationFilesAbsent']);self.assertFalse(proof['publicApiDeletionProved'])
+ self.assertFalse(proof['garbageCollectorResponsibilityProved'])
+ with self.assertRaises(ValueError):p.transition(before,before,bm,bm,BUCKET,KEY,old,new)
+
+ def test_deleted_current_exposes_previous_and_marker_creation_preserves_bytes(self):
+ cases=[([entry(1),entry(2,latest=True)],[entry(1,latest=True)]),
+ ([entry(1,latest=True)],[entry(1),entry(2,latest=True,marker=True)]),
+ ([entry(1),entry(2,latest=True,marker=True)],[entry(1,latest=True)])]
+ for old,new in cases:
+ before,bm=physical(old);after,am=physical(new)
+ proof=p.transition(before,after,bm,am,BUCKET,KEY,old,new)
+ self.assertEqual(proof['retainedVersions'],[uid(1)])
+
+ def test_unversioned_delete_requires_four_absence_observations(self):
+ old=[entry(1,latest=True)];before,bm=physical(old)
+ # Candidate uses nil UUID for an unversioned metadata entry.
+ old[0]['versionId']='null'
+ for row in bm['rows']:row['decoded']['versions'][0]['versionId']=None
+ am={'inspectorSha256':p.INSPECTOR_SHA,'rows':[dict(disk=d,key=KEY,absent=True) for d in range(4)]}
+ proof=p.transition(before,{'formats':before['formats'],'files':[]},bm,am,BUCKET,KEY,old,[])
+ self.assertEqual(proof['deletedVersions'],['null'])
+ with self.assertRaises(ValueError):p.key_state(before,am,BUCKET,KEY,[])
+ with self.assertRaises(ValueError):p.key_state({'files':[]},am,BUCKET,KEY,old)
+
+ def test_new_version_matches_reference_by_coding_index_not_physical_disk(self):
+ old=[entry(1,latest=True)];ref=entry(9,key=CONTROL,latest=True,payload='new')
+ new=[entry(1),entry(2,latest=True,payload='new')]
+ before,bm=physical(old+[ref]);after,am=physical(new)
+ refs={uid(2):dict(key=CONTROL,versionId=uid(9),entry=ref)}
+ proof=p.transition(before,after,bm,am,BUCKET,KEY,old,new,references=refs)
+ self.assertEqual(proof['createdDataVersions'],[uid(2)])
+ for kind in ('none','unknown','wrong-bytes','same-key','wrong-payload'):
+ bad=copy.deepcopy(after);badrefs=copy.deepcopy(refs)
+ if kind=='none':badrefs={}
+ if kind=='unknown':badrefs[uid(3)]=badrefs[uid(2)]
+ if kind=='wrong-bytes':next(v for v in bad['files'] if uid(1002) in v['path'])['sha256']='0'*64
+ if kind=='same-key':badrefs[uid(2)]['key']=KEY
+ if kind=='wrong-payload':badrefs[uid(2)]['entry']['payload']['size']=1
+ with self.subTest(kind=kind),self.assertRaises(ValueError):p.transition(before,bad,bm,am,BUCKET,KEY,old,new,references=badrefs)
+
+ def test_retained_corruption_and_unbound_physical_files_fail(self):
+ entries=[entry(1,latest=True)];before,bm=physical(entries)
+ for kind in ('duplicate','foreign-generation','bad-old-shard','bad-format'):
+ after=copy.deepcopy(before)
+ if kind=='duplicate':after['files'].append(copy.deepcopy(after['files'][0]))
+ if kind=='foreign-generation':after['files'].append(dict(disk=0,path=f'{BUCKET}/{KEY}/{uid(8000)}/part.1',size=1,sha256='a'*64))
+ if kind=='bad-old-shard':after['files'][1]['sha256']='0'*64
+ if kind=='bad-format':after['formats']=[]
+ with self.subTest(kind=kind),self.assertRaises(ValueError):p.transition(before,after,bm,bm,BUCKET,KEY,entries,entries)
+
+ def test_repair_backup_only_accepts_exact_original_metadata_bytes(self):
+ entries=[entry(1,latest=True)];before,bm=physical(entries);after=copy.deepcopy(before)
+ original=next(v for v in before['files'] if v['disk']==3 and v['path'].endswith('/xl.meta'))
+ backup={**original,'path':f'{BUCKET}/{KEY}/{uid(1001)}/xl.meta.bkp'};after['files'].append(backup)
+ proof=p.transition(before,after,bm,bm,BUCKET,KEY,entries,entries)
+ self.assertEqual(proof['verifiedMetadataBackups'],[backup])
+ for bad in ({**backup,'sha256':'0'*64},{**backup,'path':f'{BUCKET}/{KEY}/{uid(9000)}/xl.meta.bkp'}):
+ after['files'][-1]=bad
+ with self.assertRaises(ValueError):p.transition(before,after,bm,bm,BUCKET,KEY,entries,entries)
+
+ def test_additive_backup_accepts_each_ack_prefix_not_only_original_or_final_bytes(self):
+ old=[entry(1,latest=True)];new=[entry(1),entry(2,payload='new'),entry(3,latest=True,payload='new')]
+ ref=entry(9,key=CONTROL,latest=True,payload='new');before,bm=physical(old+[ref]);after,am=physical(new)
+ refs={uid(i):dict(key=CONTROL,versionId=uid(9),entry=ref) for i in (2,3)}
+ for count in range(3):
+ with self.subTest(count=count):
+ snapshot=copy.deepcopy(after);full=copy.deepcopy(am['rows'][3]['decoded'])
+ full['versions']=full['versions'][:count+1]
+ for v in full['versions']:v['isLatest']=v['versionId']==uid(count+1)
+ backup=dict(disk=3,path=f'{BUCKET}/{KEY}/{uid(1001)}/xl.meta.bkp',size=321,sha256='a'*64)
+ snapshot['files'].append(backup)
+ observations=dict(inspectorSha256=p.INSPECTOR_SHA,rows=[{**backup,'decoded':full}])
+ proof=p.transition(before,snapshot,bm,am,BUCKET,KEY,old,new,references=refs,
+ append_order=[uid(2),uid(3)],backup_metadata=observations)
+ self.assertEqual(proof['backupSemanticProofs'][0]['acknowledgedPutPrefix'],count)
+ self.assertEqual(proof['verifiedMetadataBackups'],[backup])
+ self.assertFalse(proof['backupSemanticProofs'][0]['liveAuthorityClaimed'])
+ self.assertFalse(proof['backupSemanticProofs'][0]['unexposedMetadataVerified'])
+
+ def test_changed_backup_rejects_unknown_or_corrupt_or_nonserial_views(self):
+ old=[entry(1,latest=True)];new=[entry(1),entry(2,payload='new'),entry(3,latest=True,payload='new')]
+ ref=entry(9,key=CONTROL,latest=True,payload='new');before,bm=physical(old+[ref]);after,am=physical(new)
+ refs={uid(i):dict(key=CONTROL,versionId=uid(9),entry=ref) for i in (2,3)}
+ backup=dict(disk=3,path=f'{BUCKET}/{KEY}/{uid(1001)}/xl.meta.bkp',size=321,sha256='a'*64)
+ after['files'].append(backup)
+ original=dict(inspectorSha256=p.INSPECTOR_SHA,rows=[{**backup,'decoded':copy.deepcopy(am['rows'][3]['decoded'])}])
+ for kind in ('no-decode','sha','size','inspector','duplicate-row','missing-old','skipped-put','unknown-version',
+ 'duplicate-version','latest','directory','parts','erasure','inline','unknown-field','bucket',
+ 'order','duplicate-order','no-order','unbound-directory','live-shard'):
+ snapshot=copy.deepcopy(after);obs=copy.deepcopy(original);order=[uid(2),uid(3)]
+ value=obs['rows'][0]['decoded'];versions=value['versions']
+ if kind=='no-decode':obs=None
+ if kind=='sha':obs['rows'][0]['sha256']='b'*64
+ if kind=='size':obs['rows'][0]['size']=322
+ if kind=='inspector':obs['inspectorSha256']='b'*64
+ if kind=='duplicate-row':obs['rows'].append(copy.deepcopy(obs['rows'][0]))
+ if kind=='missing-old':versions.pop(0)
+ if kind=='skipped-put':versions.pop(1)
+ if kind=='unknown-version':versions[1]['versionId']=uid(99)
+ if kind=='duplicate-version':versions.append(copy.deepcopy(versions[0]))
+ if kind=='latest':versions[0]['isLatest']=True
+ if kind=='directory':versions[0]['dataDir']=uid(99)
+ if kind=='parts':versions[1]['parts'][0]['size']=1
+ if kind=='erasure':versions[1]['erasure']['index']=1
+ if kind=='inline':versions[1]['inline']={}
+ if kind=='unknown-field':versions[1]['unbound']='hidden mutation'
+ if kind=='bucket':value['bucket']='foreign'
+ if kind=='order':order.reverse()
+ if kind=='duplicate-order':order=[uid(2),uid(2)]
+ if kind=='no-order':order=None
+ if kind=='unbound-directory':snapshot['files'][-1]['path']=f'{BUCKET}/{KEY}/{uid(999)}/xl.meta.bkp'
+ if kind=='live-shard':snapshot['files'][1]['sha256']='0'*64
+ with self.subTest(kind=kind),self.assertRaises(ValueError):
+ p.transition(before,snapshot,bm,am,BUCKET,KEY,old,new,references=refs,append_order=order,backup_metadata=obs)
+
+
+
+if __name__=='__main__':unittest.main()
diff --git a/test_validation_heal_version_race_runner.py b/test_validation_heal_version_race_runner.py
new file mode 100644
index 00000000..217b82ed
--- /dev/null
+++ b/test_validation_heal_version_race_runner.py
@@ -0,0 +1,214 @@
+"""Actual Recorder/controller boundaries; synthetic fleet data is not live proof."""
+import copy
+from contextlib import ExitStack
+import hashlib
+import json
+import os
+from pathlib import Path
+import tempfile
+import threading
+import time
+from types import SimpleNamespace
+import unittest
+from unittest.mock import Mock,patch
+import uuid
+
+from scripts.validation_heal import version_race_runner as runner,version_race_fixture as model,race
+from scripts.validation_heal.heal_probe import CLOCK_DOMAIN
+from scripts.validation_heal.heal_workload import Recorder
+
+
+class SDKProbe:
+ def __init__(self,binding,trace):
+ self.binding=binding;self.trace=trace;self.rows={};self.sequence=0;self.versioned=False;self.fail=None
+
+ def s3(self,operation,params,body=None):
+ started=time.monotonic_ns();self.trace.append(('s3',operation,copy.deepcopy(params)))
+ if operation==self.fail:raise OSError('unknown original request; no retry')
+ response={'ResponseMetadata':{'HTTPStatusCode':200}};key=params.get('Key');self.sequence+=1
+ if operation=='put_bucket_versioning':self.versioned=True
+ elif operation=='put_object':
+ vid=str(uuid.UUID(int=10000-self.sequence)) if self.versioned else 'null'
+ etag='"'+hashlib.md5(body,usedforsecurity=False).hexdigest()+'"'
+ row={'key':key,'versionId':vid,'deleteMarker':False,'payload':{'size':len(body),'sha256':hashlib.sha256(body).hexdigest()},
+ 'properties':{'ETag':etag,'ContentLength':len(body),'ContentType':params['ContentType'],
+ 'Metadata':copy.deepcopy(params['Metadata']),'StorageClass':'STANDARD'},
+ 'tags':[{'Key':'fixture','Value':'DV' if self.versioned else 'DB'},{'Key':'suite','Value':'heal'}]}
+ self.rows.setdefault(key,[]).append(row);response.update(VersionId=vid,ETag=etag)
+ elif operation=='delete_object':
+ response['ResponseMetadata']['HTTPStatusCode']=204
+ if 'VersionId' in params:
+ original=next(v for v in self.rows[key] if v['versionId']==params['VersionId'])
+ self.rows[key].remove(original);response.update(VersionId=original['versionId'],DeleteMarker=original['deleteMarker'])
+ elif self.versioned:
+ vid=str(uuid.UUID(int=10000-self.sequence));self.rows[key].append({'key':key,'versionId':vid,'deleteMarker':True})
+ response.update(VersionId=vid,DeleteMarker=True)
+ else:self.rows[key]=[]
+ elif operation=='delete_objects':
+ response['Deleted']=[]
+ for item in params['Delete']['Objects']:
+ original=next(v for v in self.rows[item['Key']] if v['versionId']==item['VersionId'])
+ self.rows[item['Key']].remove(original)
+ response['Deleted'].append({**item,'DeleteMarker':original['deleteMarker']})
+ elif operation=='get_object':
+ wanted=next((v for v in self.rows.get(key,[]) if v['versionId']==params['VersionId']),None) if 'VersionId' in params else (self.rows.get(key) or [None])[-1]
+ marker=bool(wanted and wanted['deleteMarker']);code=405 if marker and 'VersionId' in params else 404
+ response={'ResponseMetadata':{'HTTPStatusCode':code,'HTTPHeaders':{}},
+ 'Error':{'Code':'MethodNotAllowed' if code==405 else 'NoSuchVersion' if 'VersionId' in params else 'NoSuchKey'}}
+ if marker:response['ResponseMetadata']['HTTPHeaders'].update({'x-amz-delete-marker':'true','x-amz-version-id':wanted['versionId']})
+ elif operation!='create_bucket':raise AssertionError(operation)
+ return {'operation':operation,'params':copy.deepcopy(params),'response':response,
+ 'acknowledged':200<=response['ResponseMetadata']['HTTPStatusCode']<300,'resultUnknown':False,
+ 'clockDomain':CLOCK_DOMAIN,'clientCallInvoked':True,'callStartedMonotonicNs':started,
+ 'callFinishedMonotonicNs':time.monotonic_ns()}
+
+ def inventory(self,bucket,**unused):
+ self.trace.append(('inventory',bucket));rows=[]
+ for values in self.rows.values():
+ for i,row in enumerate(values):rows.append({**copy.deepcopy(row),'isLatest':i==len(values)-1})
+ return {'bucket':bucket,'entries':sorted(rows,key=lambda v:(v['key'],v['versionId']))}
+
+ def admin(self,path,query,body):
+ return {'httpStatus':200,'response':{'clientToken':str(uuid.UUID(int=1))}}
+
+
+class FakeFleet:
+ def __init__(self,nodes,directory,run_id,*unused,**kwargs):
+ self.nodes=nodes;self.evidence=directory;self.run_id=run_id;self.faults=[];self.records=[];self.cleaned=False
+ def record(self,label,value):self.records.append((label,value));return value
+ def deploy(self,*unused):pass
+ def stop(self):self.records.append(('stop',{}))
+ def start(self):self.records.append(('start',{}))
+ def snapshot(self,label,**kwargs):
+ self.records.append((label,{}));return {node:{'formats':[],'files':[]} for node in self.nodes}
+ def remote(self,node,action,**params):
+ self.records.append(('remote',{'node':node,'action':action,**params}));return {}
+ def cleanup(self):self.cleaned=True;return True
+
+
+class VersionRunnerTests(unittest.TestCase):
+ def test_actual_foreground_uses_one_sdk_receipt_and_releases_cap_without_sdk_retry(self):
+ for reject in (False,True):
+ with self.subTest(reject=reject),tempfile.TemporaryDirectory() as tmp:
+ trace=[];binding={'bucketPrefix':'rv-heal-owned','runId':'gh-12345-1-h1','maxInventoryBytes':512*1024**2}
+ probe=SDKProbe(binding,trace);seed=Recorder(probe,Path(tmp)/'seed')
+ ledger=model.seed_fixture(seed,binding['bucketPrefix'],'batch-version-delete',controls=3,size=17)
+ ledger.bind_baseline(probe.inventory(ledger.bucket));plan=model.mutation_plan(ledger,'batch-version-delete',size=17)
+ entered,release=threading.Event(),threading.Event();real=probe.s3
+ def call(operation,params,body=None):
+ start=time.monotonic_ns();entered.set()
+ if not release.wait(5):raise TimeoutError('test worker did not drain')
+ if reject:raise OSError('original reply lost')
+ value=real(operation,params,body);value.update(callStartedMonotonicNs=start,callFinishedMonotonicNs=time.monotonic_ns())
+ return value
+ probe.s3=call;writer=Recorder(probe,Path(tmp)/'foreground');fleet=FakeFleet(['node1'],Path(tmp),'gh-12345-1-h1')
+ path='/rustfs/admin/v3/heal/'+ledger.bucket;token=str(uuid.UUID(int=1))
+ def status(*unused):
+ self.assertTrue(entered.wait(5));a=time.monotonic_ns();b=time.monotonic_ns()
+ value={'httpStatus':200,'path':path,'query':[('clientToken',token)],'clockDomain':CLOCK_DOMAIN,
+ 'callStartedMonotonicNs':a,'callFinishedMonotonicNs':b,
+ 'response':{'summary':'running','settings':race.MARKER_REFERENCE['effective'],
+ 'outcome':{'counters':{'processed':1}}}}
+ release.set();return value
+ with patch.object(runner.race,'observe',side_effect=status),patch.object(runner,'io_cap') as cap:
+ if reject:
+ with self.assertRaises(ValueError):runner.foreground(writer,ledger,plan,probe,fleet,path,token,race.MARKER_REFERENCE,'batch-version-delete','node1')
+ self.assertTrue(ledger.poisoned)
+ else:
+ values,overlap=runner.foreground(writer,ledger,plan,probe,fleet,path,token,race.MARKER_REFERENCE,'batch-version-delete','node1')
+ self.assertEqual(len(values),1);self.assertTrue(overlap)
+ cap.assert_called_once_with(fleet,runner.CASE,'batch-version-delete',False,nodes=['node1'])
+ self.assertEqual(writer.sequence,1)
+
+ def controller(self,branch,*,overlap=True,fail=None,width=4):
+ tmp=tempfile.TemporaryDirectory();self.addCleanup(tmp.cleanup);root=Path(tmp.name)
+ candidate=root/'candidate.json';candidate.write_text(json.dumps({'source_sha':race.MARKER_CANDIDATE,'package_sha256':'c'*64}))
+ rc=root/'rc';rc.write_bytes(b'client');nodes=['node1'] if width==4 else ['node1','node2','node3','node4']
+ args=SimpleNamespace(candidate=candidate,evidence=root/'evidence',rc_bin=rc,tool_directory=root,
+ testing_commit='a'*40,binary_sha='b'*64,case_id=runner.CASE)
+ instances=[];probes=[];trace=[]
+ def fleet(*a,**kw):value=FakeFleet(*a,**kw);instances.append(value);return value
+ def probe(binding,endpoint=None):
+ if not probes:probes.append(SDKProbe(binding,trace))
+ return probes[0]
+ real_seed=model.seed_fixture
+ def seed(*a,**kw):
+ probes[0].fail='put_object' if fail=='seed' else None
+ return real_seed(*a,**kw,size=31)
+ def foreground(recorder,ledger,plan,*a):
+ trace.append(('foreground',));probes[0].fail=plan[0]['operation'] if fail=='foreground' else None
+ values=[model.submit(recorder,ledger,step) for step in plan]
+ runner.receipts(recorder);return values,overlap
+ def mapped(snapshot,bucket,entries):
+ return {(row['key'],row['versionId']):{'cohort':[]} for row in entries if not row['deleteMarker']}
+ def targets(snapshot,bucket,ledger,*a):
+ return [{'node':nodes[0],'disk':3,'key':key,'relativePath':f'{bucket}/{key}/xl.meta',
+ 'sha256':'d'*64,'size':300} for key in sorted(ledger.keys)]
+ def physical_transition(*a):
+ trace.append(('physical',))
+ if fail=='physical':raise ValueError('original physical result refused')
+ return {'physicalProofBeforeGETOrRescue':True}
+ def root_records(*a,**kw):
+ return {'original-root':{}} if fail=='root' else {}
+ def streams(*a,**kw):
+ if fail=='streams-io' and a[2]=='G04-final-full-streams':raise OSError('fsync failed')
+ return {'count':kw['count']}
+ with ExitStack() as stack:
+ stack.enter_context(patch.dict(os.environ,{'GITHUB_RUN_ID':'12345','GITHUB_RUN_ATTEMPT':'1'}))
+ for owner,name,value in [(runner,'Fleet',fleet),(runner,'Probe',probe),(runner,'verify_inspector',lambda *a:{'binarySha256':'e'*64}),
+ (runner,'validate_topology',Mock()),(model,'seed_fixture',seed),(runner.physical,'metadata_targets',targets),
+ (runner.race,'map_fixture',mapped),(runner,'verify_part_manifest',streams),(runner,'validate_missing_batch',Mock()),
+ (runner,'io_cap',Mock()),(runner,'partial',Mock()),(runner,'foreground',foreground),
+ (runner,'terminal',lambda *a:{'summary':'finished'}),(runner.physical,'transition',physical_transition),
+ (runner.physical,'reconstruction',Mock(return_value={})),(runner.race,'terminal_proof',lambda *a:{'completed':True}),
+ (runner,'journal_records',Mock()),(runner,'exact_records',root_records)]:
+ stack.enter_context(patch.object(owner,name,value))
+ result=runner.run_branch(args,'H1' if width==4 else 'H2',nodes,branch)
+ return result,instances[0],probes[0],trace
+
+ def test_all_source_g04_plans_preserve_257_metadata_faults_and_full_negative_contracts(self):
+ for width in (4,16):
+ for branch in model.BRANCHES[4:]:
+ with self.subTest(width=width,branch=branch):
+ result,fleet,probe,trace=self.controller(branch,width=width)
+ self.assertEqual((result['verdict'],result['cleanup']),('PASS','PASS'))
+ faults=[v for label,v in fleet.records if label=='remote' and v['action']=='apply-fault']
+ self.assertEqual(len(faults),257);self.assertEqual({v['disk'] for v in faults},{3})
+ self.assertTrue(all(v['relativePath'].endswith('/xl.meta') and v['faultId'].startswith('G04-') for v in faults))
+ self.assertEqual(fleet.faults,[]);self.assertTrue(fleet.cleaned)
+ foreground_index=trace.index(('foreground',));physical_index=trace.index(('physical',))
+ final_reads=[i for i,v in enumerate(trace) if v[0]=='s3' and v[1]=='get_object']
+ self.assertTrue(foreground_index None:
+ self.assertEqual(15, len(ILM_CASE_IDS))
+ self.assertEqual(("ilm.configuration-roundtrip",), ilm_harness_scored_cases(["ilm.configuration-roundtrip"]))
+ with self.assertRaises(ValueError):
+ ilm_harness_scored_cases(["missing.case"])
+
+ def test_result_requires_selected_case_evidence_and_cleanup(self) -> None:
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ case_id = "ilm.configuration-roundtrip"
+ (root / "proof.json").write_text("{}", encoding="utf-8")
+ result = {"status": {"state": "pass", "cases": [{
+ "id": case_id, "state": "pass", "evidence": ["proof.json"],
+ }]}}
+ manifest = {"executionPlan": {"scoredCases": [case_id]},
+ "cleanupOutcome": {"state": "pass"}, "preservedBuckets": []}
+ (root / "result.json").write_text(json.dumps(result), encoding="utf-8")
+ (root / "run-manifest.json").write_text(json.dumps(manifest), encoding="utf-8")
+ verify(case_id, root)
+ result["status"]["cases"][0]["evidence"] = ["../outside.json"]
+ (root / "result.json").write_text(json.dumps(result), encoding="utf-8")
+ with self.assertRaises(OSError):
+ verify(case_id, root)
+ result["status"]["cases"][0]["evidence"] = ["proof.json"]
+ (root / "result.json").write_text(json.dumps(result), encoding="utf-8")
+ manifest["preservedBuckets"] = ["leftover"]
+ (root / "run-manifest.json").write_text(json.dumps(manifest), encoding="utf-8")
+ with self.assertRaisesRegex(ValueError, "cleanup"):
+ verify(case_id, root)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test_validation_kms.sh b/test_validation_kms.sh
new file mode 100644
index 00000000..b7375d3a
--- /dev/null
+++ b/test_validation_kms.sh
@@ -0,0 +1,75 @@
+#!/usr/bin/env bash
+set -Eeuo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+source "${SCRIPT_DIR}/rustfs-kms-test.sh"
+
+MOCK_SOURCE=""
+MOCK_BUCKET_DELETED=0
+MOCK_LEAK=0
+
+kms_create_key() {
+ KMS_KEY_ID="key-validation-1"
+}
+
+aws_run() {
+ local operation="${2:-}" argument previous="" key="" body="" kid=""
+ AWS_CODE=0
+ AWS_OUT='{}'
+ for argument in "$@"; do
+ case "${previous}" in
+ --key) key="${argument}" ;;
+ --body) body="${argument}" ;;
+ --ssekms-key-id) kid="${argument}" ;;
+ esac
+ previous="${argument}"
+ done
+ case "${operation}" in
+ create-bucket) ;;
+ put-object)
+ if [[ "${kid}" == missing-* ]]; then
+ AWS_CODE=255
+ AWS_OUT='InvalidKMSKey'
+ if [ "${MOCK_LEAK}" -eq 1 ]; then AWS_OUT="${AWS_OUT} ${marker}"; fi
+ else
+ MOCK_SOURCE="${body}"
+ AWS_OUT="$(jq -nc --arg kid "${kid}" '{ServerSideEncryption:"aws:kms",SSEKMSKeyId:$kid}')"
+ fi
+ ;;
+ head-object)
+ if [ "${key}" = rejected ]; then
+ AWS_CODE=255
+ AWS_OUT='NotFound'
+ else
+ AWS_OUT="$(jq -nc --arg kid "${KMS_KEY_ID}" --argjson bytes "$(wc -c < "${MOCK_SOURCE}")" \
+ '{ServerSideEncryption:"aws:kms",SSEKMSKeyId:$kid,ContentLength:$bytes,Metadata:{validation:"kms-roundtrip"}}')"
+ fi
+ ;;
+ get-object)
+ cp "${MOCK_SOURCE}" "${@: -1}"
+ ;;
+ delete-object) ;;
+ delete-bucket) MOCK_BUCKET_DELETED=$((MOCK_BUCKET_DELETED + 1)) ;;
+ *) printf 'unexpected AWS operation: %s\n' "${operation}" >&2; return 1 ;;
+ esac
+}
+
+test_validation_kms_sse_kms_round_trip
+[ "${VALIDATION_OBJECT_CLEANUP_STATUS}" = PASS ]
+[ "${MOCK_BUCKET_DELETED}" -eq 1 ]
+jq -e '.key_id == "key-validation-1" and .metadata.validation == "kms-roundtrip" and .bytes > 4096' \
+ "${TEST_TMP}/validation-fixture.json" >/dev/null
+
+MOCK_LEAK=1
+if test_validation_kms_sse_kms_round_trip; then
+ printf 'plaintext leak passed\n' >&2; exit 1
+fi
+[ "${MOCK_BUCKET_DELETED}" -eq 2 ] || { printf 'failed case did not clean bucket\n' >&2; exit 1; }
+
+"${SCRIPT_DIR}/rustfs-kms-test.sh" --validation-case kms.sse-kms-round-trip \
+ --dry-run --yes --package-url https://example.invalid/rustfs.deb \
+ > "${TEST_TMP}/validation-dry-run.log"
+grep -q '\[DRY-RUN\] kms.sse-kms-round-trip SseKmsRoundTrip' "${TEST_TMP}/validation-dry-run.log"
+if grep -q '\[DRY-RUN\] KMS-' "${TEST_TMP}/validation-dry-run.log"; then
+ printf 'validation mode scheduled legacy KMS cases\n' >&2; exit 1
+fi
diff --git a/test_validation_migration.py b/test_validation_migration.py
new file mode 100644
index 00000000..d82c2798
--- /dev/null
+++ b/test_validation_migration.py
@@ -0,0 +1,373 @@
+#!/usr/bin/env python3
+
+import copy
+import json
+import subprocess
+import sys
+import tempfile
+import unittest
+from pathlib import Path
+
+from scripts.validation_catalog import build_snapshot, validate_reuse_candidates, validate_snapshot
+from scripts.validation_report import validate_results
+from scripts.validation_migration_review import summarize, validate_review
+
+
+ROOT = Path(__file__).resolve().parent
+SOURCE = Path("/Volumes/Developer/KAI/rustfs-release-validation/scenarios/catalog.yaml")
+
+
+class CatalogTest(unittest.TestCase):
+ def test_snapshot_contains_all_source_cases(self) -> None:
+ snapshot = json.loads((ROOT / "validation-cases.json").read_text(encoding="utf-8"))
+ validate_snapshot(snapshot)
+ validate_reuse_candidates(snapshot, ROOT)
+ self.assertEqual(158, sum(len(module["cases"]) for module in snapshot["modules"]))
+ if SOURCE.is_file():
+ self.assertEqual(build_snapshot(SOURCE), snapshot)
+
+ def test_duplicate_case_is_rejected(self) -> None:
+ snapshot = json.loads((ROOT / "validation-cases.json").read_text(encoding="utf-8"))
+ snapshot["modules"][0]["cases"].append(snapshot["modules"][0]["cases"][0])
+ with self.assertRaisesRegex(ValueError, "duplicate case ID"):
+ validate_snapshot(snapshot)
+
+
+class ReportTest(unittest.TestCase):
+ def setUp(self) -> None:
+ self.tmp = tempfile.TemporaryDirectory()
+ self.results = Path(self.tmp.name) / "cases"
+ self.results.mkdir()
+ self.catalog = json.loads((ROOT / "validation-cases.json").read_text(encoding="utf-8"))
+ self.case = self.catalog["modules"][0]["cases"][0]
+
+ def tearDown(self) -> None:
+ self.tmp.cleanup()
+
+ def write_result(self, name: str, **changes: object) -> None:
+ value = {
+ "schema_version": 1,
+ "case_id": self.case["id"],
+ "source_status": self.case["source_status"],
+ "migration_status": "unverified",
+ "verdict": "BLOCKED",
+ "reason": "executor not migrated",
+ "profile": "lifecycle-a",
+ }
+ value.update(changes)
+ (self.results / name).write_text(json.dumps(value), encoding="utf-8")
+
+ def test_missing_implemented_case_fails_gate(self) -> None:
+ _, errors = validate_results(self.catalog, self.results, gate_statuses={"implemented"})
+ self.assertIn(f"missing gate result: {self.case['id']}", errors)
+
+ def test_selected_case_requires_live_pass(self) -> None:
+ _, errors = validate_results(
+ self.catalog, self.results, gate_statuses=set(), gate_cases={self.case["id"]},
+ )
+ self.assertIn(f"missing gate result: {self.case['id']}", errors)
+ with self.assertRaisesRegex(ValueError, "unknown gate case"):
+ validate_results(
+ self.catalog, self.results, gate_statuses=set(), gate_cases={"missing.case"},
+ )
+
+ def test_duplicate_and_running_fail(self) -> None:
+ self.write_result("one.json", verdict="RUNNING")
+ self.write_result("two.json", verdict="RUNNING")
+ _, errors = validate_results(self.catalog, self.results, gate_statuses=set())
+ self.assertIn(f"duplicate result: {self.case['id']}", errors)
+ self.assertIn(f"{self.case['id']}: result remained RUNNING", errors)
+
+ def test_pass_needs_verified_assertions_cleanup_and_provenance(self) -> None:
+ self.write_result("one.json", verdict="PASS")
+ _, errors = validate_results(self.catalog, self.results, gate_statuses=set())
+ self.assertIn(f"{self.case['id']}: PASS without verified migration", errors)
+ self.assertIn(f"{self.case['id']}: PASS lacks one result per source assertion", errors)
+ self.assertIn(f"{self.case['id']}: PASS requires clean environment", errors)
+ self.assertIn(f"{self.case['id']}: PASS lacks candidate and client provenance", errors)
+
+ def test_complete_case_contract(self) -> None:
+ evidence = self.results.parent / "case-evidence.json"
+ evidence.write_text("{}", encoding="utf-8")
+ self.write_result(
+ "one.json",
+ verdict="PASS",
+ migration_status="verified",
+ assertions=[
+ {"source_assertion": text, "status": "PASS", "evidence": ["case-evidence.json"]}
+ for text in self.case["assertions"]
+ ],
+ cleanup_status="PASS",
+ provenance={
+ "candidate_sha256": "a" * 64,
+ "binary_sha256": "e" * 64,
+ "candidate_url": "https://example.invalid/rustfs.deb",
+ "source_commit": "b" * 40,
+ "testing_commit": "c" * 40,
+ "rc_path": "/opt/bin/rc",
+ "rc_sha256": "d" * 64,
+ },
+ )
+ _, errors = validate_results(self.catalog, self.results, gate_statuses=set())
+ self.assertEqual([], errors)
+
+ def test_pass_cannot_relabel_source_assertions(self) -> None:
+ (self.results.parent / "evidence.json").write_text("{}", encoding="utf-8")
+ self.write_result(
+ "one.json",
+ verdict="PASS",
+ migration_status="verified",
+ assertions=[
+ {"source_assertion": "weaker assertion", "status": "PASS", "evidence": ["evidence.json"]}
+ for _ in self.case["assertions"]
+ ],
+ cleanup_status="PASS",
+ provenance={
+ "candidate_sha256": "a" * 64,
+ "binary_sha256": "e" * 64,
+ "candidate_url": "https://example.invalid/rustfs.deb",
+ "source_commit": "b" * 40,
+ "testing_commit": "c" * 40,
+ "rc_path": "/opt/bin/rc",
+ "rc_sha256": "d" * 64,
+ },
+ )
+ _, errors = validate_results(self.catalog, self.results, gate_statuses=set())
+ self.assertIn(f"{self.case['id']}: assertion 1 lacks matching source text or evidence", errors)
+
+ def test_missing_evidence_file_fails(self) -> None:
+ self.write_result(
+ "one.json",
+ verdict="PASS",
+ migration_status="verified",
+ assertions=[
+ {"source_assertion": text, "status": "PASS", "evidence": ["missing.json"]}
+ for text in self.case["assertions"]
+ ],
+ cleanup_status="PASS",
+ provenance={
+ "candidate_sha256": "a" * 64,
+ "binary_sha256": "e" * 64,
+ "candidate_url": "https://example.invalid/rustfs.deb",
+ "source_commit": "b" * 40,
+ "testing_commit": "c" * 40,
+ "rc_path": "/opt/bin/rc",
+ "rc_sha256": "d" * 64,
+ },
+ )
+ _, errors = validate_results(self.catalog, self.results, gate_statuses=set())
+ self.assertIn(f"{self.case['id']}: assertion 1 has missing or unsafe evidence paths", errors)
+
+
+class ResultWriterTest(unittest.TestCase):
+ def test_writer_and_report_contract(self) -> None:
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ results = root / "cases"
+ results.mkdir()
+ rc = root / "rc"
+ rc.write_bytes(b"test-rc")
+ candidate = root / "candidate.json"
+ candidate.write_text(json.dumps({
+ "package_url": "https://example.invalid/rustfs.deb",
+ "package_sha256": "a" * 64,
+ "source_sha": "c" * 40,
+ }), encoding="utf-8")
+ evidence = root / "evidence.json"
+ evidence.write_text("{}", encoding="utf-8")
+ command = [
+ sys.executable, str(ROOT / "scripts/validation_result.py"),
+ "--case-id", "s3.basic-object-round-trip",
+ "--profile", "single-1disk",
+ "--result-file", str(results / "s3.basic-object-round-trip.json"),
+ ]
+ subprocess.run(command + ["--verdict", "RUNNING"], check=True, capture_output=True)
+ self.assertEqual("RUNNING", json.loads((results / "s3.basic-object-round-trip.json").read_text())["verdict"])
+ identity = ["--candidate-manifest", str(candidate), "--rc-bin", str(rc), "--testing-commit", "d" * 40]
+ subprocess.run(command + identity + ["--verdict", "INFRA_ERROR", "--cleanup-status", "PASS",
+ "--reason", "candidate install failed"], check=True, capture_output=True)
+ bootstrap = json.loads((results / "s3.basic-object-round-trip.json").read_text())
+ self.assertEqual(bootstrap["verdict"], "INFRA_ERROR")
+ self.assertEqual(bootstrap["migration_status"], "unverified")
+ self.assertIsNone(bootstrap["provenance"]["binary_sha256"])
+ subprocess.run(command + identity + ["--verdict", "BLOCKED", "--cleanup-status", "PASS",
+ "--binary-sha256", "b" * 64, "--reason", "diagnostic branches require independent canonical audit",
+ "--evidence", "evidence.json"], check=True, capture_output=True)
+ blocked = json.loads((results / "s3.basic-object-round-trip.json").read_text())
+ self.assertEqual(blocked["verdict"], "BLOCKED")
+ self.assertEqual(blocked["migration_status"], "unverified")
+ self.assertNotIn("assertions", blocked)
+ self.assertEqual(blocked["provenance"]["binary_sha256"], "b" * 64)
+ catalog = json.loads((ROOT / "validation-cases.json").read_text(encoding="utf-8"))
+ _, blocked_errors = validate_results(catalog, results, gate_statuses=set(),
+ gate_cases={"s3.basic-object-round-trip"})
+ self.assertEqual(blocked_errors, ["s3.basic-object-round-trip: gate verdict is 'BLOCKED'"])
+ invalid_pass = subprocess.run(command + identity + ["--verdict", "PASS", "--cleanup-status", "PASS",
+ "--evidence", "evidence.json"], capture_output=True)
+ self.assertNotEqual(invalid_pass.returncode, 0)
+ subprocess.run(command + [
+ "--verdict", "PASS", "--cleanup-status", "PASS",
+ "--candidate-manifest", str(candidate),
+ "--rc-bin", str(rc),
+ "--testing-commit", "d" * 40,
+ "--binary-sha256", "b" * 64,
+ "--evidence", "evidence.json",
+ ], check=True, capture_output=True)
+ catalog = json.loads((ROOT / "validation-cases.json").read_text(encoding="utf-8"))
+ _, errors = validate_results(catalog, results, gate_statuses=set())
+ self.assertEqual([], errors)
+
+ def test_predeployment_blocked_result_does_not_claim_an_installed_binary(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ rc = root / "rc"
+ rc.write_bytes(b"test-rc")
+ candidate = root / "candidate.json"
+ candidate.write_text(json.dumps({
+ "package_url": "https://example.invalid/rustfs.deb",
+ "package_sha256": "a" * 64,
+ "source_sha": "c" * 40,
+ }), encoding="utf-8")
+ result = root / "b03.json"
+ command = [sys.executable, str(ROOT / "scripts/validation_result.py"),
+ "--case-id", "heal.disk-replacement-formatted", "--profile", "cluster-1pool",
+ "--result-file", str(result), "--verdict", "BLOCKED", "--cleanup-status", "PASS",
+ "--candidate-manifest", str(candidate), "--testing-commit", "d" * 40,
+ "--rc-bin", str(rc), "--predeployment-blocked", "--reason", "mount gate failed"]
+ subprocess.run(command, check=True, capture_output=True)
+ value = json.loads(result.read_text(encoding="utf-8"))
+ self.assertEqual(value["execution_stage"], "pre-deployment")
+ self.assertEqual(value["migration_status"], "unverified")
+ self.assertIsNone(value["provenance"]["binary_sha256"])
+ self.assertNotIn("assertions", value)
+ invalid_command = command.copy()
+ invalid_command[invalid_command.index("--verdict") + 1] = "PASS"
+ invalid = subprocess.run(invalid_command, capture_output=True)
+ self.assertNotEqual(invalid.returncode, 0)
+
+ b04 = root / "b04.json"
+ b04_command = command.copy()
+ b04_command[b04_command.index("--case-id") + 1] = "heal.disk-replacement-unformatted"
+ b04_command[b04_command.index("--result-file") + 1] = str(b04)
+ subprocess.run(b04_command, check=True, capture_output=True)
+ b04_value = json.loads(b04.read_text(encoding="utf-8"))
+ self.assertEqual(b04_value["case_id"], "heal.disk-replacement-unformatted")
+ self.assertIsNone(b04_value["provenance"]["binary_sha256"])
+
+
+class ImplementationReviewTest(unittest.TestCase):
+ def setUp(self):
+ self.catalog = json.loads((ROOT / "validation-cases.json").read_text())
+ self.review = json.loads((ROOT / "validation-migration-review.json").read_text())
+
+ def test_explicit_review_covers_the_entire_catalog(self):
+ validate_review(self.review, self.catalog)
+ summary = summarize(self.review)
+ self.assertEqual(summary["total"], 158)
+ self.assertEqual(sum(summary["implementation"].values()), 158)
+ self.assertEqual(sum(summary["live"].values()), 158)
+ self.assertFalse(summary["candidate_pass_claimed"])
+ self.assertFalse(summary["all_cases_live_accepted"])
+
+ def test_retained_failure_is_independent_of_implementation_completion(self):
+ # Candidate failures must never silently become missing test code.
+ case = next(case for case in self.review["cases"]
+ if case["implementation"] == "implemented"
+ and case["live"]["state"] == "retained-failure")
+ before = summarize(self.review)["implementation"]
+ case["live"]["finding"] = "Candidate request returned a retained unexpected response; cause remains under review."
+ validate_review(self.review, self.catalog)
+ self.assertEqual(before, summarize(self.review)["implementation"])
+ with tempfile.TemporaryDirectory() as directory:
+ result = {"schema_version": 1, "case_id": case["case_id"],
+ "source_status": next(source["source_status"] for module in self.catalog["modules"]
+ for source in module["cases"] if source["id"] == case["case_id"]),
+ "migration_status": "unverified", "profile": "cluster-1pool",
+ "verdict": "FAIL", "cleanup_status": "PASS", "reason": case["live"]["finding"]}
+ result_path = Path(directory) / "retained.json"
+ result_path.write_text(json.dumps(result))
+ original = result_path.read_bytes()
+ _, errors = validate_results(self.catalog, Path(directory), gate_statuses=set(),
+ gate_cases={case["case_id"]})
+ self.assertIn(f"{case['case_id']}: gate verdict is 'FAIL'", errors)
+ self.assertEqual(original, result_path.read_bytes())
+
+ def test_missing_or_duplicate_id_is_rejected(self):
+ for change, reason in (("missing", "missing implementation reviews"),
+ ("duplicate", "duplicate implementation case")):
+ with self.subTest(change=change):
+ review = copy.deepcopy(self.review)
+ if change == "missing":
+ review["cases"].pop()
+ else:
+ review["cases"].append(copy.deepcopy(review["cases"][0]))
+ with self.assertRaisesRegex(ValueError, reason):
+ validate_review(review, self.catalog)
+
+ def test_source_or_code_drift_requires_another_review(self):
+ for change, reason in (("source", "another source catalog"),
+ ("code", "implementation changed since review"),
+ ("branch", "source branch coverage changed")):
+ with self.subTest(change=change):
+ review = copy.deepcopy(self.review)
+ if change == "source":
+ review["source_commit"] = "0" * 40
+ elif change == "code":
+ review["reviewed_files"][next(iter(review["reviewed_files"]))] = "0" * 64
+ else:
+ case = next(case for case in review["cases"] if "source_variants" in case)
+ case["source_variants"].pop(next(iter(case["source_variants"])))
+ with self.assertRaisesRegex(ValueError, reason):
+ validate_review(review, self.catalog)
+
+ def test_partial_implementation_must_name_a_concrete_gap(self):
+ case = self.review["cases"][0]
+ case.update(implementation="partial", gaps=[])
+ with self.assertRaisesRegex(ValueError, "actionable gaps"):
+ validate_review(self.review, self.catalog)
+
+ def test_default_disabled_environment_requirements_cannot_disappear(self):
+ case = next(case for case in self.review["cases"] if case["implementation"] == "environment-gated")
+ case["required_capabilities"] = []
+ with self.assertRaisesRegex(ValueError, "environment requirements drifted"):
+ validate_review(self.review, self.catalog)
+
+ def test_unreviewed_or_nonlocal_branch_references_are_rejected(self):
+ for name, reason in (("../outside.py", "nonlocal implementation reference"),
+ ("validation-migration-review.json", "unreviewed implementation reference")):
+ with self.subTest(name=name):
+ review = copy.deepcopy(self.review)
+ review["cases"][0]["references"] = [name]
+ with self.assertRaisesRegex(ValueError, reason):
+ validate_review(review, self.catalog)
+
+ def test_invalid_record_types_raise_a_review_error(self):
+ for invalid in (None, [], {"implementation": []}, {"case_id": "unknown"}):
+ with self.subTest(invalid=invalid):
+ review = copy.deepcopy(self.review)
+ review["cases"][0] = invalid
+ with self.assertRaises(ValueError):
+ validate_review(review, self.catalog)
+
+ def test_require_complete_checks_code_gaps_without_demanding_product_pass(self):
+ with tempfile.TemporaryDirectory() as directory:
+ path = Path(directory) / "review.json"
+ command = [sys.executable, "-B", str(ROOT / "scripts/validation_migration_review.py"),
+ "--review", str(path), "--require-complete"]
+ path.write_text(json.dumps(self.review))
+ result = subprocess.run(command, capture_output=True, text=True)
+ self.assertEqual(result.returncode, 0, result.stderr)
+ self.assertGreater(json.loads(result.stdout)["live"]["retained-failure"], 0)
+ self.assertFalse(json.loads(result.stdout)["candidate_pass_claimed"])
+ # A real implementation gap must fail even with unchanged product results.
+ case = next(case for case in self.review["cases"]
+ if case["implementation"] == "implemented" and case["live"]["state"] == "retained-failure")
+ case.update(implementation="partial", gaps=["Missing source controller in this negative fixture."])
+ path.write_text(json.dumps(self.review))
+ result = subprocess.run(command, capture_output=True, text=True)
+ self.assertEqual(result.returncode, 1, result.stderr)
+ self.assertEqual(json.loads(result.stdout)["remaining_implementation"][0]["case_id"], case["case_id"])
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test_validation_oidc.py b/test_validation_oidc.py
new file mode 100644
index 00000000..ee97c2d1
--- /dev/null
+++ b/test_validation_oidc.py
@@ -0,0 +1,57 @@
+"""Reject false OIDC validation success at authentication boundaries."""
+
+from datetime import datetime, timedelta, timezone
+import os
+import subprocess
+import unittest
+from unittest.mock import patch
+
+from scripts.validation_oidc import aws, sts
+
+
+class OidcBoundaryTest(unittest.TestCase):
+ def test_sts_requires_subject_and_bounded_expiration(self):
+ def response(subject, seconds):
+ expiration = (datetime.now(timezone.utc) + timedelta(seconds=seconds)).isoformat()
+ return (""
+ "test-keytest-secret"
+ "test-token"
+ f"{expiration}"
+ f"{subject}"
+ "").encode()
+ with patch("scripts.validation_oidc.http", return_value=(200, response("expected", 900))):
+ credentials, observation = sts("http://fixture", "identity-token", "expected")
+ self.assertEqual(credentials["AWS_SESSION_TOKEN"], "test-token")
+ self.assertNotIn("test-secret", str(observation))
+ for subject, seconds in (("different", 900), ("expected", -1), ("expected", 3600)):
+ with self.subTest(subject=subject, seconds=seconds):
+ with patch("scripts.validation_oidc.http", return_value=(200, response(subject, seconds))):
+ with self.assertRaises(RuntimeError):
+ sts("http://fixture", "identity-token", "expected")
+
+ def test_transport_error_cannot_prove_access_denial(self):
+ credentials = {"AWS_ACCESS_KEY_ID": "temporary-key", "AWS_SECRET_ACCESS_KEY": "temporary-secret"}
+ network_error = subprocess.CompletedProcess([], 255, "", "Could not connect to endpoint")
+ with patch("scripts.validation_oidc.subprocess.run", return_value=network_error):
+ with self.assertRaisesRegex(RuntimeError, "expected access boundary rejection"):
+ aws("http://fixture", credentials, "get-object", denied=True)
+ access_denied = subprocess.CompletedProcess([], 255, "", "An error occurred (AccessDenied) when calling GetObject")
+ with patch.dict(os.environ, {"AWS_SESSION_TOKEN": "ambient-token", "AWS_PROFILE": "ambient-profile"}):
+ with patch("scripts.validation_oidc.subprocess.run", return_value=access_denied) as run:
+ self.assertEqual(aws("http://fixture", credentials, "get-object", denied=True), "AccessDenied")
+ environment = run.call_args.kwargs["env"]
+ self.assertNotIn("AWS_SESSION_TOKEN", environment)
+ self.assertNotIn("AWS_PROFILE", environment)
+
+ def test_invalid_request_only_proves_the_session_token_boundary(self):
+ rejected = subprocess.CompletedProcess([], 254, "", "An error occurred (InvalidRequest) when calling GetObject")
+ credentials = {"AWS_ACCESS_KEY_ID": "temporary-key", "AWS_SECRET_ACCESS_KEY": "temporary-secret"}
+ with patch("scripts.validation_oidc.subprocess.run", return_value=rejected):
+ with self.assertRaisesRegex(RuntimeError, "expected access boundary rejection"):
+ aws("http://fixture", credentials, "get-object", denied=True)
+ self.assertEqual(aws("http://fixture", credentials, "get-object", denied=True,
+ rejection_codes=frozenset({"InvalidRequest"})), "InvalidRequest")
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test_validation_pool.py b/test_validation_pool.py
new file mode 100644
index 00000000..a7d1e7bf
--- /dev/null
+++ b/test_validation_pool.py
@@ -0,0 +1,24 @@
+#!/usr/bin/env python3
+
+import unittest
+
+from scripts.validation_pool.case_plan import POOL_CASE_IDS, pool_harness_scored_cases
+from scripts.validation_pool.pool_helpers import active_endpoint_indices, active_fixture_bytes
+
+
+class PoolContractTest(unittest.TestCase):
+ def test_catalog_drives_case_selection(self) -> None:
+ self.assertEqual(13, len(POOL_CASE_IDS))
+ self.assertEqual(("pool.status",), pool_harness_scored_cases(["pool.status"]))
+ with self.assertRaises(ValueError):
+ pool_harness_scored_cases(["pool.missing"])
+
+ def test_two_node_layout_and_active_fixture_size(self) -> None:
+ self.assertEqual((0,), active_endpoint_indices(2, "initial"))
+ self.assertEqual((0, 1), active_endpoint_indices(2, "expanded"))
+ self.assertEqual((1,), active_endpoint_indices(2, "retired"))
+ self.assertEqual(32 * 1024 * 1024, active_fixture_bytes(1024))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test_validation_replication.py b/test_validation_replication.py
new file mode 100644
index 00000000..4407d99a
--- /dev/null
+++ b/test_validation_replication.py
@@ -0,0 +1,43 @@
+#!/usr/bin/env python3
+
+import json
+import tempfile
+import unittest
+from pathlib import Path
+
+from scripts.validation_replication.case_plan import (
+ BUCKET_CASE_IDS, SITE_CASE_IDS, SITE_GROUP_ACTIVE_CASE_IDS,
+)
+from scripts.validation_replication.verify_result import verify
+
+
+class ReplicationContractTest(unittest.TestCase):
+ def test_catalog_and_fixture_selection(self) -> None:
+ self.assertEqual(18, len(BUCKET_CASE_IDS))
+ self.assertEqual(20, len(SITE_CASE_IDS))
+ self.assertEqual(16, len(SITE_GROUP_ACTIVE_CASE_IDS))
+ self.assertNotIn("site-replication.unconfigured-info", SITE_GROUP_ACTIVE_CASE_IDS)
+ self.assertIn("site-replication.bucket-create", SITE_GROUP_ACTIVE_CASE_IDS)
+
+ def test_pass_requires_case_fixture_and_isolation(self) -> None:
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ case_id = "site-replication.bucket-create"
+ (root / "proof.json").write_text("{}", encoding="utf-8")
+ result = {"status": {"state": "pass", "cases": [{
+ "id": case_id, "state": "pass", "evidence": ["proof.json"],
+ }]}}
+ manifest = {"executionPlan": {"scoredCases": [case_id]},
+ "isolationOutcome": {"state": "pass", "trackedBuckets": []},
+ "fixtureOutcomes": [{"state": "pass"}]}
+ (root / "result.json").write_text(json.dumps(result), encoding="utf-8")
+ (root / "run-manifest.json").write_text(json.dumps(manifest), encoding="utf-8")
+ verify(case_id, root)
+ manifest["fixtureOutcomes"][0]["state"] = "infra_error"
+ (root / "run-manifest.json").write_text(json.dumps(manifest), encoding="utf-8")
+ with self.assertRaisesRegex(ValueError, "fixture"):
+ verify(case_id, root)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test_validation_s3_basic.sh b/test_validation_s3_basic.sh
new file mode 100644
index 00000000..a20c0e31
--- /dev/null
+++ b/test_validation_s3_basic.sh
@@ -0,0 +1,73 @@
+#!/usr/bin/env bash
+set -Eeuo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+source "${SCRIPT_DIR}/rustfs-s3-compat-test.sh"
+
+MOCK_DELETED=0
+MOCK_BUCKET_DELETED=0
+MOCK_BAD_METADATA=0
+MOCK_SOURCE=""
+
+aws_run() {
+ local method="${2:-}" argument
+ AWS_CODE=0
+ AWS_OUT='{}'
+ case "${method}" in
+ create-bucket) ;;
+ put-object)
+ for argument in "$@"; do
+ if [ "${MOCK_EXPECT_BODY:-0}" -eq 1 ]; then MOCK_SOURCE="${argument}"; MOCK_EXPECT_BODY=0; break; fi
+ if [ "${argument}" = "--body" ]; then MOCK_EXPECT_BODY=1; fi
+ done
+ AWS_OUT="$(printf '{\"ETag\":\"\\\"%s\\\"\"}' "$(md5_of_file "${MOCK_SOURCE}")")"
+ ;;
+ head-object)
+ if [ "${MOCK_BAD_METADATA}" -eq 1 ]; then
+ AWS_OUT='{"ContentLength":4096,"ETag":"wrong","Metadata":{"validation":"wrong"},"ContentType":"application/octet-stream"}'
+ else
+ AWS_OUT="$(printf '{\"ContentLength\":4096,\"ETag\":\"\\\"%s\\\"\",\"Metadata\":{\"validation\":\"roundtrip\"},\"ContentType\":\"application/octet-stream\"}' "$(md5_of_file "${MOCK_SOURCE}")")"
+ fi
+ ;;
+ get-object)
+ if [ "${MOCK_DELETED}" -eq 1 ]; then
+ AWS_CODE=255
+ AWS_OUT='NoSuchKey'
+ else
+ cp "${MOCK_SOURCE}" "${@: -1}"
+ fi
+ ;;
+ list-objects-v2)
+ AWS_OUT='{"Contents":[{"Key":"object","Size":4096}]}'
+ ;;
+ delete-object) MOCK_DELETED=1 ;;
+ delete-bucket) MOCK_BUCKET_DELETED=1 ;;
+ *) printf 'unexpected AWS call: %s\n' "${method}" >&2; return 1 ;;
+ esac
+}
+
+test_validation_s3_basic_object_round_trip
+[ "${MOCK_BUCKET_DELETED}" -eq 1 ]
+
+MOCK_DELETED=0
+MOCK_BUCKET_DELETED=0
+MOCK_BAD_METADATA=1
+if test_validation_s3_basic_object_round_trip; then
+ printf 'validation case accepted bad HEAD metadata\n' >&2
+ exit 1
+fi
+[ "${MOCK_BUCKET_DELETED}" -eq 1 ] || { printf 'failure did not clean bucket\n' >&2; exit 1; }
+
+"${SCRIPT_DIR}/rustfs-s3-compat-test.sh" --validation-case s3.basic-object-round-trip \
+ --dry-run --yes --package-url https://example.invalid/rustfs.deb \
+ > "${TEST_TMP}/validation-dry-run.log"
+grep -q '\[DRY-RUN\] s3.basic-object-round-trip BasicObjectRoundTrip' "${TEST_TMP}/validation-dry-run.log"
+if grep -q '\[DRY-RUN\] S3C-' "${TEST_TMP}/validation-dry-run.log"; then
+ printf 'validation mode scheduled legacy S3C cases\n' >&2
+ exit 1
+fi
+if "${SCRIPT_DIR}/rustfs-s3-compat-test.sh" --validation-case unknown \
+ --dry-run --yes > "${TEST_TMP}/validation-bad-case.log" 2>&1; then
+ printf 'unknown validation case was accepted\n' >&2
+ exit 1
+fi
diff --git a/test_validation_scanner_usage.sh b/test_validation_scanner_usage.sh
new file mode 100644
index 00000000..5d5f75b9
--- /dev/null
+++ b/test_validation_scanner_usage.sh
@@ -0,0 +1,131 @@
+#!/usr/bin/env bash
+set -Eeuo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+source "${SCRIPT_DIR}/rustfs-storage-test.sh"
+
+MOCK_OBJECTS_FILE="${TEST_TMP}/mock-objects.jsonl"
+MOCK_BUCKET=""
+MOCK_BUCKET_DELETED=0
+MOCK_DELETE_FAIL=0
+: > "${MOCK_OBJECTS_FILE}"
+
+aws_run() {
+ local operation="${2:-}" key="" body="" previous="" argument
+ AWS_CODE=0
+ AWS_OUT='{}'
+ case "${operation}" in
+ create-bucket)
+ MOCK_BUCKET="$4"
+ ;;
+ put-object)
+ for argument in "$@"; do
+ case "${previous}" in
+ --key) key="${argument}" ;;
+ --body) body="${argument}" ;;
+ esac
+ previous="${argument}"
+ done
+ jq -nc --arg key "${key}" --argjson size "$(wc -c < "${body}")" '{Key:$key,Size:$size}' >> "${MOCK_OBJECTS_FILE}"
+ ;;
+ list-objects-v2)
+ AWS_OUT="$(jq -s '{Contents:.}' "${MOCK_OBJECTS_FILE}")"
+ ;;
+ delete-object) ;;
+ delete-bucket)
+ MOCK_BUCKET_DELETED=$((MOCK_BUCKET_DELETED + 1))
+ if [ "${MOCK_DELETE_FAIL}" -eq 1 ]; then AWS_CODE=1; AWS_OUT='mock cleanup failure'; fi
+ ;;
+ *) printf 'unexpected AWS operation: %s\n' "${operation}" >&2; return 1 ;;
+ esac
+}
+
+admin_api() {
+ local path="$2" count
+ printf '200' > "${ADMIN_API_CODE_FILE}"
+ count="$(wc -l < "${MOCK_OBJECTS_FILE}" | tr -d ' ')"
+ case "${path}" in
+ /rustfs/admin/v3/scanner/status)
+ jq -nc --argjson now "$(date -u +%s)" \
+ '{enabled:true,freshness:{state:"fresh",last_cycle_end_unix_secs:$now},metrics:{last_cycle_result:"success",leader_lock_held_by_this_process:true},cycle_schedule:{execution_role:"leader"}}'
+ ;;
+ /rustfs/admin/v3/datausageinfo)
+ if [ "${count}" -eq 0 ]; then
+ printf '{"scanner_cycle":0,"usage_snapshot_complete":true,"buckets_usage":{}}\n'
+ else
+ jq -nc --arg bucket "${MOCK_BUCKET}" --argjson bytes "$(jq -s 'map(.Size) | add' "${MOCK_OBJECTS_FILE}")" \
+ '{scanner_cycle:1,usage_snapshot_complete:true,usage_snapshot_converged:true,buckets_usage:{($bucket):{objects_count:20,size:$bytes}}}'
+ fi
+ ;;
+ *) printf 'unexpected Admin path: %s\n' "${path}" >&2; return 1 ;;
+ esac
+}
+
+test_validation_scanner_usage_convergence
+[ "${VALIDATION_OBJECT_CLEANUP_STATUS}" = PASS ]
+[ "${MOCK_BUCKET_DELETED}" -eq 1 ]
+jq -e '.expected_count == 20 and .expected_size == 20690 and (.objects | length) == 20' \
+ "${TEST_TMP}/validation-fixture.json" >/dev/null
+validation_scanner_usage_matches "${TEST_TMP}/validation-scanner.json" "${TEST_TMP}/validation-usage.json" \
+ "${MOCK_BUCKET}" 0 20690 0
+
+jq '.metrics.last_cycle_result = "partial"' "${TEST_TMP}/validation-scanner.json" > "${TEST_TMP}/bad-scanner.json"
+if validation_scanner_usage_matches "${TEST_TMP}/bad-scanner.json" "${TEST_TMP}/validation-usage.json" "${MOCK_BUCKET}" 0 20690 0; then
+ printf 'partial scanner cycle passed\n' >&2; exit 1
+fi
+jq '.scanner_cycle = 0' "${TEST_TMP}/validation-usage.json" > "${TEST_TMP}/bad-usage.json"
+if validation_scanner_usage_matches "${TEST_TMP}/validation-scanner.json" "${TEST_TMP}/bad-usage.json" "${MOCK_BUCKET}" 0 20690 0; then
+ printf 'stale usage snapshot passed\n' >&2; exit 1
+fi
+jq '.buckets_usage[].size = 1' "${TEST_TMP}/validation-usage.json" > "${TEST_TMP}/bad-usage.json"
+if validation_scanner_usage_matches "${TEST_TMP}/validation-scanner.json" "${TEST_TMP}/bad-usage.json" "${MOCK_BUCKET}" 0 20690 0; then
+ printf 'wrong fixture size passed\n' >&2; exit 1
+fi
+jq '.buckets_usage[].objects_count = 19' "${TEST_TMP}/validation-usage.json" > "${TEST_TMP}/bad-usage.json"
+if validation_scanner_usage_matches "${TEST_TMP}/validation-scanner.json" "${TEST_TMP}/bad-usage.json" "${MOCK_BUCKET}" 0 20690 0; then
+ printf 'wrong object count passed\n' >&2; exit 1
+fi
+jq '.usage_snapshot_complete = false' "${TEST_TMP}/validation-usage.json" > "${TEST_TMP}/bad-usage.json"
+if validation_scanner_usage_matches "${TEST_TMP}/validation-scanner.json" "${TEST_TMP}/bad-usage.json" "${MOCK_BUCKET}" 0 20690 0; then
+ printf 'incomplete usage snapshot passed\n' >&2; exit 1
+fi
+
+MOCK_DELETE_FAIL=1
+: > "${MOCK_OBJECTS_FILE}"
+if test_validation_scanner_usage_convergence; then
+ printf 'failed cleanup passed\n' >&2; exit 1
+fi
+[ "${VALIDATION_OBJECT_CLEANUP_STATUS}" = FAIL ]
+
+"${SCRIPT_DIR}/rustfs-storage-test.sh" --validation-case scanner.usage-convergence \
+ --dry-run --yes --package-url https://example.invalid/rustfs.deb \
+ > "${TEST_TMP}/validation-dry-run.log"
+grep -q '\[DRY-RUN\] scanner.usage-convergence UsageConvergence' "${TEST_TMP}/validation-dry-run.log"
+grep -q 'RUSTFS_SCANNER_CYCLE=5' "${TEST_TMP}/validation-dry-run.log"
+if grep -q '\[DRY-RUN\] STOR-' "${TEST_TMP}/validation-dry-run.log"; then
+ printf 'validation mode scheduled legacy storage cases\n' >&2; exit 1
+fi
+
+"${SCRIPT_DIR}/rustfs-storage-test.sh" --validation-case scanner.status-readiness \
+ --dry-run --yes --package-url https://example.invalid/rustfs.deb \
+ > "${TEST_TMP}/validation-status-dry-run.log"
+grep -q '\[DRY-RUN\] scanner.status-readiness StatusReadiness' "${TEST_TMP}/validation-status-dry-run.log"
+grep -q 'RUSTFS_VOLUMES="http://rustfs-node{1...2}:9000/data/rustfs{1...4}/mnmd"' "${TEST_TMP}/validation-status-dry-run.log"
+[ "$(grep -c 'RUSTFS_SCANNER_CYCLE=5' "${TEST_TMP}/validation-status-dry-run.log")" -eq 2 ]
+
+jq -nc --argjson now "$(date -u +%s)" '{enabled:true,
+ runtime_config:{cycle_interval_seconds:{value:5,source:"env"},start_delay_seconds:{value:0,source:"env"}},
+ metrics:{leader_lock_held_by_this_process:true,leader_lock_state:"acquired",last_cycle_result:"success",last_cycle_end_unix_secs:$now},
+ freshness:{state:"fresh"},cycle_schedule:{execution_role:"leader"}}' > "${TEST_TMP}/ready-leader.json"
+jq -n '{enabled:true,
+ runtime_config:{cycle_interval_seconds:{value:5,source:"env"},start_delay_seconds:{value:0,source:"env"}},
+ metrics:{leader_lock_held_by_this_process:false},freshness:{state:"fresh"},cycle_schedule:{execution_role:"follower"}}' \
+ > "${TEST_TMP}/ready-follower.json"
+validation_scanner_status_ready "${TEST_TMP}/ready-leader.json" "${TEST_TMP}/ready-follower.json" 0
+jq '.runtime_config.cycle_interval_seconds.value = 60' "${TEST_TMP}/ready-follower.json" > "${TEST_TMP}/wrong-config.json"
+if validation_scanner_status_ready "${TEST_TMP}/ready-leader.json" "${TEST_TMP}/wrong-config.json" 0; then
+ printf 'wrong scanner configuration passed\n' >&2; exit 1
+fi
+if validation_scanner_status_ready "${TEST_TMP}/ready-leader.json" "${TEST_TMP}/ready-leader.json" 0; then
+ printf 'two scanner leaders passed\n' >&2; exit 1
+fi
diff --git a/test_validation_tier.sh b/test_validation_tier.sh
new file mode 100644
index 00000000..5155434a
--- /dev/null
+++ b/test_validation_tier.sh
@@ -0,0 +1,85 @@
+#!/usr/bin/env bash
+set -Eeuo pipefail
+
+TEST_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+TEST_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/rustfs-validation-tier.XXXXXX")"
+export RUSTFS_TIER_TEST_TMP="${TEST_ROOT}/source"
+export TIER_SOURCE_ONLY=1
+# shellcheck source=rustfs-tier-test.sh
+source "${TEST_SCRIPT_DIR}/rustfs-tier-test.sh"
+[ "${SCRIPT_DIR}" = "${TEST_SCRIPT_DIR}" ]
+trap 'rm -rf -- "${TEST_ROOT}"' EXIT
+
+RUN_TOKEN=unit-1
+MOCK_CORRUPT=0
+MOCK_GET_COUNT=0
+
+new_bucket() { :; }
+add_tier() { RC_CODE=0; }
+rc_run() { RC_CODE=0; }
+run_transition() { printf '{"job_id":"job-1"}\n'; }
+wait_transition_job() { [ "$1" = job-1 ]; }
+sleep() { :; }
+
+aws_run() {
+ local operation="${2:-}" argument previous="" output=""
+ AWS_CODE=0
+ AWS_OUT='{}'
+ for argument in "$@"; do
+ if [ "${previous}" = "--bucket" ]; then
+ [ "${argument}" = "val-tier-unit-1" ] || return 1
+ fi
+ previous="${argument}"
+ done
+ case "${operation}" in
+ put-object) ;;
+ head-object)
+ if [ "${MOCK_GET_COUNT}" -ge 1 ]; then
+ AWS_OUT='{"StorageClass":"WARM","Restore":"ongoing-request=\"false\""}'
+ else
+ AWS_OUT='{"StorageClass":"WARM"}'
+ fi
+ ;;
+ get-object)
+ MOCK_GET_COUNT=$((MOCK_GET_COUNT + 1))
+ output="${*: -1}"
+ if [ "${MOCK_CORRUPT}" -eq 1 ]; then
+ printf 'corrupt' > "${output}"
+ else
+ cp "${TEST_TMP}/validation-source.bin" "${output}"
+ fi
+ ;;
+ restore-object) ;;
+ *) printf 'unexpected AWS operation: %s\n' "${operation}" >&2; return 1 ;;
+ esac
+}
+
+test_validation_tiering_transition_and_restore
+[ "${MOCK_GET_COUNT}" -eq 2 ]
+jq -e '.tier == "WARM" and .restore_complete == true and
+ (.source_sha256 | test("^[0-9a-f]{64}$")) and
+ .source_sha256 == .transitioned_sha256 and .source_sha256 == .restored_sha256' \
+ "${TEST_TMP}/validation-object.json" >/dev/null
+
+MOCK_CORRUPT=1
+MOCK_GET_COUNT=0
+if test_validation_tiering_transition_and_restore; then
+ printf 'corrupted transitioned object passed\n' >&2
+ exit 1
+fi
+
+TIER_SOURCE_ONLY=0 "${TEST_SCRIPT_DIR}/rustfs-tier-test.sh" --validation-case tiering.transition-and-restore \
+ --topology single-single --dry-run --yes --package-url https://example.invalid/rustfs.deb \
+ > "${TEST_ROOT}/validation-dry-run.log"
+grep -q '\[DRY-RUN\] tiering.transition-and-restore TransitionAndRestore' "${TEST_ROOT}/validation-dry-run.log"
+if grep -q '\[DRY-RUN\] TIER-' "${TEST_ROOT}/validation-dry-run.log"; then
+ printf 'validation mode scheduled legacy tier cases\n' >&2
+ exit 1
+fi
+
+for case_id in events.delivery audit.request-correlation; do
+ TIER_SOURCE_ONLY=0 "${TEST_SCRIPT_DIR}/rustfs-tier-test.sh" --validation-case "${case_id}" \
+ --topology single-single --dry-run --yes --package-url https://example.invalid/rustfs.deb \
+ > "${TEST_ROOT}/${case_id}.log"
+ grep -q "\[DRY-RUN\] ${case_id}" "${TEST_ROOT}/${case_id}.log"
+done
diff --git a/test_validation_webhooks.py b/test_validation_webhooks.py
new file mode 100644
index 00000000..940e5965
--- /dev/null
+++ b/test_validation_webhooks.py
@@ -0,0 +1,74 @@
+"""Contract checks for canonical webhook evidence parsing."""
+
+import os
+from pathlib import Path
+import tempfile
+import unittest
+from unittest.mock import patch
+
+from scripts.validation_webhooks import audit_summary, event_summary, records
+
+
+class ValidationWebhooksTest(unittest.TestCase):
+ def test_event_requires_exactly_one_matching_put(self):
+ event = {"Records": [{
+ "eventName": "s3:ObjectCreated:Put",
+ "s3": {"bucket": {"name": "bucket"}, "object": {"key": "round+trip.bin"}},
+ }]}
+ self.assertEqual(event_summary([event], "bucket", "round trip.bin")["delivery_count"], 1)
+ with self.assertRaisesRegex(ValueError, "found 2"):
+ event_summary([event, event], "bucket", "round trip.bin")
+ with self.assertRaisesRegex(ValueError, "unexpected event type"):
+ event_summary([{"Records": [{**event["Records"][0], "eventName": "s3:ObjectRemoved:Delete"}]}], "bucket", "round trip.bin")
+
+ def test_audit_requires_correlated_operations_and_no_secret(self):
+ put = {"api": {"name": "PutObject", "bucket": "bucket", "object": "key", "status_code": 200},
+ "requestID": "request-put"}
+ get = {"api": {"name": "GetObject", "bucket": "bucket", "object": "key", "status_code": 200},
+ "requestID": "request-get"}
+ self.assertEqual(set(audit_summary([put, get], "bucket", "key")["operations"]), {"PutObject", "GetObject"})
+ with self.assertRaisesRegex(ValueError, "share a request ID"):
+ audit_summary([put, {**get, "requestID": "request-put"}], "bucket", "key")
+ with self.assertRaisesRegex(ValueError, "lacks requestID"):
+ audit_summary([put, {**get, "requestID": ""}], "bucket", "key")
+ with patch.dict(os.environ, {"RUSTFS_SECRET_KEY": "private-value"}):
+ with self.assertRaisesRegex(ValueError, "exposes RUSTFS_SECRET_KEY"):
+ audit_summary([put, {**get, "requestHeader": {"x-secret": "private-value"}}], "bucket", "key")
+ public_identity = {**put, "accessKey": "public-access-key"}
+ self.assertEqual(
+ audit_summary([public_identity, get], "bucket", "key")["secret_check"],
+ "PASS",
+ )
+
+ def test_records_rejects_non_object_jsonl(self):
+ with tempfile.TemporaryDirectory() as directory:
+ path = Path(directory) / "events.jsonl"
+ path.write_text('{}\n[]\n', encoding="utf-8")
+ with self.assertRaisesRegex(ValueError, "line 2"):
+ records(path)
+
+ def test_audit_accepts_s3_operation_names_and_keeps_boundaries(self):
+ def entry(name, request_id, key="key"):
+ return {"api": {"name": name, "bucket": "bucket", "object": key, "status_code": 200},
+ "requestID": request_id}
+ put = entry("s3:PutObject", "put-id")
+ get = entry("s3:GetObject", "get-id")
+ result = audit_summary([put, get], "bucket", "key")
+ self.assertEqual(result["operations"]["GetObject"]["api_name"], "s3:GetObject")
+ wrapped = [{"Records": [put]}, {"Records": [get]}]
+ self.assertEqual(audit_summary(wrapped, "bucket", "key"), result)
+ with self.assertRaisesRegex(ValueError, "array of objects"):
+ audit_summary([{"Records": [put, "invalid"]}], "bucket", "key")
+ with self.assertRaisesRegex(ValueError, "not both delivered"):
+ audit_summary([put, entry("s3:GetObject", "other-id", "other-key")], "bucket", "key")
+ with self.assertRaisesRegex(ValueError, "not both delivered"):
+ audit_summary([put, entry("admin:GetObject", "admin-id")], "bucket", "key")
+ with patch.dict(os.environ, {"RUSTFS_SECRET_KEY": "private-value"}):
+ with self.assertRaisesRegex(ValueError, "exposes RUSTFS_SECRET_KEY"):
+ audit_summary([put, {**get, "accessKey": "private-value"}], "bucket", "key")
+ with self.assertRaisesRegex(ValueError, "exposes RUSTFS_SECRET_KEY"):
+ audit_summary([{"Records": [put, {**get, "accessKey": "private-value"}]}], "bucket", "key")
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/testdata/i05-historical-get-39711742431.ndjson b/testdata/i05-historical-get-39711742431.ndjson
new file mode 100644
index 00000000..ce873b30
--- /dev/null
+++ b/testdata/i05-historical-get-39711742431.ndjson
@@ -0,0 +1,3 @@
+{"acquire_ms":0,"bucket":"rv-heal-bd855cc838e268dc","component":"ecstore","event":"object_namespace_lock","filename":"crates/ecstore/src/diagnostics/object_lock.rs","hold_ms":0,"level":"TRACE","line_number":159,"lock_attempt_id":"3a86c2cc-ada5-4e8a-9e0d-dbe3bb7c1489","message":"Object namespace lock diagnostic","mode":"read","namespace":"pool-0","object":"objects/0000.bin","op":"get_object","owner":":::9000","request-id":"1ba2503b-78ea-40cd-b2ff-003f54662c9c","request_id":"1ba2503b-78ea-40cd-b2ff-003f54662c9c","requested_version_id":"d34e1ece-27de-4e0e-967e-e9238b61132d","span":{"content_length":"0","method":"GET","name":"http-request","peer_addr":"::ffff:10.0.0.4","request_id":"1ba2503b-78ea-40cd-b2ff-003f54662c9c","span_id":"unknown","trace_id":"unknown","uri":"/rv-heal-bd855cc838e268dc/objects/0000.bin?versionId=d34e1ece-27de-4e0e-967e-e9238b61132d","user_agent":"auto-testing-i05/7629f31a-8105-40e8-b287-cd86965b37ff","version":"HTTP/1.1"},"spans":[{"content_length":"0","method":"GET","name":"http-request","peer_addr":"::ffff:10.0.0.4","request_id":"1ba2503b-78ea-40cd-b2ff-003f54662c9c","span_id":"unknown","trace_id":"unknown","uri":"/rv-heal-bd855cc838e268dc/objects/0000.bin?versionId=d34e1ece-27de-4e0e-967e-e9238b61132d","user_agent":"auto-testing-i05/7629f31a-8105-40e8-b287-cd86965b37ff","version":"HTTP/1.1"}],"state":"acquiring","subsystem":"object_lock","target":"rustfs_ecstore::object_lock_diag","threadId":"ThreadId(7)","threadName":"rustfs-worker","timeout_ms":1000,"timestamp":"2026-10-22T02:31:14.276873476Z"}
+{"acquire_ms":0,"bucket":"rv-heal-bd855cc838e268dc","component":"ecstore","event":"object_namespace_lock","filename":"crates/ecstore/src/diagnostics/object_lock.rs","hold_ms":0,"level":"TRACE","line_number":159,"lock_attempt_id":"3a86c2cc-ada5-4e8a-9e0d-dbe3bb7c1489","message":"Object namespace lock diagnostic","mode":"read","namespace":"pool-0","object":"objects/0000.bin","op":"get_object","owner":":::9000","request-id":"1ba2503b-78ea-40cd-b2ff-003f54662c9c","request_id":"1ba2503b-78ea-40cd-b2ff-003f54662c9c","requested_version_id":"d34e1ece-27de-4e0e-967e-e9238b61132d","span":{"content_length":"0","method":"GET","name":"http-request","peer_addr":"::ffff:10.0.0.4","request_id":"1ba2503b-78ea-40cd-b2ff-003f54662c9c","span_id":"unknown","trace_id":"unknown","uri":"/rv-heal-bd855cc838e268dc/objects/0000.bin?versionId=d34e1ece-27de-4e0e-967e-e9238b61132d","user_agent":"auto-testing-i05/7629f31a-8105-40e8-b287-cd86965b37ff","version":"HTTP/1.1"},"spans":[{"content_length":"0","method":"GET","name":"http-request","peer_addr":"::ffff:10.0.0.4","request_id":"1ba2503b-78ea-40cd-b2ff-003f54662c9c","span_id":"unknown","trace_id":"unknown","uri":"/rv-heal-bd855cc838e268dc/objects/0000.bin?versionId=d34e1ece-27de-4e0e-967e-e9238b61132d","user_agent":"auto-testing-i05/7629f31a-8105-40e8-b287-cd86965b37ff","version":"HTTP/1.1"}],"state":"acquired","subsystem":"object_lock","target":"rustfs_ecstore::object_lock_diag","threadId":"ThreadId(7)","threadName":"rustfs-worker","timeout_ms":1000,"timestamp":"2026-10-22T02:31:14.276939705Z"}
+{"acquire_ms":0,"bucket":"rv-heal-bd855cc838e268dc","component":"ecstore","event":"object_namespace_lock","filename":"crates/ecstore/src/diagnostics/object_lock.rs","hold_ms":0,"level":"TRACE","line_number":159,"lock_attempt_id":"3a86c2cc-ada5-4e8a-9e0d-dbe3bb7c1489","message":"Object namespace lock diagnostic","mode":"read","namespace":"pool-0","object":"objects/0000.bin","op":"get_object","owner":":::9000","request-id":"1ba2503b-78ea-40cd-b2ff-003f54662c9c","request_id":"1ba2503b-78ea-40cd-b2ff-003f54662c9c","requested_version_id":"d34e1ece-27de-4e0e-967e-e9238b61132d","span":{"content_length":"0","method":"GET","name":"http-request","peer_addr":"::ffff:10.0.0.4","request_id":"1ba2503b-78ea-40cd-b2ff-003f54662c9c","span_id":"unknown","trace_id":"unknown","uri":"/rv-heal-bd855cc838e268dc/objects/0000.bin?versionId=d34e1ece-27de-4e0e-967e-e9238b61132d","user_agent":"auto-testing-i05/7629f31a-8105-40e8-b287-cd86965b37ff","version":"HTTP/1.1"},"spans":[{"content_length":"0","method":"GET","name":"http-request","peer_addr":"::ffff:10.0.0.4","request_id":"1ba2503b-78ea-40cd-b2ff-003f54662c9c","span_id":"unknown","trace_id":"unknown","uri":"/rv-heal-bd855cc838e268dc/objects/0000.bin?versionId=d34e1ece-27de-4e0e-967e-e9238b61132d","user_agent":"auto-testing-i05/7629f31a-8105-40e8-b287-cd86965b37ff","version":"HTTP/1.1"}],"state":"guard_dropped","subsystem":"object_lock","target":"rustfs_ecstore::object_lock_diag","threadId":"ThreadId(8)","threadName":"rustfs-worker","timeout_ms":1000,"timestamp":"2026-10-22T02:31:14.277240674Z"}
diff --git a/testdata/i05-successor-metadata-39788321908.ndjson b/testdata/i05-successor-metadata-39788321908.ndjson
new file mode 100644
index 00000000..8313e3a6
--- /dev/null
+++ b/testdata/i05-successor-metadata-39788321908.ndjson
@@ -0,0 +1,15 @@
+{"acquire_ms": 0, "bucket": ".rustfs.sys", "component": "ecstore", "event": "object_namespace_lock", "filename": "crates/ecstore/src/diagnostics/object_lock.rs", "hold_ms": 0, "level": "TRACE", "line_number": 159, "lock_attempt_id": "010f7a1e-4727-4e75-837b-5495a2e08917", "message": "Object namespace lock diagnostic", "mode": "read", "namespace": "pool-0", "object": "buckets/rv-heal-5dd2430e083c6140/.bucket-incarnation", "op": "get_object", "owner": ":::9000", "request-id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "request_id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "span": {"content_length": "1048773", "content_type": "application/octet-stream", "method": "PUT", "name": "http-request", "peer_addr": "::ffff:10.0.0.4", "request_id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "span_id": "unknown", "trace_id": "unknown", "uri": "/rv-heal-5dd2430e083c6140/objects/0000.bin", "user_agent": "Boto3/1.43.77 md/Botocore#1.43.77 ua/2.1 os/linux#6.17.0-1022-azure md/arch#x86_64 lang/python#3.12.3 md/pyimpl#CPython m/N,Z,D,b,e cfg/retry-mode#legacy Botocore/1.43.77 auto-testing-i05-successor/73a9d02c-6579-44ce-8152-011f367dc891", "version": "HTTP/1.1"}, "state": "acquiring", "subsystem": "object_lock", "target": "rustfs_ecstore::object_lock_diag", "threadId": "ThreadId(9)", "threadName": "rustfs-worker", "timeout_ms": 1000, "timestamp": "2026-10-22T13:37:19.815580878Z"}
+{"acquire_ms": 0, "bucket": ".rustfs.sys", "component": "ecstore", "event": "object_namespace_lock", "filename": "crates/ecstore/src/diagnostics/object_lock.rs", "hold_ms": 0, "level": "TRACE", "line_number": 159, "lock_attempt_id": "010f7a1e-4727-4e75-837b-5495a2e08917", "message": "Object namespace lock diagnostic", "mode": "read", "namespace": "pool-0", "object": "buckets/rv-heal-5dd2430e083c6140/.bucket-incarnation", "op": "get_object", "owner": ":::9000", "request-id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "request_id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "span": {"content_length": "1048773", "content_type": "application/octet-stream", "method": "PUT", "name": "http-request", "peer_addr": "::ffff:10.0.0.4", "request_id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "span_id": "unknown", "trace_id": "unknown", "uri": "/rv-heal-5dd2430e083c6140/objects/0000.bin", "user_agent": "Boto3/1.43.77 md/Botocore#1.43.77 ua/2.1 os/linux#6.17.0-1022-azure md/arch#x86_64 lang/python#3.12.3 md/pyimpl#CPython m/N,Z,D,b,e cfg/retry-mode#legacy Botocore/1.43.77 auto-testing-i05-successor/73a9d02c-6579-44ce-8152-011f367dc891", "version": "HTTP/1.1"}, "state": "acquired", "subsystem": "object_lock", "target": "rustfs_ecstore::object_lock_diag", "threadId": "ThreadId(9)", "threadName": "rustfs-worker", "timeout_ms": 1000, "timestamp": "2026-10-22T13:37:19.81565574Z"}
+{"acquire_ms": 0, "bucket": ".rustfs.sys", "component": "ecstore", "event": "object_namespace_lock", "filename": "crates/ecstore/src/diagnostics/object_lock.rs", "hold_ms": 0, "level": "TRACE", "line_number": 159, "lock_attempt_id": "010f7a1e-4727-4e75-837b-5495a2e08917", "message": "Object namespace lock diagnostic", "mode": "read", "namespace": "pool-0", "object": "buckets/rv-heal-5dd2430e083c6140/.bucket-incarnation", "op": "get_object", "owner": ":::9000", "request-id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "request_id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "span": {"content_length": "1048773", "content_type": "application/octet-stream", "method": "PUT", "name": "http-request", "peer_addr": "::ffff:10.0.0.4", "request_id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "span_id": "unknown", "trace_id": "unknown", "uri": "/rv-heal-5dd2430e083c6140/objects/0000.bin", "user_agent": "Boto3/1.43.77 md/Botocore#1.43.77 ua/2.1 os/linux#6.17.0-1022-azure md/arch#x86_64 lang/python#3.12.3 md/pyimpl#CPython m/N,Z,D,b,e cfg/retry-mode#legacy Botocore/1.43.77 auto-testing-i05-successor/73a9d02c-6579-44ce-8152-011f367dc891", "version": "HTTP/1.1"}, "state": "guard_dropped", "subsystem": "object_lock", "target": "rustfs_ecstore::object_lock_diag", "threadId": "ThreadId(9)", "threadName": "rustfs-worker", "timeout_ms": 1000, "timestamp": "2026-10-22T13:37:19.815935188Z"}
+{"acquire_ms": 0, "bucket": "rv-heal-5dd2430e083c6140", "component": "ecstore", "event": "object_namespace_lock", "filename": "crates/ecstore/src/diagnostics/object_lock.rs", "hold_ms": 0, "level": "TRACE", "line_number": 159, "lock_attempt_id": "b31b1a30-9431-425e-9a92-909b47ac6bfc", "message": "Object namespace lock diagnostic", "mode": "write", "namespace": "pool-0", "object": "objects/0000.bin", "op": "put_object_commit", "owner": ":::9000", "request-id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "request_id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "span": {"content_length": "1048773", "content_type": "application/octet-stream", "method": "PUT", "name": "http-request", "peer_addr": "::ffff:10.0.0.4", "request_id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "span_id": "unknown", "trace_id": "unknown", "uri": "/rv-heal-5dd2430e083c6140/objects/0000.bin", "user_agent": "Boto3/1.43.77 md/Botocore#1.43.77 ua/2.1 os/linux#6.17.0-1022-azure md/arch#x86_64 lang/python#3.12.3 md/pyimpl#CPython m/N,Z,D,b,e cfg/retry-mode#legacy Botocore/1.43.77 auto-testing-i05-successor/73a9d02c-6579-44ce-8152-011f367dc891", "version": "HTTP/1.1"}, "state": "acquiring", "subsystem": "object_lock", "target": "rustfs_ecstore::object_lock_diag", "threadId": "ThreadId(9)", "threadName": "rustfs-worker", "timeout_ms": 60000, "timestamp": "2026-10-22T13:37:19.815977441Z"}
+{"acquire_ms": 260, "bucket": "rv-heal-5dd2430e083c6140", "component": "ecstore", "event": "object_namespace_lock", "filename": "crates/ecstore/src/diagnostics/object_lock.rs", "hold_ms": 0, "level": "TRACE", "line_number": 159, "lock_attempt_id": "b31b1a30-9431-425e-9a92-909b47ac6bfc", "message": "Object namespace lock diagnostic", "mode": "write", "namespace": "pool-0", "object": "objects/0000.bin", "op": "put_object_commit", "owner": ":::9000", "request-id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "request_id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "span": {"content_length": "1048773", "content_type": "application/octet-stream", "method": "PUT", "name": "http-request", "peer_addr": "::ffff:10.0.0.4", "request_id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "span_id": "unknown", "trace_id": "unknown", "uri": "/rv-heal-5dd2430e083c6140/objects/0000.bin", "user_agent": "Boto3/1.43.77 md/Botocore#1.43.77 ua/2.1 os/linux#6.17.0-1022-azure md/arch#x86_64 lang/python#3.12.3 md/pyimpl#CPython m/N,Z,D,b,e cfg/retry-mode#legacy Botocore/1.43.77 auto-testing-i05-successor/73a9d02c-6579-44ce-8152-011f367dc891", "version": "HTTP/1.1"}, "state": "acquired", "subsystem": "object_lock", "target": "rustfs_ecstore::object_lock_diag", "threadId": "ThreadId(8)", "threadName": "rustfs-worker", "timeout_ms": 60000, "timestamp": "2026-10-22T13:37:20.076585778Z"}
+{"acquire_ms": 0, "bucket": ".rustfs.sys", "component": "ecstore", "event": "object_namespace_lock", "filename": "crates/ecstore/src/diagnostics/object_lock.rs", "hold_ms": 0, "level": "TRACE", "line_number": 159, "lock_attempt_id": "cab752a7-f3c2-4f03-a127-c474bb0828f0", "message": "Object namespace lock diagnostic", "mode": "read", "namespace": "pool-0", "object": "buckets/rv-heal-5dd2430e083c6140/.metadata.bin", "op": "get_object", "owner": ":::9000", "request-id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "request_id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "span": {"content_length": "1048773", "content_type": "application/octet-stream", "method": "PUT", "name": "http-request", "peer_addr": "::ffff:10.0.0.4", "request_id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "span_id": "unknown", "trace_id": "unknown", "uri": "/rv-heal-5dd2430e083c6140/objects/0000.bin", "user_agent": "Boto3/1.43.77 md/Botocore#1.43.77 ua/2.1 os/linux#6.17.0-1022-azure md/arch#x86_64 lang/python#3.12.3 md/pyimpl#CPython m/N,Z,D,b,e cfg/retry-mode#legacy Botocore/1.43.77 auto-testing-i05-successor/73a9d02c-6579-44ce-8152-011f367dc891", "version": "HTTP/1.1"}, "state": "acquiring", "subsystem": "object_lock", "target": "rustfs_ecstore::object_lock_diag", "threadId": "ThreadId(8)", "threadName": "rustfs-worker", "timeout_ms": 1000, "timestamp": "2026-10-22T13:37:20.076775592Z"}
+{"acquire_ms": 0, "bucket": ".rustfs.sys", "component": "ecstore", "event": "object_namespace_lock", "filename": "crates/ecstore/src/diagnostics/object_lock.rs", "hold_ms": 0, "level": "TRACE", "line_number": 159, "lock_attempt_id": "cab752a7-f3c2-4f03-a127-c474bb0828f0", "message": "Object namespace lock diagnostic", "mode": "read", "namespace": "pool-0", "object": "buckets/rv-heal-5dd2430e083c6140/.metadata.bin", "op": "get_object", "owner": ":::9000", "request-id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "request_id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "span": {"content_length": "1048773", "content_type": "application/octet-stream", "method": "PUT", "name": "http-request", "peer_addr": "::ffff:10.0.0.4", "request_id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "span_id": "unknown", "trace_id": "unknown", "uri": "/rv-heal-5dd2430e083c6140/objects/0000.bin", "user_agent": "Boto3/1.43.77 md/Botocore#1.43.77 ua/2.1 os/linux#6.17.0-1022-azure md/arch#x86_64 lang/python#3.12.3 md/pyimpl#CPython m/N,Z,D,b,e cfg/retry-mode#legacy Botocore/1.43.77 auto-testing-i05-successor/73a9d02c-6579-44ce-8152-011f367dc891", "version": "HTTP/1.1"}, "state": "acquired", "subsystem": "object_lock", "target": "rustfs_ecstore::object_lock_diag", "threadId": "ThreadId(8)", "threadName": "rustfs-worker", "timeout_ms": 1000, "timestamp": "2026-10-22T13:37:20.076806618Z"}
+{"acquire_ms": 0, "bucket": ".rustfs.sys", "component": "ecstore", "event": "object_namespace_lock", "filename": "crates/ecstore/src/diagnostics/object_lock.rs", "hold_ms": 0, "level": "TRACE", "line_number": 159, "lock_attempt_id": "cab752a7-f3c2-4f03-a127-c474bb0828f0", "message": "Object namespace lock diagnostic", "mode": "read", "namespace": "pool-0", "object": "buckets/rv-heal-5dd2430e083c6140/.metadata.bin", "op": "get_object", "owner": ":::9000", "request-id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "request_id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "span": {"content_length": "1048773", "content_type": "application/octet-stream", "method": "PUT", "name": "http-request", "peer_addr": "::ffff:10.0.0.4", "request_id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "span_id": "unknown", "trace_id": "unknown", "uri": "/rv-heal-5dd2430e083c6140/objects/0000.bin", "user_agent": "Boto3/1.43.77 md/Botocore#1.43.77 ua/2.1 os/linux#6.17.0-1022-azure md/arch#x86_64 lang/python#3.12.3 md/pyimpl#CPython m/N,Z,D,b,e cfg/retry-mode#legacy Botocore/1.43.77 auto-testing-i05-successor/73a9d02c-6579-44ce-8152-011f367dc891", "version": "HTTP/1.1"}, "state": "guard_dropped", "subsystem": "object_lock", "target": "rustfs_ecstore::object_lock_diag", "threadId": "ThreadId(7)", "threadName": "rustfs-worker", "timeout_ms": 1000, "timestamp": "2026-10-22T13:37:20.077111504Z"}
+{"acquire_ms": 0, "bucket": ".rustfs.sys", "component": "ecstore", "event": "object_namespace_lock", "filename": "crates/ecstore/src/diagnostics/object_lock.rs", "hold_ms": 0, "level": "TRACE", "line_number": 159, "lock_attempt_id": "681c651c-1aed-4441-8e63-0eb601b67c3e", "message": "Object namespace lock diagnostic", "mode": "read", "namespace": "pool-0", "object": "buckets/rv-heal-5dd2430e083c6140/.bucket-incarnation", "op": "get_object", "owner": ":::9000", "request-id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "request_id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "span": {"content_length": "1048773", "content_type": "application/octet-stream", "method": "PUT", "name": "http-request", "peer_addr": "::ffff:10.0.0.4", "request_id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "span_id": "unknown", "trace_id": "unknown", "uri": "/rv-heal-5dd2430e083c6140/objects/0000.bin", "user_agent": "Boto3/1.43.77 md/Botocore#1.43.77 ua/2.1 os/linux#6.17.0-1022-azure md/arch#x86_64 lang/python#3.12.3 md/pyimpl#CPython m/N,Z,D,b,e cfg/retry-mode#legacy Botocore/1.43.77 auto-testing-i05-successor/73a9d02c-6579-44ce-8152-011f367dc891", "version": "HTTP/1.1"}, "state": "acquiring", "subsystem": "object_lock", "target": "rustfs_ecstore::object_lock_diag", "threadId": "ThreadId(7)", "threadName": "rustfs-worker", "timeout_ms": 1000, "timestamp": "2026-10-22T13:37:20.077174268Z"}
+{"acquire_ms": 0, "bucket": ".rustfs.sys", "component": "ecstore", "event": "object_namespace_lock", "filename": "crates/ecstore/src/diagnostics/object_lock.rs", "hold_ms": 0, "level": "TRACE", "line_number": 159, "lock_attempt_id": "681c651c-1aed-4441-8e63-0eb601b67c3e", "message": "Object namespace lock diagnostic", "mode": "read", "namespace": "pool-0", "object": "buckets/rv-heal-5dd2430e083c6140/.bucket-incarnation", "op": "get_object", "owner": ":::9000", "request-id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "request_id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "span": {"content_length": "1048773", "content_type": "application/octet-stream", "method": "PUT", "name": "http-request", "peer_addr": "::ffff:10.0.0.4", "request_id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "span_id": "unknown", "trace_id": "unknown", "uri": "/rv-heal-5dd2430e083c6140/objects/0000.bin", "user_agent": "Boto3/1.43.77 md/Botocore#1.43.77 ua/2.1 os/linux#6.17.0-1022-azure md/arch#x86_64 lang/python#3.12.3 md/pyimpl#CPython m/N,Z,D,b,e cfg/retry-mode#legacy Botocore/1.43.77 auto-testing-i05-successor/73a9d02c-6579-44ce-8152-011f367dc891", "version": "HTTP/1.1"}, "state": "acquired", "subsystem": "object_lock", "target": "rustfs_ecstore::object_lock_diag", "threadId": "ThreadId(7)", "threadName": "rustfs-worker", "timeout_ms": 1000, "timestamp": "2026-10-22T13:37:20.077205675Z"}
+{"acquire_ms": 0, "bucket": ".rustfs.sys", "component": "ecstore", "event": "object_namespace_lock", "filename": "crates/ecstore/src/diagnostics/object_lock.rs", "hold_ms": 0, "level": "TRACE", "line_number": 159, "lock_attempt_id": "681c651c-1aed-4441-8e63-0eb601b67c3e", "message": "Object namespace lock diagnostic", "mode": "read", "namespace": "pool-0", "object": "buckets/rv-heal-5dd2430e083c6140/.bucket-incarnation", "op": "get_object", "owner": ":::9000", "request-id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "request_id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "span": {"content_length": "1048773", "content_type": "application/octet-stream", "method": "PUT", "name": "http-request", "peer_addr": "::ffff:10.0.0.4", "request_id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "span_id": "unknown", "trace_id": "unknown", "uri": "/rv-heal-5dd2430e083c6140/objects/0000.bin", "user_agent": "Boto3/1.43.77 md/Botocore#1.43.77 ua/2.1 os/linux#6.17.0-1022-azure md/arch#x86_64 lang/python#3.12.3 md/pyimpl#CPython m/N,Z,D,b,e cfg/retry-mode#legacy Botocore/1.43.77 auto-testing-i05-successor/73a9d02c-6579-44ce-8152-011f367dc891", "version": "HTTP/1.1"}, "state": "guard_dropped", "subsystem": "object_lock", "target": "rustfs_ecstore::object_lock_diag", "threadId": "ThreadId(9)", "threadName": "rustfs-worker", "timeout_ms": 1000, "timestamp": "2026-10-22T13:37:20.0773692Z"}
+{"acquire_ms": 260, "bucket": "rv-heal-5dd2430e083c6140", "component": "ecstore", "event": "object_namespace_lock", "filename": "crates/ecstore/src/diagnostics/object_lock.rs", "hold_ms": 8, "level": "TRACE", "line_number": 159, "lock_attempt_id": "b31b1a30-9431-425e-9a92-909b47ac6bfc", "message": "Object namespace lock diagnostic", "mode": "write", "namespace": "pool-0", "object": "objects/0000.bin", "op": "put_object_commit", "owner": ":::9000", "request-id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "request_id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "span": {"content_length": "1048773", "content_type": "application/octet-stream", "method": "PUT", "name": "http-request", "peer_addr": "::ffff:10.0.0.4", "request_id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "span_id": "unknown", "trace_id": "unknown", "uri": "/rv-heal-5dd2430e083c6140/objects/0000.bin", "user_agent": "Boto3/1.43.77 md/Botocore#1.43.77 ua/2.1 os/linux#6.17.0-1022-azure md/arch#x86_64 lang/python#3.12.3 md/pyimpl#CPython m/N,Z,D,b,e cfg/retry-mode#legacy Botocore/1.43.77 auto-testing-i05-successor/73a9d02c-6579-44ce-8152-011f367dc891", "version": "HTTP/1.1"}, "state": "guard_dropped", "subsystem": "object_lock", "target": "rustfs_ecstore::object_lock_diag", "threadId": "ThreadId(10)", "threadName": "rustfs-worker", "timeout_ms": 60000, "timestamp": "2026-10-22T13:37:20.085553232Z"}
+{"acquire_ms": 0, "bucket": ".rustfs.sys", "component": "ecstore", "event": "object_namespace_lock", "filename": "crates/ecstore/src/diagnostics/object_lock.rs", "hold_ms": 0, "level": "TRACE", "line_number": 159, "lock_attempt_id": "6c6613ad-fe92-47c6-949f-b4c934cf8ef0", "message": "Object namespace lock diagnostic", "mode": "read", "namespace": "pool-0", "object": "buckets/rv-heal-5dd2430e083c6140/.bucket-incarnation", "op": "get_object", "owner": ":::9000", "request-id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "request_id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "span": {"content_length": "1048773", "content_type": "application/octet-stream", "method": "PUT", "name": "http-request", "peer_addr": "::ffff:10.0.0.4", "request_id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "span_id": "unknown", "trace_id": "unknown", "uri": "/rv-heal-5dd2430e083c6140/objects/0000.bin", "user_agent": "Boto3/1.43.77 md/Botocore#1.43.77 ua/2.1 os/linux#6.17.0-1022-azure md/arch#x86_64 lang/python#3.12.3 md/pyimpl#CPython m/N,Z,D,b,e cfg/retry-mode#legacy Botocore/1.43.77 auto-testing-i05-successor/73a9d02c-6579-44ce-8152-011f367dc891", "version": "HTTP/1.1"}, "state": "acquiring", "subsystem": "object_lock", "target": "rustfs_ecstore::object_lock_diag", "threadId": "ThreadId(10)", "threadName": "rustfs-worker", "timeout_ms": 1000, "timestamp": "2026-10-22T13:37:20.085660141Z"}
+{"acquire_ms": 0, "bucket": ".rustfs.sys", "component": "ecstore", "event": "object_namespace_lock", "filename": "crates/ecstore/src/diagnostics/object_lock.rs", "hold_ms": 0, "level": "TRACE", "line_number": 159, "lock_attempt_id": "6c6613ad-fe92-47c6-949f-b4c934cf8ef0", "message": "Object namespace lock diagnostic", "mode": "read", "namespace": "pool-0", "object": "buckets/rv-heal-5dd2430e083c6140/.bucket-incarnation", "op": "get_object", "owner": ":::9000", "request-id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "request_id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "span": {"content_length": "1048773", "content_type": "application/octet-stream", "method": "PUT", "name": "http-request", "peer_addr": "::ffff:10.0.0.4", "request_id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "span_id": "unknown", "trace_id": "unknown", "uri": "/rv-heal-5dd2430e083c6140/objects/0000.bin", "user_agent": "Boto3/1.43.77 md/Botocore#1.43.77 ua/2.1 os/linux#6.17.0-1022-azure md/arch#x86_64 lang/python#3.12.3 md/pyimpl#CPython m/N,Z,D,b,e cfg/retry-mode#legacy Botocore/1.43.77 auto-testing-i05-successor/73a9d02c-6579-44ce-8152-011f367dc891", "version": "HTTP/1.1"}, "state": "acquired", "subsystem": "object_lock", "target": "rustfs_ecstore::object_lock_diag", "threadId": "ThreadId(10)", "threadName": "rustfs-worker", "timeout_ms": 1000, "timestamp": "2026-10-22T13:37:20.085684588Z"}
+{"acquire_ms": 0, "bucket": ".rustfs.sys", "component": "ecstore", "event": "object_namespace_lock", "filename": "crates/ecstore/src/diagnostics/object_lock.rs", "hold_ms": 0, "level": "TRACE", "line_number": 159, "lock_attempt_id": "6c6613ad-fe92-47c6-949f-b4c934cf8ef0", "message": "Object namespace lock diagnostic", "mode": "read", "namespace": "pool-0", "object": "buckets/rv-heal-5dd2430e083c6140/.bucket-incarnation", "op": "get_object", "owner": ":::9000", "request-id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "request_id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "span": {"content_length": "1048773", "content_type": "application/octet-stream", "method": "PUT", "name": "http-request", "peer_addr": "::ffff:10.0.0.4", "request_id": "6afbeca5-eefd-4ad0-b2c3-39b6b92ae0a5", "span_id": "unknown", "trace_id": "unknown", "uri": "/rv-heal-5dd2430e083c6140/objects/0000.bin", "user_agent": "Boto3/1.43.77 md/Botocore#1.43.77 ua/2.1 os/linux#6.17.0-1022-azure md/arch#x86_64 lang/python#3.12.3 md/pyimpl#CPython m/N,Z,D,b,e cfg/retry-mode#legacy Botocore/1.43.77 auto-testing-i05-successor/73a9d02c-6579-44ce-8152-011f367dc891", "version": "HTTP/1.1"}, "state": "guard_dropped", "subsystem": "object_lock", "target": "rustfs_ecstore::object_lock_diag", "threadId": "ThreadId(10)", "threadName": "rustfs-worker", "timeout_ms": 1000, "timestamp": "2026-10-22T13:37:20.085869895Z"}
diff --git a/testdata/i05-successor-slow-acquire-39810035277.ndjson b/testdata/i05-successor-slow-acquire-39810035277.ndjson
new file mode 100644
index 00000000..92f1aa44
--- /dev/null
+++ b/testdata/i05-successor-slow-acquire-39810035277.ndjson
@@ -0,0 +1,16 @@
+{"acquire_ms":0,"bucket":".rustfs.sys","component":"ecstore","event":"object_namespace_lock","filename":"crates/ecstore/src/diagnostics/object_lock.rs","hold_ms":0,"level":"TRACE","line_number":159,"lock_attempt_id":"b44942f5-f89b-4b21-a00b-64716e353981","message":"Object namespace lock diagnostic","mode":"read","namespace":"pool-0","object":"buckets/rv-heal-e9da35d01389c2ae/.bucket-incarnation","op":"get_object","owner":"rustfs-node1:9000","request-id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","request_id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","spans":[{"content_length":"1048773","content_type":"application/octet-stream","method":"PUT","name":"http-request","peer_addr":"::ffff:10.0.0.4","request_id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","span_id":"unknown","trace_id":"unknown","uri":"/rv-heal-e9da35d01389c2ae/objects/0000.bin","user_agent":"Boto3/1.43.77 md/Botocore#1.43.77 ua/2.1 os/linux#6.17.0-1022-azure md/arch#x86_64 lang/python#3.12.3 md/pyimpl#CPython m/e,N,Z,b,D cfg/retry-mode#legacy Botocore/1.43.77 auto-testing-i05-successor/5a37a691-d1d8-4591-b5a4-f174b7fe4818","version":"HTTP/1.1"}],"state":"acquiring","subsystem":"object_lock","target":"rustfs_ecstore::object_lock_diag","threadId":"ThreadId(7)","threadName":"rustfs-worker","timeout_ms":1000,"timestamp":"2026-10-22T16:01:37.184272458Z"}
+{"acquire_ms":0,"bucket":".rustfs.sys","component":"ecstore","event":"object_namespace_lock","filename":"crates/ecstore/src/diagnostics/object_lock.rs","hold_ms":0,"level":"TRACE","line_number":159,"lock_attempt_id":"b44942f5-f89b-4b21-a00b-64716e353981","message":"Object namespace lock diagnostic","mode":"read","namespace":"pool-0","object":"buckets/rv-heal-e9da35d01389c2ae/.bucket-incarnation","op":"get_object","owner":"rustfs-node1:9000","request-id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","request_id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","spans":[{"content_length":"1048773","content_type":"application/octet-stream","method":"PUT","name":"http-request","peer_addr":"::ffff:10.0.0.4","request_id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","span_id":"unknown","trace_id":"unknown","uri":"/rv-heal-e9da35d01389c2ae/objects/0000.bin","user_agent":"Boto3/1.43.77 md/Botocore#1.43.77 ua/2.1 os/linux#6.17.0-1022-azure md/arch#x86_64 lang/python#3.12.3 md/pyimpl#CPython m/e,N,Z,b,D cfg/retry-mode#legacy Botocore/1.43.77 auto-testing-i05-successor/5a37a691-d1d8-4591-b5a4-f174b7fe4818","version":"HTTP/1.1"}],"state":"acquired","subsystem":"object_lock","target":"rustfs_ecstore::object_lock_diag","threadId":"ThreadId(10)","threadName":"rustfs-worker","timeout_ms":1000,"timestamp":"2026-10-22T16:01:37.18460302Z"}
+{"acquire_ms":0,"bucket":".rustfs.sys","component":"ecstore","event":"object_namespace_lock","filename":"crates/ecstore/src/diagnostics/object_lock.rs","hold_ms":0,"level":"TRACE","line_number":159,"lock_attempt_id":"b44942f5-f89b-4b21-a00b-64716e353981","message":"Object namespace lock diagnostic","mode":"read","namespace":"pool-0","object":"buckets/rv-heal-e9da35d01389c2ae/.bucket-incarnation","op":"get_object","owner":"rustfs-node1:9000","request-id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","request_id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","spans":[{"content_length":"1048773","content_type":"application/octet-stream","method":"PUT","name":"http-request","peer_addr":"::ffff:10.0.0.4","request_id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","span_id":"unknown","trace_id":"unknown","uri":"/rv-heal-e9da35d01389c2ae/objects/0000.bin","user_agent":"Boto3/1.43.77 md/Botocore#1.43.77 ua/2.1 os/linux#6.17.0-1022-azure md/arch#x86_64 lang/python#3.12.3 md/pyimpl#CPython m/e,N,Z,b,D cfg/retry-mode#legacy Botocore/1.43.77 auto-testing-i05-successor/5a37a691-d1d8-4591-b5a4-f174b7fe4818","version":"HTTP/1.1"}],"state":"guard_dropped","subsystem":"object_lock","target":"rustfs_ecstore::object_lock_diag","threadId":"ThreadId(8)","threadName":"rustfs-worker","timeout_ms":1000,"timestamp":"2026-10-22T16:01:37.185387906Z"}
+{"acquire_ms":0,"bucket":"rv-heal-e9da35d01389c2ae","component":"ecstore","event":"object_namespace_lock","filename":"crates/ecstore/src/diagnostics/object_lock.rs","hold_ms":0,"level":"TRACE","line_number":159,"lock_attempt_id":"de759fe0-8dfe-4b32-841a-047a23b8ed7c","message":"Object namespace lock diagnostic","mode":"write","namespace":"pool-0","object":"objects/0000.bin","op":"put_object_commit","owner":"rustfs-node1:9000","request-id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","request_id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","spans":[{"content_length":"1048773","content_type":"application/octet-stream","method":"PUT","name":"http-request","peer_addr":"::ffff:10.0.0.4","request_id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","span_id":"unknown","trace_id":"unknown","uri":"/rv-heal-e9da35d01389c2ae/objects/0000.bin","user_agent":"Boto3/1.43.77 md/Botocore#1.43.77 ua/2.1 os/linux#6.17.0-1022-azure md/arch#x86_64 lang/python#3.12.3 md/pyimpl#CPython m/e,N,Z,b,D cfg/retry-mode#legacy Botocore/1.43.77 auto-testing-i05-successor/5a37a691-d1d8-4591-b5a4-f174b7fe4818","version":"HTTP/1.1"}],"state":"acquiring","subsystem":"object_lock","target":"rustfs_ecstore::object_lock_diag","threadId":"ThreadId(8)","threadName":"rustfs-worker","timeout_ms":60000,"timestamp":"2026-10-22T16:01:37.185440815Z"}
+{"acquire_ms":1213,"bucket":"rv-heal-e9da35d01389c2ae","component":"ecstore","event":"object_namespace_lock","filename":"crates/ecstore/src/diagnostics/object_lock.rs","hold_ms":0,"level":"TRACE","line_number":159,"lock_attempt_id":"de759fe0-8dfe-4b32-841a-047a23b8ed7c","message":"Object namespace lock diagnostic","mode":"write","namespace":"pool-0","object":"objects/0000.bin","op":"put_object_commit","owner":"rustfs-node1:9000","request-id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","request_id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","spans":[{"content_length":"1048773","content_type":"application/octet-stream","method":"PUT","name":"http-request","peer_addr":"::ffff:10.0.0.4","request_id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","span_id":"unknown","trace_id":"unknown","uri":"/rv-heal-e9da35d01389c2ae/objects/0000.bin","user_agent":"Boto3/1.43.77 md/Botocore#1.43.77 ua/2.1 os/linux#6.17.0-1022-azure md/arch#x86_64 lang/python#3.12.3 md/pyimpl#CPython m/e,N,Z,b,D cfg/retry-mode#legacy Botocore/1.43.77 auto-testing-i05-successor/5a37a691-d1d8-4591-b5a4-f174b7fe4818","version":"HTTP/1.1"}],"state":"acquired","subsystem":"object_lock","target":"rustfs_ecstore::object_lock_diag","threadId":"ThreadId(9)","threadName":"rustfs-worker","timeout_ms":60000,"timestamp":"2026-10-22T16:01:38.399392933Z"}
+{"acquire_ms":"1214","bucket":"rv-heal-e9da35d01389c2ae","filename":"crates/ecstore/src/set_disk/mod.rs","level":"WARN","line_number":5218,"message":"object namespace lock acquisition exceeded threshold","mode":"write","object":"objects/0000.bin","op":"put_object_commit","owner":"rustfs-node1:9000","request-id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","request_id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","spans":[{"content_length":"1048773","content_type":"application/octet-stream","method":"PUT","name":"http-request","peer_addr":"::ffff:10.0.0.4","request_id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","span_id":"unknown","trace_id":"unknown","uri":"/rv-heal-e9da35d01389c2ae/objects/0000.bin","user_agent":"Boto3/1.43.77 md/Botocore#1.43.77 ua/2.1 os/linux#6.17.0-1022-azure md/arch#x86_64 lang/python#3.12.3 md/pyimpl#CPython m/e,N,Z,b,D cfg/retry-mode#legacy Botocore/1.43.77 auto-testing-i05-successor/5a37a691-d1d8-4591-b5a4-f174b7fe4818","version":"HTTP/1.1"}],"target":"rustfs_ecstore::object_lock_diag","threadId":"ThreadId(9)","threadName":"rustfs-worker","threshold_ms":"500","timestamp":"2026-10-22T16:01:38.399447324Z"}
+{"acquire_ms":0,"bucket":".rustfs.sys","component":"ecstore","event":"object_namespace_lock","filename":"crates/ecstore/src/diagnostics/object_lock.rs","hold_ms":0,"level":"TRACE","line_number":159,"lock_attempt_id":"5986ab7e-ec8a-4721-a43d-8e43b08dd2c6","message":"Object namespace lock diagnostic","mode":"read","namespace":"pool-0","object":"buckets/rv-heal-e9da35d01389c2ae/.metadata.bin","op":"get_object","owner":"rustfs-node1:9000","request-id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","request_id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","spans":[{"content_length":"1048773","content_type":"application/octet-stream","method":"PUT","name":"http-request","peer_addr":"::ffff:10.0.0.4","request_id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","span_id":"unknown","trace_id":"unknown","uri":"/rv-heal-e9da35d01389c2ae/objects/0000.bin","user_agent":"Boto3/1.43.77 md/Botocore#1.43.77 ua/2.1 os/linux#6.17.0-1022-azure md/arch#x86_64 lang/python#3.12.3 md/pyimpl#CPython m/e,N,Z,b,D cfg/retry-mode#legacy Botocore/1.43.77 auto-testing-i05-successor/5a37a691-d1d8-4591-b5a4-f174b7fe4818","version":"HTTP/1.1"}],"state":"acquiring","subsystem":"object_lock","target":"rustfs_ecstore::object_lock_diag","threadId":"ThreadId(9)","threadName":"rustfs-worker","timeout_ms":1000,"timestamp":"2026-10-22T16:01:38.400853841Z"}
+{"acquire_ms":0,"bucket":".rustfs.sys","component":"ecstore","event":"object_namespace_lock","filename":"crates/ecstore/src/diagnostics/object_lock.rs","hold_ms":0,"level":"TRACE","line_number":159,"lock_attempt_id":"5986ab7e-ec8a-4721-a43d-8e43b08dd2c6","message":"Object namespace lock diagnostic","mode":"read","namespace":"pool-0","object":"buckets/rv-heal-e9da35d01389c2ae/.metadata.bin","op":"get_object","owner":"rustfs-node1:9000","request-id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","request_id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","spans":[{"content_length":"1048773","content_type":"application/octet-stream","method":"PUT","name":"http-request","peer_addr":"::ffff:10.0.0.4","request_id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","span_id":"unknown","trace_id":"unknown","uri":"/rv-heal-e9da35d01389c2ae/objects/0000.bin","user_agent":"Boto3/1.43.77 md/Botocore#1.43.77 ua/2.1 os/linux#6.17.0-1022-azure md/arch#x86_64 lang/python#3.12.3 md/pyimpl#CPython m/e,N,Z,b,D cfg/retry-mode#legacy Botocore/1.43.77 auto-testing-i05-successor/5a37a691-d1d8-4591-b5a4-f174b7fe4818","version":"HTTP/1.1"}],"state":"acquired","subsystem":"object_lock","target":"rustfs_ecstore::object_lock_diag","threadId":"ThreadId(9)","threadName":"rustfs-worker","timeout_ms":1000,"timestamp":"2026-10-22T16:01:38.401178747Z"}
+{"acquire_ms":0,"bucket":".rustfs.sys","component":"ecstore","event":"object_namespace_lock","filename":"crates/ecstore/src/diagnostics/object_lock.rs","hold_ms":0,"level":"TRACE","line_number":159,"lock_attempt_id":"5986ab7e-ec8a-4721-a43d-8e43b08dd2c6","message":"Object namespace lock diagnostic","mode":"read","namespace":"pool-0","object":"buckets/rv-heal-e9da35d01389c2ae/.metadata.bin","op":"get_object","owner":"rustfs-node1:9000","request-id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","request_id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","spans":[{"content_length":"1048773","content_type":"application/octet-stream","method":"PUT","name":"http-request","peer_addr":"::ffff:10.0.0.4","request_id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","span_id":"unknown","trace_id":"unknown","uri":"/rv-heal-e9da35d01389c2ae/objects/0000.bin","user_agent":"Boto3/1.43.77 md/Botocore#1.43.77 ua/2.1 os/linux#6.17.0-1022-azure md/arch#x86_64 lang/python#3.12.3 md/pyimpl#CPython m/e,N,Z,b,D cfg/retry-mode#legacy Botocore/1.43.77 auto-testing-i05-successor/5a37a691-d1d8-4591-b5a4-f174b7fe4818","version":"HTTP/1.1"}],"state":"guard_dropped","subsystem":"object_lock","target":"rustfs_ecstore::object_lock_diag","threadId":"ThreadId(10)","threadName":"rustfs-worker","timeout_ms":1000,"timestamp":"2026-10-22T16:01:38.402115996Z"}
+{"acquire_ms":0,"bucket":".rustfs.sys","component":"ecstore","event":"object_namespace_lock","filename":"crates/ecstore/src/diagnostics/object_lock.rs","hold_ms":0,"level":"TRACE","line_number":159,"lock_attempt_id":"bd1959e7-754d-4142-a590-a49bde62f2c2","message":"Object namespace lock diagnostic","mode":"read","namespace":"pool-0","object":"buckets/rv-heal-e9da35d01389c2ae/.bucket-incarnation","op":"get_object","owner":"rustfs-node1:9000","request-id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","request_id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","spans":[{"content_length":"1048773","content_type":"application/octet-stream","method":"PUT","name":"http-request","peer_addr":"::ffff:10.0.0.4","request_id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","span_id":"unknown","trace_id":"unknown","uri":"/rv-heal-e9da35d01389c2ae/objects/0000.bin","user_agent":"Boto3/1.43.77 md/Botocore#1.43.77 ua/2.1 os/linux#6.17.0-1022-azure md/arch#x86_64 lang/python#3.12.3 md/pyimpl#CPython m/e,N,Z,b,D cfg/retry-mode#legacy Botocore/1.43.77 auto-testing-i05-successor/5a37a691-d1d8-4591-b5a4-f174b7fe4818","version":"HTTP/1.1"}],"state":"acquiring","subsystem":"object_lock","target":"rustfs_ecstore::object_lock_diag","threadId":"ThreadId(10)","threadName":"rustfs-worker","timeout_ms":1000,"timestamp":"2026-10-22T16:01:38.402176987Z"}
+{"acquire_ms":0,"bucket":".rustfs.sys","component":"ecstore","event":"object_namespace_lock","filename":"crates/ecstore/src/diagnostics/object_lock.rs","hold_ms":0,"level":"TRACE","line_number":159,"lock_attempt_id":"bd1959e7-754d-4142-a590-a49bde62f2c2","message":"Object namespace lock diagnostic","mode":"read","namespace":"pool-0","object":"buckets/rv-heal-e9da35d01389c2ae/.bucket-incarnation","op":"get_object","owner":"rustfs-node1:9000","request-id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","request_id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","spans":[{"content_length":"1048773","content_type":"application/octet-stream","method":"PUT","name":"http-request","peer_addr":"::ffff:10.0.0.4","request_id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","span_id":"unknown","trace_id":"unknown","uri":"/rv-heal-e9da35d01389c2ae/objects/0000.bin","user_agent":"Boto3/1.43.77 md/Botocore#1.43.77 ua/2.1 os/linux#6.17.0-1022-azure md/arch#x86_64 lang/python#3.12.3 md/pyimpl#CPython m/e,N,Z,b,D cfg/retry-mode#legacy Botocore/1.43.77 auto-testing-i05-successor/5a37a691-d1d8-4591-b5a4-f174b7fe4818","version":"HTTP/1.1"}],"state":"acquired","subsystem":"object_lock","target":"rustfs_ecstore::object_lock_diag","threadId":"ThreadId(9)","threadName":"rustfs-worker","timeout_ms":1000,"timestamp":"2026-10-22T16:01:38.402496128Z"}
+{"acquire_ms":0,"bucket":".rustfs.sys","component":"ecstore","event":"object_namespace_lock","filename":"crates/ecstore/src/diagnostics/object_lock.rs","hold_ms":0,"level":"TRACE","line_number":159,"lock_attempt_id":"bd1959e7-754d-4142-a590-a49bde62f2c2","message":"Object namespace lock diagnostic","mode":"read","namespace":"pool-0","object":"buckets/rv-heal-e9da35d01389c2ae/.bucket-incarnation","op":"get_object","owner":"rustfs-node1:9000","request-id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","request_id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","spans":[{"content_length":"1048773","content_type":"application/octet-stream","method":"PUT","name":"http-request","peer_addr":"::ffff:10.0.0.4","request_id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","span_id":"unknown","trace_id":"unknown","uri":"/rv-heal-e9da35d01389c2ae/objects/0000.bin","user_agent":"Boto3/1.43.77 md/Botocore#1.43.77 ua/2.1 os/linux#6.17.0-1022-azure md/arch#x86_64 lang/python#3.12.3 md/pyimpl#CPython m/e,N,Z,b,D cfg/retry-mode#legacy Botocore/1.43.77 auto-testing-i05-successor/5a37a691-d1d8-4591-b5a4-f174b7fe4818","version":"HTTP/1.1"}],"state":"guard_dropped","subsystem":"object_lock","target":"rustfs_ecstore::object_lock_diag","threadId":"ThreadId(10)","threadName":"rustfs-worker","timeout_ms":1000,"timestamp":"2026-10-22T16:01:38.403118789Z"}
+{"acquire_ms":1213,"bucket":"rv-heal-e9da35d01389c2ae","component":"ecstore","event":"object_namespace_lock","filename":"crates/ecstore/src/diagnostics/object_lock.rs","hold_ms":10,"level":"TRACE","line_number":159,"lock_attempt_id":"de759fe0-8dfe-4b32-841a-047a23b8ed7c","message":"Object namespace lock diagnostic","mode":"write","namespace":"pool-0","object":"objects/0000.bin","op":"put_object_commit","owner":"rustfs-node1:9000","request-id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","request_id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","spans":[{"content_length":"1048773","content_type":"application/octet-stream","method":"PUT","name":"http-request","peer_addr":"::ffff:10.0.0.4","request_id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","span_id":"unknown","trace_id":"unknown","uri":"/rv-heal-e9da35d01389c2ae/objects/0000.bin","user_agent":"Boto3/1.43.77 md/Botocore#1.43.77 ua/2.1 os/linux#6.17.0-1022-azure md/arch#x86_64 lang/python#3.12.3 md/pyimpl#CPython m/e,N,Z,b,D cfg/retry-mode#legacy Botocore/1.43.77 auto-testing-i05-successor/5a37a691-d1d8-4591-b5a4-f174b7fe4818","version":"HTTP/1.1"}],"state":"guard_dropped","subsystem":"object_lock","target":"rustfs_ecstore::object_lock_diag","threadId":"ThreadId(9)","threadName":"rustfs-worker","timeout_ms":60000,"timestamp":"2026-10-22T16:01:38.40996034Z"}
+{"acquire_ms":0,"bucket":".rustfs.sys","component":"ecstore","event":"object_namespace_lock","filename":"crates/ecstore/src/diagnostics/object_lock.rs","hold_ms":0,"level":"TRACE","line_number":159,"lock_attempt_id":"9811a447-de36-4725-a3bc-62921e50d2cf","message":"Object namespace lock diagnostic","mode":"read","namespace":"pool-0","object":"buckets/rv-heal-e9da35d01389c2ae/.bucket-incarnation","op":"get_object","owner":"rustfs-node1:9000","request-id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","request_id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","spans":[{"content_length":"1048773","content_type":"application/octet-stream","method":"PUT","name":"http-request","peer_addr":"::ffff:10.0.0.4","request_id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","span_id":"unknown","trace_id":"unknown","uri":"/rv-heal-e9da35d01389c2ae/objects/0000.bin","user_agent":"Boto3/1.43.77 md/Botocore#1.43.77 ua/2.1 os/linux#6.17.0-1022-azure md/arch#x86_64 lang/python#3.12.3 md/pyimpl#CPython m/e,N,Z,b,D cfg/retry-mode#legacy Botocore/1.43.77 auto-testing-i05-successor/5a37a691-d1d8-4591-b5a4-f174b7fe4818","version":"HTTP/1.1"}],"state":"acquiring","subsystem":"object_lock","target":"rustfs_ecstore::object_lock_diag","threadId":"ThreadId(7)","threadName":"rustfs-worker","timeout_ms":1000,"timestamp":"2026-10-22T16:01:38.410458527Z"}
+{"acquire_ms":0,"bucket":".rustfs.sys","component":"ecstore","event":"object_namespace_lock","filename":"crates/ecstore/src/diagnostics/object_lock.rs","hold_ms":0,"level":"TRACE","line_number":159,"lock_attempt_id":"9811a447-de36-4725-a3bc-62921e50d2cf","message":"Object namespace lock diagnostic","mode":"read","namespace":"pool-0","object":"buckets/rv-heal-e9da35d01389c2ae/.bucket-incarnation","op":"get_object","owner":"rustfs-node1:9000","request-id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","request_id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","spans":[{"content_length":"1048773","content_type":"application/octet-stream","method":"PUT","name":"http-request","peer_addr":"::ffff:10.0.0.4","request_id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","span_id":"unknown","trace_id":"unknown","uri":"/rv-heal-e9da35d01389c2ae/objects/0000.bin","user_agent":"Boto3/1.43.77 md/Botocore#1.43.77 ua/2.1 os/linux#6.17.0-1022-azure md/arch#x86_64 lang/python#3.12.3 md/pyimpl#CPython m/e,N,Z,b,D cfg/retry-mode#legacy Botocore/1.43.77 auto-testing-i05-successor/5a37a691-d1d8-4591-b5a4-f174b7fe4818","version":"HTTP/1.1"}],"state":"acquired","subsystem":"object_lock","target":"rustfs_ecstore::object_lock_diag","threadId":"ThreadId(9)","threadName":"rustfs-worker","timeout_ms":1000,"timestamp":"2026-10-22T16:01:38.410760226Z"}
+{"acquire_ms":0,"bucket":".rustfs.sys","component":"ecstore","event":"object_namespace_lock","filename":"crates/ecstore/src/diagnostics/object_lock.rs","hold_ms":0,"level":"TRACE","line_number":159,"lock_attempt_id":"9811a447-de36-4725-a3bc-62921e50d2cf","message":"Object namespace lock diagnostic","mode":"read","namespace":"pool-0","object":"buckets/rv-heal-e9da35d01389c2ae/.bucket-incarnation","op":"get_object","owner":"rustfs-node1:9000","request-id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","request_id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","spans":[{"content_length":"1048773","content_type":"application/octet-stream","method":"PUT","name":"http-request","peer_addr":"::ffff:10.0.0.4","request_id":"f159b0a8-ab69-48a9-ae2c-f96751aa8844","span_id":"unknown","trace_id":"unknown","uri":"/rv-heal-e9da35d01389c2ae/objects/0000.bin","user_agent":"Boto3/1.43.77 md/Botocore#1.43.77 ua/2.1 os/linux#6.17.0-1022-azure md/arch#x86_64 lang/python#3.12.3 md/pyimpl#CPython m/e,N,Z,b,D cfg/retry-mode#legacy Botocore/1.43.77 auto-testing-i05-successor/5a37a691-d1d8-4591-b5a4-f174b7fe4818","version":"HTTP/1.1"}],"state":"guard_dropped","subsystem":"object_lock","target":"rustfs_ecstore::object_lock_diag","threadId":"ThreadId(9)","threadName":"rustfs-worker","timeout_ms":1000,"timestamp":"2026-10-22T16:01:38.411429057Z"}
diff --git a/testdata/validation/checkpoint-preserve-order.json b/testdata/validation/checkpoint-preserve-order.json
new file mode 100644
index 00000000..1a1be238
--- /dev/null
+++ b/testdata/validation/checkpoint-preserve-order.json
@@ -0,0 +1 @@
+{"schema_version":8,"admin":{"set_disk_id":"pool_0_set_0","options":{"recursive":true,"dryRun":false,"remove":false,"recreate":false,"scanMode":2,"updateParity":false,"nolock":false,"readRepair":false,"pool":0,"set":0},"buckets":{"rv-heal-dc8c2832d64dc739":"15ea0c01-bcbf-457c-af28-114e4c53ad70"},"outcome":{"execution":{"state":"running"},"coverage":"partial","counters":{"processed":8,"healed":0,"unchanged":8,"skipped":0,"failed":0,"unknown":0,"attemptFailures":0,"overflowed":false},"objects":[{"identity":{"kind":"object","bucket":"rv-heal-dc8c2832d64dc739","object":"objects/0000.bin","versionId":"00000000-0000-0000-0000-000000000000","bucketIncarnationId":"15ea0c01-bcbf-457c-af28-114e4c53ad70","poolIndex":0,"setIndex":0},"disposition":{"state":"verified_healthy"},"detail":null},{"identity":{"kind":"object","bucket":"rv-heal-dc8c2832d64dc739","object":"objects/0001.bin","versionId":"00000000-0000-0000-0000-000000000000","bucketIncarnationId":"15ea0c01-bcbf-457c-af28-114e4c53ad70","poolIndex":0,"setIndex":0},"disposition":{"state":"verified_healthy"},"detail":null},{"identity":{"kind":"object","bucket":"rv-heal-dc8c2832d64dc739","object":"objects/0002.bin","versionId":"00000000-0000-0000-0000-000000000000","bucketIncarnationId":"15ea0c01-bcbf-457c-af28-114e4c53ad70","poolIndex":0,"setIndex":0},"disposition":{"state":"verified_healthy"},"detail":null},{"identity":{"kind":"object","bucket":"rv-heal-dc8c2832d64dc739","object":"objects/0003.bin","versionId":"00000000-0000-0000-0000-000000000000","bucketIncarnationId":"15ea0c01-bcbf-457c-af28-114e4c53ad70","poolIndex":0,"setIndex":0},"disposition":{"state":"verified_healthy"},"detail":null},{"identity":{"kind":"object","bucket":"rv-heal-dc8c2832d64dc739","object":"objects/0004.bin","versionId":"00000000-0000-0000-0000-000000000000","bucketIncarnationId":"15ea0c01-bcbf-457c-af28-114e4c53ad70","poolIndex":0,"setIndex":0},"disposition":{"state":"verified_healthy"},"detail":null},{"identity":{"kind":"object","bucket":"rv-heal-dc8c2832d64dc739","object":"objects/0005.bin","versionId":"00000000-0000-0000-0000-000000000000","bucketIncarnationId":"15ea0c01-bcbf-457c-af28-114e4c53ad70","poolIndex":0,"setIndex":0},"disposition":{"state":"verified_healthy"},"detail":null},{"identity":{"kind":"object","bucket":"rv-heal-dc8c2832d64dc739","object":"objects/0006.bin","versionId":"00000000-0000-0000-0000-000000000000","bucketIncarnationId":"15ea0c01-bcbf-457c-af28-114e4c53ad70","poolIndex":0,"setIndex":0},"disposition":{"state":"verified_healthy"},"detail":null},{"identity":{"kind":"object","bucket":"rv-heal-dc8c2832d64dc739","object":"objects/0007.bin","versionId":"00000000-0000-0000-0000-000000000000","bucketIncarnationId":"15ea0c01-bcbf-457c-af28-114e4c53ad70","poolIndex":0,"setIndex":0},"disposition":{"state":"verified_healthy"},"detail":null}],"objectsTruncated":false},"completed":false},"task_id":"7f2e8f64-2c9b-4a62-8841-cfd836167b2f","checkpoint_time":1791814611,"current_bucket_index":0,"current_object_index":0,"processed_objects":["16:objects/0001.bin00000000-0000-0000-0000-000000000000","16:objects/0002.bin00000000-0000-0000-0000-000000000000","16:objects/0004.bin00000000-0000-0000-0000-000000000000","16:objects/0007.bin00000000-0000-0000-0000-000000000000","16:objects/0005.bin00000000-0000-0000-0000-000000000000","16:objects/0000.bin00000000-0000-0000-0000-000000000000","16:objects/0003.bin00000000-0000-0000-0000-000000000000","16:objects/0006.bin00000000-0000-0000-0000-000000000000"],"failed_objects":[],"skipped_objects":[],"successful_objects":8,"failed_object_count":0,"skipped_object_count":0,"skipped_new_versions":0,"skipped_ilm_expired":0,"processed_bytes":41944024,"total_objects":0,"total_bytes":0,"baseline_generation":null,"baseline_known":false,"counter_unknown":false,"integrity_digest":"tQuvmURbMQYq0R7+UEx8gMaPDOvxEcI9Vgb+939gBIY="}
\ No newline at end of file
diff --git a/testdata/validation/i05-descriptor-drain-39754893467.zip b/testdata/validation/i05-descriptor-drain-39754893467.zip
new file mode 100644
index 00000000..79a6302c
Binary files /dev/null and b/testdata/validation/i05-descriptor-drain-39754893467.zip differ
diff --git a/testdata/validation/i05-drain-budget-39746002648.zip b/testdata/validation/i05-drain-budget-39746002648.zip
new file mode 100644
index 00000000..a2f977b1
Binary files /dev/null and b/testdata/validation/i05-drain-budget-39746002648.zip differ
diff --git a/testdata/validation/i05-filter-startup-39761856012.zip b/testdata/validation/i05-filter-startup-39761856012.zip
new file mode 100644
index 00000000..969d20b7
Binary files /dev/null and b/testdata/validation/i05-filter-startup-39761856012.zip differ
diff --git a/testdata/validation/i05-h2-filter-startup-39764496504.zip b/testdata/validation/i05-h2-filter-startup-39764496504.zip
new file mode 100644
index 00000000..a01de72c
Binary files /dev/null and b/testdata/validation/i05-h2-filter-startup-39764496504.zip differ
diff --git a/testdata/validation/i05-pre-contender-39711742431.md b/testdata/validation/i05-pre-contender-39711742431.md
new file mode 100644
index 00000000..b8eddeae
--- /dev/null
+++ b/testdata/validation/i05-pre-contender-39711742431.md
@@ -0,0 +1,13 @@
+The accompanying ZIP repacks all 58 original files from RustFS Functional Chain
+[39711742431/1](https://github.com/rustfs/rustfs/actions/runs/39711742431),
+artifact 12397696964. Every member's name and bytes were compared with the
+downloaded original. It is a regression fixture, not the official ZIP bytes;
+the official archive digest is
+`e9ab49593f0423f0649066aa71325d59fadc0e7e6d0328071e2f06f45790325f`.
+
+The original result is FAIL/cleanup FAIL. Nine successful seed request/ACK pairs
+precede one closed historical GET. The observer rejected its diagnostic version
+field, no contender PUT was submitted and H2 never started. The original three
+lock events also show an already dropped guard. This archive cannot establish
+an acquired timeout window or canonical acceptance. Its stopped physical scene
+and reserved owner are used only to authorize non-destructive isolation.
diff --git a/testdata/validation/i05-pre-contender-39711742431.zip b/testdata/validation/i05-pre-contender-39711742431.zip
new file mode 100644
index 00000000..0fde8389
Binary files /dev/null and b/testdata/validation/i05-pre-contender-39711742431.zip differ
diff --git a/testdata/validation/mrf-mixed-payload.bin b/testdata/validation/mrf-mixed-payload.bin
new file mode 100644
index 00000000..26de5cfb
Binary files /dev/null and b/testdata/validation/mrf-mixed-payload.bin differ
diff --git a/testdata/validation/root-terminal-options.json b/testdata/validation/root-terminal-options.json
new file mode 100644
index 00000000..1a9e3689
--- /dev/null
+++ b/testdata/validation/root-terminal-options.json
@@ -0,0 +1,34 @@
+{
+ "candidateCommit": "1aa9be87d18cf941e5020f19e8622c26ba4b10ac",
+ "runId": 38277312274,
+ "runAttempt": 1,
+ "admissionRecordSha256": "ff199cf1a3913ce407aaf46fd8bb3c93492975a329e779f26ffb499352a9a291",
+ "terminalRecordSha256": "a9841d9339e3784ea12572569c32e45b58a996325c62dc57a5925ad76271b9c9",
+ "admissionOptions": {
+ "scan_mode": 2,
+ "remove_corrupted": false,
+ "recreate_missing": false,
+ "update_parity": false,
+ "recursive": true,
+ "dry_run": false,
+ "no_lock": false,
+ "timeout": null,
+ "pool_index": 0,
+ "set_index": 0
+ },
+ "terminalOptions": {
+ "scan_mode": 2,
+ "remove_corrupted": false,
+ "recreate_missing": false,
+ "update_parity": false,
+ "recursive": true,
+ "dry_run": false,
+ "no_lock": false,
+ "timeout": {
+ "secs": 300,
+ "nanos": 0
+ },
+ "pool_index": 0,
+ "set_index": 0
+ }
+}
diff --git a/validation-cases.json b/validation-cases.json
new file mode 100644
index 00000000..07b20057
--- /dev/null
+++ b/validation-cases.json
@@ -0,0 +1,4150 @@
+{
+ "schema": 1,
+ "source": "rustfs-release-validation/scenarios/catalog.yaml",
+ "source_commit": "524eb5b32c3236497fdd8a9271df0494fa3d9974",
+ "source_sha256": "463fdd02dee43e42fef4f3aacf7506709ce87af535e1a53d91c521bc2e361cad",
+ "modules": [
+ {
+ "id": "pool-lifecycle",
+ "target_environments": [
+ "lifecycle-a"
+ ],
+ "required_capabilities": [
+ "expansion",
+ "rebalance",
+ "decommission",
+ "restart-recovery"
+ ],
+ "cases": [
+ {
+ "id": "pool.status",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api"
+ ],
+ "assertions": [
+ "A clean seed topology reports exactly one active pool with a stable ID, non-empty command line, and internally valid capacity counters.",
+ "After expansion, rc and the Admin API report the same two unique pools, lifecycle states, operation states, and capacity values.",
+ "Pool-specific status selected by either numeric ID or exact command-line expression agrees with the corresponding list entry and no response exposes credentials."
+ ],
+ "probes": {
+ "rc": "rc --json admin pool list rustfs; rc --json admin pool status rustfs POOL_ID --by-id",
+ "adminApi": "GET /rustfs/admin/v3/pools/list"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_pool_expand.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "pool.expand-and-rebalance",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "Existing objects are written and checksum-verified while only the seed pool is configured.",
+ "Appending the second pool makes every declared node ready and preserves pool order, IDs, and the complete logical object manifest.",
+ "rc expansion start is acknowledged with a non-empty operation ID that remains stable in Admin API and rc status.",
+ "Rebalance is observed active, completes with non-zero object, version, or byte movement, and reports no failed state, last error, cleanup warning, or incomplete stop propagation.",
+ "Every object remains readable with identical size, SHA-256, metadata, and ETag after rebalance."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_pool_expand.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "pool.expansion-restart-persistence",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "The expanded two-pool map survives a graceful full-cluster restart on the same candidate.",
+ "A rebalance already showing non-zero progress resumes after the restart under the same operation ID, never regresses its counters, and completes cleanly.",
+ "Every active node returns readiness and the same pool membership after restart.",
+ "Seed-pool objects remain checksum-correct when read through both expansion-pool nodes."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_pool_expand.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "pool.rebalance-stop-resume",
+ "source_status": "implemented",
+ "priority": "P1",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "A rebalance with real data is observed in Started state with non-zero progress before stop is issued.",
+ "Stop converges on all participating pools, records stoppedAt, and has no failed peers, pending terminal reload, last error, or cleanup warning.",
+ "The stopped operation remains stopped under the same ID after a full-cluster restart and is not auto-resumed.",
+ "A new start uses a different operation ID, resumes remaining work, and reaches a clean non-zero terminal completion without data loss."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_pool_expand.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "pool.rebalance-live-io",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "PUT, GET, HEAD, LIST, COPY, and DELETE remain available while rebalance is actively moving data.",
+ "The movement status remains active with non-zero progress across the online-I/O observation window.",
+ "Baseline and newly committed objects remain checksum-correct after terminal completion."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_pool_expand.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "pool.decommission",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "Seed data is created before expansion so the selected source pool provably owns customer objects.",
+ "Decommission start is acknowledged, becomes active, and reaches complete/decommissioned with non-zero moved objects and bytes.",
+ "Failed object and byte counters are zero, queued work is empty, and no unresolved entries remain.",
+ "The complete logical manifest is readable and checksum-correct from surviving-pool nodes."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_pool_expand.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "pool.decommission-cancel-clear-resume",
+ "source_status": "implemented",
+ "priority": "P1",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "Cancel is issued only after non-zero movement and its canceled/blocked state survives a full-cluster restart without corrupting committed objects or auto-resuming.",
+ "Clear removes only terminal decommission metadata, restores the pool to an eligible active state, and remains cleared after a second restart.",
+ "A subsequent start is accepted and completes cleanly with the original manifest intact."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_pool_expand.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "pool.decommission-restart-recovery",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "A decommission with non-zero progress survives a graceful restart of every configured node.",
+ "Recovery never reports failed, canceled, unresolved, or regressed movement counters and eventually reaches terminal success.",
+ "Objects written before the restart remain checksum-correct on the surviving pool."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_pool_expand.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "pool.decommission-versioned-integrity",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "Multiple immutable versions and a delete marker created on the seed pool retain their exact version IDs and payload hashes after decommission.",
+ "Historical versions remain directly addressable and the sole delete marker remains latest; an unversioned GET returns that marker's exact identity without resurrecting the object."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_pool_expand.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "pool.decommission-multipart-integrity",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "A completed multipart object retains its length, ETag, metadata, and SHA-256 after decommission.",
+ "An in-progress multipart upload and uploaded part remain addressable and can be completed after the source pool is retired.",
+ "The resumed upload disappears from the pending list and its final length, ETag, metadata, and payload are exact."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_pool_expand.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "pool.decommission-live-io",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "Concurrent PUT operations through multiple active endpoints succeed or retry only documented transient responses while decommission has non-zero progress.",
+ "Concurrent GET, HEAD, LIST, COPY, and DELETE operations remain customer-visible during movement.",
+ "Baseline and concurrently committed objects remain complete and checksum-correct after terminal success."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_pool_expand.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "pool.retired-config-removal",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "Only after successful decommission, the retired pool expression can be removed and the surviving-pool nodes can restart cleanly.",
+ "The resulting topology reports exactly one active pool and no reference to the retired pool command line.",
+ "Migrated objects and a fresh post-removal object remain readable through the stable service endpoint."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_pool_expand.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "pool.operation-guards",
+ "source_status": "implemented",
+ "priority": "P1",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "Status/start/cancel/clear with unknown pool IDs, idle cancel/clear, single-pool rebalance, and attempts to retire the only pool fail closed without partial lifecycle metadata.",
+ "Duplicate starts are rejected, decommission is rejected while rebalance is active, and rebalance is rejected while decommission is active.",
+ "A canceled decommission cannot restart until its terminal metadata is explicitly cleared.",
+ "Cancel, stop, and clear return the cluster to a clean state while preserving all committed objects."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_pool_expand.sh"
+ ],
+ "case_ids": []
+ }
+ }
+ ]
+ },
+ {
+ "id": "heal-scanner",
+ "target_environments": [
+ "single-4disk",
+ "cluster-1pool"
+ ],
+ "required_capabilities": [
+ "heal",
+ "scanner"
+ ],
+ "cases": [
+ {
+ "id": "heal.admin-healthy-noop",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "对健康对象分别执行 normal/deep,再重复查询",
+ "不修改用户数据或版本;Normal 只接受 `metadata_healthy`,Deep 才要求 `verified_healthy`,healthy 与 repaired 分开计数,不能把检查盘数当修复盘数。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-a",
+ "designCode": "A01",
+ "procedure": "docs/heal-cases.md#A01",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DB",
+ "fault": "F0"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.admin-scope-isolation",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "分别以 root、bucket、prefix、object、pool/set 选择器启动",
+ "目标范围实际遍历;相邻前缀、其他桶及未选 set 的故障保持原状;按对象清单而非目录数量验收。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-a",
+ "designCode": "A02",
+ "procedure": "docs/heal-cases.md#A02",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DB,DS",
+ "fault": "F1"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.admin-token-cross-node-status",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "从节点甲启动,经乙及稳定入口按原 token 查询",
+ "任务身份、scope、终态及核心计数有界收敛;状态缺失/未知不能解释为完成。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-a",
+ "designCode": "A03",
+ "procedure": "docs/heal-cases.md#A03",
+ "executionMode": "operator-assisted",
+ "topology": "H2",
+ "fixture": "DB",
+ "fault": "F1"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.admin-dry-run",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "对缺片与损坏对象执行 dryRun,参数化 remove/recreate",
+ "有明确 dry_run_observed 诊断;用户对象分片、xl.meta、版本与删除标记不变;后台修复干扰须排除。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-a",
+ "designCode": "A04",
+ "procedure": "docs/heal-cases.md#A04",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DB,DV",
+ "fault": "F1,F2"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.admin-overlap-and-force-start",
+ "source_status": "procedure-ready",
+ "priority": "P1",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "活动中重发同范围、父子前缀和不相交请求,再 forceStart",
+ "按已固定 overlap policy 验证合并返回原 token 或明确冲突;forceStart 不能泄漏旧任务责任或并发破坏对象。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-a",
+ "designCode": "A05",
+ "procedure": "docs/heal-cases.md#A05",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DB,DS",
+ "fault": "F1"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.admin-stop-and-stale-token",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "非零修复进度时 stop,稳定后新建任务,再发旧 token 的查询/停止",
+ "停止需等在途写结束并稳定;新任务身份独立,旧请求不得终止新任务;重复 stop 按公开契约处理。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-a",
+ "designCode": "A06",
+ "procedure": "docs/heal-cases.md#A06",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DB",
+ "fault": "F1"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.admin-invalid-request",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "发送非法 selector、重复 query、非法布尔/scanMode、冲突 flags、超长 body",
+ "服务端返回已核实的结构化错误;无误启动、范围扩大或持久化副作用;CLI 参数错误不能替代服务端负例。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-a",
+ "designCode": "A07",
+ "procedure": "docs/heal-cases.md#A07",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DB",
+ "fault": "F0"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.admin-authorization",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "匿名、坏签名、过期签名、无 HealAdminAction 用户与最小授权用户分别请求",
+ "未授权 start/status/stop 均不能越权或泄漏对象信息;授权用户可完成规定操作,凭据不得写入证据。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-a",
+ "designCode": "A08",
+ "procedure": "docs/heal-cases.md#A08",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DB",
+ "fault": "F0"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.admin-nolock-safety",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "通过签名 API 传 nolock=true,并与同对象提交制造确定性争用",
+ "兼容字段不得跳过存储层对象锁;最后成功写入和已确认删除保持正确,原生屏障与真机竞争证据分开。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-a",
+ "designCode": "A09",
+ "procedure": "docs/heal-cases.md#A09",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DL,DV",
+ "fault": "F1,F9"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.admin-option-contract",
+ "source_status": "procedure-ready",
+ "priority": "P1",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "参数化 recursive、recreate、remove、updateParity、readRepair 与合法/非法 pool/set 组合",
+ "仅测试已核实的候选语义;normal/deep 与危险 remove 的副作用明确;API 独有选项不得伪称 rc 已覆盖。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-a",
+ "designCode": "A10",
+ "procedure": "docs/heal-cases.md#A10",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DB,DV",
+ "fault": "F1,F2"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.disk-loss-and-repair",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "让一个已确认 set 成员盘对 heal 服务不可用,持续读取,恢复原盘",
+ "记录真实故障窗口和源/目标盘身份;在可恢复条件下最终补齐目标分片及完整 manifest,不只验证降级 GET。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-b",
+ "designCode": "B01",
+ "procedure": "docs/heal-cases.md#B01",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DB",
+ "fault": "F4"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.disk-readonly-and-permission",
+ "source_status": "procedure-ready",
+ "priority": "P1",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "仅对独占测试卷制造只读/拒绝访问,恢复后重试",
+ "不可写目标不得 reported repaired;错误及待修责任可见;恢复后真实写入目标且不落到系统盘或其他目录。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-b",
+ "designCode": "B02",
+ "procedure": "docs/heal-cases.md#B02",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DB",
+ "fault": "F4,F5"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.disk-replacement-formatted",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "换入空的新逻辑盘,确认新 disk UUID/正确格式后触发重建",
+ "新盘与旧盘身份可区分且属于正确 pool/set/slot;逐版本重建完成,不能从隔离备份复制回来冒充 heal。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-b",
+ "designCode": "B03",
+ "procedure": "docs/heal-cases.md#B03",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DB,DV",
+ "fault": "F8"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.disk-replacement-unformatted",
+ "source_status": "procedure-ready",
+ "priority": "P1",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "引入空未格式化测试盘,并与带异集群格式的盘分别比较",
+ "按候选支持的初始化契约安全接纳或明确拒绝;不得覆盖外来身份或将未就绪盘标为可写健康。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-b",
+ "designCode": "B04",
+ "procedure": "docs/heal-cases.md#B04",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DB",
+ "fault": "F8"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.multiple-shard-loss",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "按实际同版本 EC 布局依次缺失 1..M 个 shard,覆盖 data/parity 角色组合",
+ "每个参数实例单独满足读取与写入 quorum;满足修复条件时全部目标重建,不把跨版本或跨 set 的 shard 合并。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-b",
+ "designCode": "B05",
+ "procedure": "docs/heal-cases.md#B05",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DB,DV",
+ "fault": "F1"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.read-quorum-boundary",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "保留恰好 R 个同版本权威 shard,测试读取及修复提交,再恢复写 quorum",
+ "读取边界和修复写入边界分别判定;不能因还能 GET 就要求在写 quorum 不足时提交成功。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-b",
+ "designCode": "B06",
+ "procedure": "docs/heal-cases.md#B06",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DB",
+ "fault": "F1"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.below-read-quorum",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "使权威来源小于 R,执行 GET 与 deep heal,再恢复被隔离源",
+ "故障期间不得返回伪造的成功内容、空对象或误删记录;明确 unresolved/失败;恢复后原对象和版本完整。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-b",
+ "designCode": "B07",
+ "procedure": "docs/heal-cases.md#B07",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DB,DV",
+ "fault": "F1"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.below-write-quorum",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "构造读 quorum 尚存而写/锁 quorum 不足,执行 heal 与新 PUT",
+ "不能虚报持久化修复或成功写入;分别记录对象、元数据、分布式锁 quorum;恢复后核对所有已确认写入。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-b",
+ "designCode": "B08",
+ "procedure": "docs/heal-cases.md#B08",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DB,DL",
+ "fault": "F4"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.node-outage-rejoin",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "停止同一跨节点 set 的一个节点,执行允许的降级 I/O,再重入",
+ "按实际丢失盘数判定可用性;缺失期间成功提交的对象补齐;节点重新 ready 不等于所有对象已重建。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-b",
+ "designCode": "B09",
+ "procedure": "docs/heal-cases.md#B09",
+ "executionMode": "operator-assisted",
+ "topology": "H2",
+ "fixture": "DB,DV,DL",
+ "fault": "F6"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.disk-and-node-flapping",
+ "source_status": "procedure-ready",
+ "priority": "P1",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "重复短暂离线/在线并在重建途中再次失联",
+ "重试与队列有界、身份稳定,无重复任务风暴;最终故障解除后收敛,所有已确认对象保持正确。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-b",
+ "designCode": "B10",
+ "procedure": "docs/heal-cases.md#B10",
+ "executionMode": "operator-assisted",
+ "topology": "H2",
+ "fixture": "DB,DL",
+ "fault": "F4,F6"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.missing-part-file",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "仅隔离指定对象某个 part/shard,保留 format 与其他元数据,执行 deep heal",
+ "对应数据或校验分片重新生成,part/版本身份准确;不得只看到 xl.meta 重现就通过。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-c",
+ "designCode": "C01",
+ "procedure": "docs/heal-cases.md#C01",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DB,DM",
+ "fault": "F1"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.data-shard-bitrot",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "在指定 data shard 的已知偏移翻转字节,包含首/中/尾位置",
+ "故障前后摘要证明注入生效;deep 或 read-repair 发现损坏并修复;独立 bitrot/EC 及 S3 全内容验证通过。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-c",
+ "designCode": "C02",
+ "procedure": "docs/heal-cases.md#C02",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DB,DM",
+ "fault": "F2"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.parity-shard-bitrot",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "损坏 parity shard,使普通 GET 仍可能成功,再执行 deep heal",
+ "必须识别并重建损坏 parity;只有 GET 正确、无目标分片修复不能通过。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-c",
+ "designCode": "C03",
+ "procedure": "docs/heal-cases.md#C03",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DB,DM",
+ "fault": "F2"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.truncated-and-zeroed-shard",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "分别截断 shard、置零和破坏校验头,保留正确元数据仲裁来源",
+ "短读、全零和校验损坏被准确识别;修复不能接受截断内容或错误长度。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-c",
+ "designCode": "C04",
+ "procedure": "docs/heal-cases.md#C04",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DB,DM",
+ "fault": "F2"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.missing-or-corrupt-xlmeta",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "在少数成员上移走或损坏 xl.meta/inline 内容,保留足够权威来源",
+ "恢复同一对象的全部应存版本、数据引用与删除标记;不以新建空元数据替代真正修复。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-c",
+ "designCode": "C05",
+ "procedure": "docs/heal-cases.md#C05",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DB,DV",
+ "fault": "F3"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.stale-version-rejoin",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "盘离线后执行覆盖/版本删除,再让携带旧元数据的盘重入",
+ "以已确认的新代次/版本为准;旧当前值与已删版本不复活,旧盘被纠正而非成为新的错误权威。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-c",
+ "designCode": "C06",
+ "procedure": "docs/heal-cases.md#C06",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DV,DL",
+ "fault": "F4"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.conflicting-subquorum-authority",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "用受控原生夹具构造互不构成 quorum 的版本/代次集合,再做对应真机反例",
+ "禁止把不同版本的 shard 数量并集当成权威;无法证明时安全拒绝且保留可恢复证据,不猜测较新时间戳。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-c",
+ "designCode": "C07",
+ "procedure": "docs/heal-cases.md#C07",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DV",
+ "fault": "F3,F9"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.metadata-payload-mismatch",
+ "source_status": "procedure-ready",
+ "priority": "P1",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "构造元数据声称某长度/摘要但实际 part 不匹配的少数副本",
+ "元数据与内容联合判断;不把可解析的 xl.meta 当健康;权威不足时拒绝覆盖正确副本。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-c",
+ "designCode": "C08",
+ "procedure": "docs/heal-cases.md#C08",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DB,DV",
+ "fault": "F2,F3"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.foreign-format-and-unknown-schema",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "提供外来 disk/set 身份、未来 schema 或不支持的记录格式",
+ "明确隔离/拒绝且不破坏原记录,不把未知当 missing 后直接重建;解除故障后原数据可用。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-c",
+ "designCode": "C09",
+ "procedure": "docs/heal-cases.md#C09",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DB",
+ "fault": "F3,F8"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.dangling-and-orphan-safety",
+ "source_status": "procedure-ready",
+ "priority": "P1",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "构造 dangling part、旧 data_dir 与共享历史引用,分别 remove=false/true、宽限期前后检查",
+ "默认不得破坏仍可恢复数据;显式 remove 只清除具有充分删除证明且过期的本次孤儿,不删共享版本或 MPU。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-c",
+ "designCode": "C10",
+ "procedure": "docs/heal-cases.md#C10",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DV,DM",
+ "fault": "F1,F3"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.bucket-incarnation-recreation",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "盘离线时完成旧桶合法清空/删除,再同名建桶写入新对象,恢复旧盘与旧任务",
+ "旧 bucket incarnation 的 checkpoint/MRF/元数据不得污染新桶;旧任务不得删除或恢复新 incarnation 的对象。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-c",
+ "designCode": "C11",
+ "procedure": "docs/heal-cases.md#C11",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DV",
+ "fault": "F4,F9"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.object-size-boundaries",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "覆盖 0/1 字节、页/inline/EC block 边界±1以及大对象",
+ "每个实际存储形态均确认后注入;长度、SHA-256、ETag、HEAD/LIST 与边界 Range 正确,无假设阈值。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-d",
+ "designCode": "D01",
+ "procedure": "docs/heal-cases.md#D01",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DB",
+ "fault": "F1,F2"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.completed-multipart-object",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "修复已完成的不等长多 part 对象,分别损坏首/中/末 part",
+ "完整内容、part 顺序、原版本/ETag/元数据和跨 part Range 不变;multipart ETag 不能当整对象 MD5。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-d",
+ "designCode": "D02",
+ "procedure": "docs/heal-cases.md#D02",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DM",
+ "fault": "F1,F2"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.historical-versions",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "不同版本分布不同损坏位置,按对象和递归范围修复",
+ "所有原 versionId 可寻址且内容正确,最新版本身份不漂移,无多余版本或遗漏历史版本。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-d",
+ "designCode": "D03",
+ "procedure": "docs/heal-cases.md#D03",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DV",
+ "fault": "F1,F3"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.delete-marker-semantics",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "缺失/陈旧 marker 副本后 heal,并按当前值与精确 marker versionId 查询",
+ "当前 GET/HEAD 与指定 marker 的状态码及身份头符合契约;marker 不复活数据,LIST versions 无重复。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-d",
+ "designCode": "D04",
+ "procedure": "docs/heal-cases.md#D04",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DV",
+ "fault": "F3,F4"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.versioning-suspended-null",
+ "source_status": "procedure-ready",
+ "priority": "P1",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "在版本开启、暂停、null 版本覆盖之间建夹具再修复",
+ "null 和非 null 版本不混淆,不新增假版本;暂停前历史对象仍准确可读。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-d",
+ "designCode": "D05",
+ "procedure": "docs/heal-cases.md#D05",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DV",
+ "fault": "F1,F3"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.object-metadata-and-tags",
+ "source_status": "procedure-ready",
+ "priority": "P1",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "包含用户 metadata、Content-Type/Encoding/Disposition、tags 的对象修复",
+ "对外 metadata、tags 和相关响应头逐项相同;不要求无关内部元数据字节完全一致。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-d",
+ "designCode": "D06",
+ "procedure": "docs/heal-cases.md#D06",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DS",
+ "fault": "F1,F3"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.range-head-and-conditions",
+ "source_status": "procedure-ready",
+ "priority": "P1",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "修复前后验证 HEAD、合法/越界 Range、If-Match/If-None-Match 及时间条件",
+ "200/206/304/412/416 等按逐请求契约判断,绝不返回旧内容、错误范围或错误 ETag。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-d",
+ "designCode": "D07",
+ "procedure": "docs/heal-cases.md#D07",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DB,DM",
+ "fault": "F1,F2"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.encrypted-objects",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "按候选支持的 SSE-S3/SSE-KMS/SSE-C 建加密对象并修复",
+ "授权解密后原内容及版本一致,物理验证使用正确编码/密文层;密钥不可用时不误删、不降级明文,证据脱敏。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-d",
+ "designCode": "D08",
+ "procedure": "docs/heal-cases.md#D08",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DE",
+ "fault": "F1,F2"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.object-lock-retention",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "对开启 retention/legal hold 的受保护版本修复,期间测试非法删除",
+ "heal 不绕过保留/hold,也不丢失其元数据;拒绝删除与合法修复均有明确证据。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-d",
+ "designCode": "D09",
+ "procedure": "docs/heal-cases.md#D09",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DO,DV",
+ "fault": "F1,F3"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.special-keys-and-pagination",
+ "source_status": "procedure-ready",
+ "priority": "P1",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "覆盖中文、空格、百分号、编码斜线、多层前缀和多页对象列表",
+ "逻辑 key 与物理路径映射准确,不跨桶/前缀;分页无遗漏重复,不能把用户 key 当可执行 shell 或裸文件路径。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-d",
+ "designCode": "D10",
+ "procedure": "docs/heal-cases.md#D10",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DS",
+ "fault": "F1,F3"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.bucket-metadata-integrity",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "在隔离桶白名单内测试可支持的桶元数据恢复,并核对外部 bucket 配置",
+ "版本设置、策略/标签/保留配置按契约保持;不得遍历凭据配置或把未知内部记录当可重建数据。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-d",
+ "designCode": "D11",
+ "procedure": "docs/heal-cases.md#D11",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DS,DV",
+ "fault": "F3"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.manual-normal-attribution",
+ "source_status": "procedure-ready",
+ "priority": "P1",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "禁止预读目标,保留 format,隔离背景干扰后显式 normal heal",
+ "证明本次 Admin token 实际覆盖目标并修复可由 normal 检出的缺失;不要求 normal 发现所有静默 bitrot。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-e",
+ "designCode": "E01",
+ "procedure": "docs/heal-cases.md#E01",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DB",
+ "fault": "F1"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.manual-deep-attribution",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "在普通读取仍可成功的 parity 损坏上显式 deep heal",
+ "按 token/对象/版本及物理变更证明 deep 检查和修复,不把后台提前修复算给手工任务。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-e",
+ "designCode": "E02",
+ "procedure": "docs/heal-cases.md#E02",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DB",
+ "fault": "F2"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.read-repair-attribution",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "不发 Admin heal,对已确认的特定损坏对象发一次可追踪 GET/Range",
+ "读取正确且后续真实补片;若走异步队列需证实 source=read-repair,不能只有 GET 成功。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-e",
+ "designCode": "E03",
+ "procedure": "docs/heal-cases.md#E03",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DB,DV",
+ "fault": "F1,F2"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.scanner-cold-object-repair",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "不 GET/HEAD 损坏目标、不发手工 heal,仅等待已启用的 scanner 路径",
+ "冷对象被发现并修复,来源和遍历窗口明确;scanner 周期结束但目标没检查不能通过。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-e",
+ "designCode": "E04",
+ "procedure": "docs/heal-cases.md#E04",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DB",
+ "fault": "F1,F2"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.mrf-offline-write-replay",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "成员离线时完成有 quorum 的 PUT/DELETE,确认待修责任后重入",
+ "durable MRF 责任持久且身份精确;只在 repaired/verified healthy/authoritatively absent 等已证明结果后释放,成功提交不丢失。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-e",
+ "designCode": "E05",
+ "procedure": "docs/heal-cases.md#E05",
+ "executionMode": "operator-assisted",
+ "topology": "H2",
+ "fixture": "DL,DV",
+ "fault": "F4"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.replacement-auto-rebuild",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "替换盘接纳后不发 Admin/目标 GET,观察自动 replacement heal",
+ "替换代次、ready/completion 与盘上全部目标版本一致;不能用服务 ready、队列空或计数重置代替完成。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-e",
+ "designCode": "E06",
+ "procedure": "docs/heal-cases.md#E06",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DB,DV",
+ "fault": "F8"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.root-graceful-coordinator-restart",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "root heal 非零进度时优雅重启原协调者,保留原拓扑与记录盘",
+ "在明确支持的边界内恢复原 task ID 和剩余责任/预算,完成后无遗留活动记录;不扩张为协调盘丢失后的故障转移。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-f",
+ "designCode": "F01",
+ "procedure": "docs/heal-cases.md#F01",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DX",
+ "fault": "F6"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.target-graceful-restart",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "替换目标仅完成部分重建时优雅重启目标节点",
+ "故障确实发生于部分完成窗口;重启后续建同一目标盘/代次,最终 manifest 与全部目标分片正确。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-f",
+ "designCode": "F02",
+ "procedure": "docs/heal-cases.md#F02",
+ "executionMode": "operator-assisted",
+ "topology": "H2",
+ "fixture": "DB,DV,DX",
+ "fault": "F6,F8"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.target-process-crash",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "部分重建时 SIGKILL 测试进程,再正常启动",
+ "unclean shutdown 与修复进度有真实证据;已提交对象不丢失,未完成目标不会被当完整;明确不是断电测试。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-f",
+ "designCode": "F03",
+ "procedure": "docs/heal-cases.md#F03",
+ "executionMode": "operator-assisted",
+ "topology": "H2",
+ "fixture": "DB,DV,DX",
+ "fault": "F6,F8"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.coordinator-process-crash",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "协调者非优雅退出并恢复,分别观察 root、MRF、replacement",
+ "按各机制已核实的持久化边界判断;不得把丢失的易失 token 当成功,需显式记录剩余责任并可安全重扫恢复。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-f",
+ "designCode": "F04",
+ "procedure": "docs/heal-cases.md#F04",
+ "executionMode": "operator-assisted",
+ "topology": "H2",
+ "fixture": "DB,DX",
+ "fault": "F6"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.full-cluster-restart",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "有未完成修复责任时重启全部测试节点,固定原拓扑/磁盘身份",
+ "有持久化保证的责任不遗失;全部端点恢复一致,旧版本/marker/MPU 与目标分片完整,重复重启不扩散错误。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-f",
+ "designCode": "F05",
+ "procedure": "docs/heal-cases.md#F05",
+ "executionMode": "operator-assisted",
+ "topology": "H2",
+ "fixture": "DB,DV,DM",
+ "fault": "F6"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.cancelled-task-restart",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "完成取消及在途退出后重启,随后 forceStart/新任务正常修复",
+ "已取消的任务不自动复活;旧 checkpoint/token 不能取消或覆盖新任务,旧责任不会误标为修复完成。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-f",
+ "designCode": "F06",
+ "procedure": "docs/heal-cases.md#F06",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DB,DX",
+ "fault": "F6"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.checkpoint-and-mrf-corruption",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "受控构造 checkpoint/MRF 截断、坏校验、未来 schema、陈旧代次及写入失败",
+ "已确认持久化责任不得静默删除或伪 ACK;未知记录保留且可诊断;精确崩溃窗口由原生屏障证明,不伪称真机命中。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-f",
+ "designCode": "F07",
+ "procedure": "docs/heal-cases.md#F07",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DB,DV",
+ "fault": "F9"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.race-unversioned-overwrite",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "同 key 连续覆盖与 heal 同时推进,包含提交尾部清理窗口",
+ "按可证明的提交顺序验证最终最后一次成功写入;不能回滚到旧 data_dir,未确认超时写入单独解析。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-g",
+ "designCode": "G01",
+ "procedure": "docs/heal-cases.md#G01",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DL",
+ "fault": "F1,F2,F9"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.race-versioned-put",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "修复历史版本时创建新版本并重复读取各 versionId",
+ "新旧版本不混片、不串内容;已返回成功的每个版本都保留,最新值按确定的先后关系验收。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-g",
+ "designCode": "G02",
+ "procedure": "docs/heal-cases.md#G02",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DV,DL",
+ "fault": "F1,F9"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.race-version-delete",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "对损坏的精确历史 versionId 发 DELETE,与 deep heal 竞争",
+ "已确认删除的版本不能被复活,其他历史版本不受影响;原生临界点测试与真机竞争分别留证。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-g",
+ "designCode": "G03",
+ "procedure": "docs/heal-cases.md#G03",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DV",
+ "fault": "F1,F2,F9"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.race-markers-and-batch-delete",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "与创建/删除 marker、批量版本删除和无版本删除分别交错",
+ "按逐项 DELETE 回执核对版本/marker 集合,无复活、误删或遗漏批量错误。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-g",
+ "designCode": "G04",
+ "procedure": "docs/heal-cases.md#G04",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DV",
+ "fault": "F3,F9"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.race-multipart-complete-abort",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "heal 期间对不同已知 UploadId 分别续传、Complete、Abort",
+ "成功 Complete 内容只出现一次;Abort 不破坏已提交对象或别的 upload;未完成 part 不被伪造为完整对象。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-g",
+ "designCode": "G05",
+ "procedure": "docs/heal-cases.md#G05",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DM,DL",
+ "fault": "F1,F9"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.race-copy-and-metadata-update",
+ "source_status": "procedure-ready",
+ "priority": "P1",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "COPY 源/目标和标签/元数据更新与修复交错",
+ "最终对象内容与对应提交的 metadata 一致,读修复发现新代次应重验/放弃陈旧提交而非覆盖。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-g",
+ "designCode": "G06",
+ "procedure": "docs/heal-cases.md#G06",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DS,DL",
+ "fault": "F1,F9"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.race-ilm-expiry-and-transition",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "分别与当前/历史版本过期、远端 transition/restore 同窗口执行",
+ "已授权过期不被恢复;迁移状态与远端版本引用正确;只有实际观察到交叠才计并发覆盖,tier 未配置需明确阻断对应实例。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-g",
+ "designCode": "G07",
+ "procedure": "docs/heal-cases.md#G07",
+ "executionMode": "operator-assisted",
+ "topology": "H2",
+ "fixture": "DV,DE",
+ "fault": "F1,F9,F11"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.race-replication-resync",
+ "source_status": "procedure-ready",
+ "priority": "P1",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "在隔离复制两端与 heal 同时运行新增/删除/重同步",
+ "对端版本、删除语义及源数据最终一致,无无限回环或重复传播;不得依赖仅源端 GET 宣称复制正确。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-g",
+ "designCode": "G08",
+ "procedure": "docs/heal-cases.md#G08",
+ "executionMode": "operator-assisted",
+ "topology": "H2",
+ "fixture": "DV,DL",
+ "fault": "F1,F9,F11"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.slow-io-and-eio",
+ "source_status": "procedure-ready",
+ "priority": "P1",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "在独占故障层注入有界延迟、短读、EIO,解除后观察恢复",
+ "暂态与永久错误分开,重试有界且无忙循环;解除故障后实际修复,期间不得提前标健康。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-i",
+ "designCode": "I01",
+ "procedure": "docs/heal-cases.md#I01",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DB,DX",
+ "fault": "F5"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.enospc-inodes-and-quota",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "限制专用测试卷空间/inode,并参数化本次桶配额",
+ "仅填专用受限卷、不耗尽共享真实盘;失败不破坏好副本或释放尚需容量,释放空间后正确续修。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-i",
+ "designCode": "I02",
+ "procedure": "docs/heal-cases.md#I02",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DB,DM",
+ "fault": "F5,F11"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.network-partition-and-lock-quorum",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "仅隔离测试服务节点间流量,覆盖少数派、读/写/锁 quorum 边界及恢复",
+ "少数派不得承诺无安全性的修复或写入;cluster coverage 不完整不能显示全局健康;恢复无分叉、跨代覆盖。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-i",
+ "designCode": "I03",
+ "procedure": "docs/heal-cases.md#I03",
+ "executionMode": "operator-assisted",
+ "topology": "H2",
+ "fixture": "DB,DV,DL",
+ "fault": "F7"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.queue-pressure-and-admission",
+ "source_status": "procedure-ready",
+ "priority": "P1",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "多来源请求冲击有限队列,混合重复任务、不同优先级和多个桶",
+ "Full/Dropped/Merged 语义与实际任务一致;丢弃不能丢 durable 责任;在预先声明负载窗口内无无限堆积或健康误报。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-i",
+ "designCode": "I04",
+ "procedure": "docs/heal-cases.md#I04",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DX",
+ "fault": "F11"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.lock-timeout-and-caller-abort",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "真实对象锁争用超过预算及调用方断开,再释放锁恢复",
+ "取消/超时不得提前释放在途所有权或重复提交;陈旧 read-repair 必须重验代次;恢复后无泄漏和错误覆盖。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-i",
+ "designCode": "I05",
+ "procedure": "docs/heal-cases.md#I05",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DL,DV",
+ "fault": "F5,F9"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.kms-and-tier-outage",
+ "source_status": "procedure-ready",
+ "priority": "P1",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "仅让已分配测试 KMS/远端 tier 暂不可用,持续查询与修复",
+ "依赖故障不得解释为对象不存在/可删/已修复,不暴露密钥;恢复后原版本可解密或恢复,责任未丢失。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-i",
+ "designCode": "I06",
+ "procedure": "docs/heal-cases.md#I06",
+ "executionMode": "operator-assisted",
+ "topology": "H2",
+ "fixture": "DE,DV",
+ "fault": "F11"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.status-partial-disabled-unknown",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "分别检查 enabled/disabled/uninitialized、全体可达及缺失 peer 的状态",
+ "idle 只在覆盖完整且真实无活动时成立;partial/unknown/degraded/disabled 不被客户端转换为全部完成。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-j",
+ "designCode": "J01",
+ "procedure": "docs/heal-cases.md#J01",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DB",
+ "fault": "F0,F6"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.outcome-counters-and-error-truth",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "同批次混合健康、可修、不可修、deferred、dry-run 对象",
+ "execution/coverage/disposition/计数和逐对象物理结果相符;unknown、skipped 或 completed_with_errors 不能算修复成功。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-j",
+ "designCode": "J02",
+ "procedure": "docs/heal-cases.md#J02",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DB,DV",
+ "fault": "F1,F2"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.incremental-cursor-and-truncation",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "生成超过结果保留上限的任务,分页轮询 sinceSeq,并跨节点/终态读取",
+ "nextSeq/minSeq 单调且 gap/truncation 明示;有界 items 不是完整责任清单,最终须用独立 manifest/计数补证,不静默丢错误。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-j",
+ "designCode": "J03",
+ "procedure": "docs/heal-cases.md#J03",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DS,DX",
+ "fault": "F1"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.repeat-idempotency",
+ "source_status": "procedure-ready",
+ "priority": "P1",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "第一次修复后重复 normal/deep、重启再检查",
+ "versioned 首次允许所有 identity repaired 或至少一个 repaired 加其余正健康态,marker 仅 repaired/metadata_healthy,计数必须与 receipt 集合一致;normal repeat 接受 `metadata_healthy`,deep repeat 要求 `verified_healthy`,repeat marker 为 `metadata_healthy` 且 healed=0;unknown/skipped/failed 不得成为 PASS。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-j",
+ "designCode": "J04",
+ "procedure": "docs/heal-cases.md#J04",
+ "executionMode": "operator-assisted",
+ "topology": "H1,H2",
+ "fixture": "DB,DV",
+ "fault": "F1"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.performance-abba",
+ "source_status": "procedure-ready",
+ "priority": "P2",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "同硬件同候选按 ABBA 运行无 heal 基线和有 heal 负载,重复并固定限速",
+ "分别报告修复吞吐、GET/PUT P95/P99、错误率和 CPU/IO;阈值事先冻结,不能事后挑轮次或把功能 PASS 充性能验收。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-j",
+ "designCode": "J06",
+ "procedure": "docs/heal-cases.md#J06",
+ "executionMode": "operator-assisted",
+ "topology": "H2",
+ "fixture": "DX,DL",
+ "fault": "F1,F11"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "scanner.usage-convergence",
+ "source_status": "planned",
+ "priority": "P1",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "Scanner and data-usage results converge to the uploaded fixture."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-storage-test.sh"
+ ],
+ "case_ids": [
+ "STOR-134"
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "id": "heal-extended",
+ "target_environments": [],
+ "required_capabilities": [
+ "heal",
+ "scanner"
+ ],
+ "cases": [
+ {
+ "id": "heal.mixed-version-upgrade-rollback",
+ "source_status": "procedure-ready",
+ "priority": "P1",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "固定前后两个明确兼容候选,覆盖滚动升级、混合 peer、回滚与重启",
+ "能力不足必须显示 partial/unsupported 而非完整成功;按格式兼容契约保留责任,旧版本不能误退休新版本记录。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-f",
+ "designCode": "F08",
+ "procedure": "docs/heal-cases.md#F08",
+ "executionMode": "operator-assisted",
+ "topology": "H2",
+ "fixture": "DB,DV",
+ "fault": "F6,F9"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.dedicated-host-forced-restart",
+ "source_status": "procedure-ready",
+ "priority": "P1",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "经专项授权在部分修复/持续写入时强制重启独占测试 VM/主机",
+ "保留平台事件与 ACK ledger;验证落盘和恢复,不把 SIGKILL 当断电;VM 强制停止也不等同于物理控制器断电。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-f",
+ "designCode": "F09",
+ "procedure": "docs/heal-cases.md#F09",
+ "executionMode": "operator-assisted",
+ "topology": "H6",
+ "fixture": "DB,DV,DX",
+ "fault": "F10"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.with-expansion-rebalance",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "先证明源 pool 持有夹具,再扩容并与修复同时 rebalance",
+ "修复目标不得指向错误 pool/set;新旧数据完整、移动终态正确,保留 scanner backlog 提交权威检查。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-h",
+ "designCode": "H01",
+ "procedure": "docs/heal-cases.md#H01",
+ "executionMode": "operator-assisted",
+ "topology": "H5",
+ "fixture": "DB,DV",
+ "fault": "F1,F2"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.with-rebalance-stop-resume",
+ "source_status": "procedure-ready",
+ "priority": "P1",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "heal 活动期间 stop/restart/resume rebalance",
+ "两类任务状态和身份独立;停止/恢复移动不误终止修复或丢失对象,恢复后两者均满足各自契约。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-h",
+ "designCode": "H02",
+ "procedure": "docs/heal-cases.md#H02",
+ "executionMode": "operator-assisted",
+ "topology": "H5",
+ "fixture": "DB,DL",
+ "fault": "F1,F6"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.with-decommission",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "分别在待退役源和存活目标制造可恢复缺片,再执行退役",
+ "不得提前删源或以临时副本冒充迁移成功;按 Pool 既定写入限制验收;只剩 survivor 后仍完整可读且有物理证明。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-h",
+ "designCode": "H03",
+ "procedure": "docs/heal-cases.md#H03",
+ "executionMode": "operator-assisted",
+ "topology": "H5",
+ "fixture": "DB,DV,DM",
+ "fault": "F1,F2"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.with-decommission-cancel-clear",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "heal/移动争用中取消退役、Clear、恢复,并覆盖容量锁超时恢复",
+ "保持持久化后通知及在途退出顺序;不误取消新代次;对 #2423 的重试窗口单独注明真机是否实际触发,不只看最终绿色。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-h",
+ "designCode": "H04",
+ "procedure": "docs/heal-cases.md#H04",
+ "executionMode": "operator-assisted",
+ "topology": "H5",
+ "fixture": "DB,DL",
+ "fault": "F1,F9"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.after-retired-pool-removal",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "源 pool 完全退役移除并停机后,只在 survivor 上触发修复",
+ "不再向已移除源写入或依赖其兜底;来源权威和版本正确;#2422 路径不能出现无权威覆盖或错误回退。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-h",
+ "designCode": "H05",
+ "procedure": "docs/heal-cases.md#H05",
+ "executionMode": "operator-assisted",
+ "topology": "H5",
+ "fixture": "DB,DV",
+ "fault": "F1,F3"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.multi-pool-target-isolation",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "仅破坏选定 pool/set,并在另一 pool 保留不同对象与控制故障",
+ "选择器、任务、容量/配额和目标 drive 一致;未选 pool 不受错误修复,跨 pool 存在不等于该目标已修复。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-h",
+ "designCode": "H06",
+ "procedure": "docs/heal-cases.md#H06",
+ "executionMode": "operator-assisted",
+ "topology": "H5",
+ "fixture": "DB,DV",
+ "fault": "F1,F4"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.ec84-multi-set-restart",
+ "source_status": "procedure-ready",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "固定 EC8+4 单 set 与至少两 set,部分重建时重启/崩溃目标",
+ "每个 set 的故障覆盖和重建分别证明,不跨 set 拼 shard;必须满足真实盘数及映射,不能用 4 盘用例替代 EC8+4。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-h",
+ "designCode": "H07",
+ "procedure": "docs/heal-cases.md#H07",
+ "executionMode": "operator-assisted",
+ "topology": "H3,H4",
+ "fixture": "DB,DV,DX",
+ "fault": "F6,F8"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "heal.long-soak",
+ "source_status": "procedure-ready",
+ "priority": "P2",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "独占环境运行预注册 24h/72h 窗口,混合修复、掉线、写入和版本删除",
+ "每次 ACK 可追踪;最终全量 manifest 正确,队列/内存/FD/临时空间无无界增长,无后台 panic 或未解释持久责任。"
+ ],
+ "probes": {
+ "design": "docs/heal-validation.md#cases-j",
+ "designCode": "J05",
+ "procedure": "docs/heal-cases.md#J05",
+ "executionMode": "operator-assisted",
+ "topology": "H2,H5",
+ "fixture": "DX,DL,DV",
+ "fault": "F4,F6,F11"
+ },
+ "reuse_candidates": {
+ "suites": [
+ "rustfs_heal_test.sh",
+ "rustfs-fault-tolerance-test.sh"
+ ],
+ "case_ids": []
+ }
+ }
+ ]
+ },
+ {
+ "id": "ilm-expiration",
+ "target_environments": [
+ "ilm-2x4"
+ ],
+ "required_capabilities": [
+ "lifecycle",
+ "scanner",
+ "versioning",
+ "multipart",
+ "restart-recovery"
+ ],
+ "cases": [
+ {
+ "id": "scanner.status-readiness",
+ "source_status": "implemented",
+ "priority": "P1",
+ "channels": [
+ "rc",
+ "admin-api"
+ ],
+ "assertions": [
+ "Scanner runtime settings match the accelerated test contract.",
+ "The leader completes a successful scanner cycle within the deadline."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-tier-test.sh",
+ "rustfs-s3-compat-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "ilm.configuration-roundtrip",
+ "source_status": "implemented",
+ "priority": "P1",
+ "channels": [
+ "rc",
+ "s3-api"
+ ],
+ "assertions": [
+ "Lifecycle configuration create, read, list, and delete agree across clients."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-tier-test.sh",
+ "rustfs-s3-compat-test.sh"
+ ],
+ "case_ids": [
+ "S3C-704"
+ ]
+ }
+ },
+ {
+ "id": "ilm.invalid-configuration",
+ "source_status": "implemented",
+ "priority": "P1",
+ "channels": [
+ "s3-api"
+ ],
+ "assertions": [
+ "Invalid days and status values fail with a 4xx response and do not persist partial configuration."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-tier-test.sh",
+ "rustfs-s3-compat-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "ilm.current-expiration",
+ "source_status": "implemented",
+ "priority": "P1",
+ "channels": [
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "Due current objects are deleted while a nonmatching control object survives."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-tier-test.sh",
+ "rustfs-s3-compat-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "ilm.tag-filter-expiration",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "s3-api"
+ ],
+ "assertions": [
+ "A tag-filtered rule expires only the matching object and preserves the control object."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-tier-test.sh",
+ "rustfs-s3-compat-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "ilm.disabled-rule",
+ "source_status": "implemented",
+ "priority": "P1",
+ "channels": [
+ "s3-api"
+ ],
+ "assertions": [
+ "A disabled rule does not mutate its matching object after the accelerated due time."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-tier-test.sh",
+ "rustfs-s3-compat-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "ilm.rule-removal",
+ "source_status": "implemented",
+ "priority": "P1",
+ "channels": [
+ "s3-api"
+ ],
+ "assertions": [
+ "Removing a rule before it becomes due prevents the queued lifecycle action."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-tier-test.sh",
+ "rustfs-s3-compat-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "ilm.versioned-current-expiration",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "s3-api"
+ ],
+ "assertions": [
+ "Current-version expiration creates a delete marker and retains the addressable original version."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-tier-test.sh",
+ "rustfs-s3-compat-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "ilm.noncurrent-expiration",
+ "source_status": "implemented",
+ "priority": "P1",
+ "channels": [
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "A due noncurrent version is deleted without changing the latest version."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-tier-test.sh",
+ "rustfs-s3-compat-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "ilm.newer-noncurrent-retention",
+ "source_status": "implemented",
+ "priority": "P1",
+ "channels": [
+ "s3-api"
+ ],
+ "assertions": [
+ "NewerNoncurrentVersions retains the requested history and expires only older versions."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-tier-test.sh",
+ "rustfs-s3-compat-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "ilm.delete-all-versions",
+ "source_status": "implemented",
+ "priority": "P1",
+ "channels": [
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "ExpiredObjectAllVersions removes every version of the matching key."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-tier-test.sh",
+ "rustfs-s3-compat-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "ilm.delete-marker-expiration",
+ "source_status": "implemented",
+ "priority": "P1",
+ "channels": [
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "The noncurrent version and its resulting expired delete marker are removed."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-tier-test.sh",
+ "rustfs-s3-compat-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "ilm.multipart-expiration",
+ "source_status": "implemented",
+ "priority": "P1",
+ "channels": [
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "A due incomplete multipart upload is automatically aborted."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-tier-test.sh",
+ "rustfs-s3-compat-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "ilm.transition-and-restore",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "An object transitions to a real remote RustFS tier without changing its bytes.",
+ "A restore request completes and the restored object remains checksum-correct."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-tier-test.sh",
+ "rustfs-s3-compat-test.sh"
+ ],
+ "case_ids": [
+ "TIER-105",
+ "TIER-107"
+ ]
+ }
+ },
+ {
+ "id": "ilm.restart-persistence",
+ "source_status": "implemented",
+ "priority": "P1",
+ "channels": [
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "A lifecycle rule survives a full cluster restart and still expires its object."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-tier-test.sh",
+ "rustfs-s3-compat-test.sh"
+ ],
+ "case_ids": []
+ }
+ }
+ ]
+ },
+ {
+ "id": "bucket-replication",
+ "target_environments": [
+ "replication-2site"
+ ],
+ "required_capabilities": [
+ "bucket-replication",
+ "versioning",
+ "multipart",
+ "scanner",
+ "restart-recovery",
+ "outage-recovery"
+ ],
+ "cases": [
+ {
+ "id": "bucket-replication.source-versioning-required",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "s3-api"
+ ],
+ "assertions": [
+ "An unversioned source is rejected without persisting a replication configuration."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-replication-test.sh"
+ ],
+ "case_ids": [
+ "REP-103"
+ ]
+ }
+ },
+ {
+ "id": "bucket-replication.target-versioning-required",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "s3-api"
+ ],
+ "assertions": [
+ "An unversioned target is rejected without creating a usable rule."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-replication-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "bucket-replication.configuration-roundtrip",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "s3-api"
+ ],
+ "assertions": [
+ "rc and S3 agree on the rule, destination, flags, and removal result."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-replication-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "bucket-replication.put-object",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "s3-api"
+ ],
+ "assertions": [
+ "Payload, metadata, and source version identity converge on the target."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-replication-test.sh"
+ ],
+ "case_ids": [
+ "REP-104"
+ ]
+ }
+ },
+ {
+ "id": "bucket-replication.sync-put-object",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "s3-api"
+ ],
+ "assertions": [
+ "A synchronous PUT returns only after the matching target version is readable."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-replication-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "bucket-replication.prefix-filter",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "s3-api"
+ ],
+ "assertions": [
+ "Prefix matching replicates the included key and excludes the control key."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-replication-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "bucket-replication.tag-filter",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "s3-api"
+ ],
+ "assertions": [
+ "Tag matching replicates the included version and excludes the control version."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-replication-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "bucket-replication.zero-byte-special-key",
+ "source_status": "implemented",
+ "priority": "P1",
+ "channels": [
+ "s3-api"
+ ],
+ "assertions": [
+ "Zero-byte and encoded keys retain their exact key, length, bytes, and version identity."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-replication-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "bucket-replication.multipart-object",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "s3-api"
+ ],
+ "assertions": [
+ "A completed multipart object converges with the same size, checksum, and version identity."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-replication-test.sh"
+ ],
+ "case_ids": [
+ "REP-108"
+ ]
+ }
+ },
+ {
+ "id": "bucket-replication.copy-object",
+ "source_status": "implemented",
+ "priority": "P1",
+ "channels": [
+ "s3-api"
+ ],
+ "assertions": [
+ "CopyObject output is replicated with its replacement metadata and payload."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-replication-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "bucket-replication.delete-marker",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "s3-api"
+ ],
+ "assertions": [
+ "A source delete marker converges by version ID while the preceding object version remains."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-replication-test.sh"
+ ],
+ "case_ids": [
+ "REP-105"
+ ]
+ }
+ },
+ {
+ "id": "bucket-replication.version-delete",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "s3-api"
+ ],
+ "assertions": [
+ "Permanent deletion removes only the addressed target version."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-replication-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "bucket-replication.existing-object-enabled",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "s3-api"
+ ],
+ "assertions": [
+ "Existing-object replication backfills an object created before the rule."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-replication-test.sh"
+ ],
+ "case_ids": [
+ "REP-107"
+ ]
+ }
+ },
+ {
+ "id": "bucket-replication.existing-object-disabled",
+ "source_status": "implemented",
+ "priority": "P1",
+ "channels": [
+ "rc",
+ "s3-api"
+ ],
+ "assertions": [
+ "An object created before a rule stays absent when existing-object replication is disabled while a new control object replicates."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-replication-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "bucket-replication.target-check",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "s3-api"
+ ],
+ "assertions": [
+ "The active target check validates write, version, multipart, delete, and cleanup paths without leaving probe artifacts."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-replication-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "bucket-replication.status-observability",
+ "source_status": "implemented",
+ "priority": "P1",
+ "channels": [
+ "rc",
+ "s3-api"
+ ],
+ "assertions": [
+ "rc list and status identify the configured target and expose no credentials."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-replication-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "bucket-replication.outage-recovery",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "s3-api"
+ ],
+ "assertions": [
+ "A target-outage write converges automatically after the target returns."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-replication-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "bucket-replication.source-restart-recovery",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "s3-api"
+ ],
+ "assertions": [
+ "A queued write survives source restart and converges after target recovery."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-replication-test.sh"
+ ],
+ "case_ids": []
+ }
+ }
+ ]
+ },
+ {
+ "id": "site-replication",
+ "target_environments": [
+ "replication-2site"
+ ],
+ "required_capabilities": [
+ "site-replication",
+ "versioning",
+ "iam",
+ "restart-recovery",
+ "outage-recovery"
+ ],
+ "cases": [
+ {
+ "id": "site-replication.unconfigured-info",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "admin-api"
+ ],
+ "assertions": [
+ "Fresh sites report disabled state without stale credentials or pending operations."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-replication-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "site-replication.invalid-credentials",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "admin-api"
+ ],
+ "assertions": [
+ "Invalid peer credentials fail closed without partially enabling either site."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-replication-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "site-replication.add-and-info",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "admin-api"
+ ],
+ "assertions": [
+ "Both sites converge on the same two unique deployments and a non-secret replication identity."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-replication-test.sh"
+ ],
+ "case_ids": [
+ "SITE-101"
+ ]
+ }
+ },
+ {
+ "id": "site-replication.status-observability",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "admin-api"
+ ],
+ "assertions": [
+ "Status exposes bucket, IAM, ILM, peer-state, and metrics scopes without secrets."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-replication-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "site-replication.bucket-create",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "s3-api"
+ ],
+ "assertions": [
+ "Bucket creation on site A becomes visible on site B."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-replication-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "site-replication.object-bidirectional",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "s3-api"
+ ],
+ "assertions": [
+ "Objects flow in both directions with matching bytes, metadata, and version IDs and without loops."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-replication-test.sh"
+ ],
+ "case_ids": [
+ "SITE-103"
+ ]
+ }
+ },
+ {
+ "id": "site-replication.versioning-metadata",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "s3-api"
+ ],
+ "assertions": [
+ "Enable converges, suspension fails closed while replication is active, and both sites remain enabled."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-replication-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "site-replication.lifecycle-expiry-toggle",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "Expiry metadata is gated while disabled, converges after enable, and the global flag can be disabled again."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-replication-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "site-replication.bucket-policy-metadata",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "s3-api"
+ ],
+ "assertions": [
+ "Bucket policy create and delete operations converge exactly."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-replication-test.sh"
+ ],
+ "case_ids": [
+ "SITE-104"
+ ]
+ }
+ },
+ {
+ "id": "site-replication.object-tags",
+ "source_status": "implemented",
+ "priority": "P1",
+ "channels": [
+ "s3-api"
+ ],
+ "assertions": [
+ "Successive object tag sets converge on the peer."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-replication-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "site-replication.delete-convergence",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "s3-api"
+ ],
+ "assertions": [
+ "Delete markers and permanent version deletes converge without deleting unrelated versions."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-replication-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "site-replication.iam-user-policy",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "s3-api"
+ ],
+ "assertions": [
+ "User, policy, and mapping converge and enforce the same allow/deny boundary on the peer."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-replication-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "site-replication.iam-group-policy",
+ "source_status": "implemented",
+ "priority": "P1",
+ "channels": [
+ "rc",
+ "s3-api"
+ ],
+ "assertions": [
+ "Group membership and group policy mapping converge and authorize the member on the peer."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-replication-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "site-replication.service-account",
+ "source_status": "implemented",
+ "priority": "P1",
+ "channels": [
+ "rc",
+ "s3-api"
+ ],
+ "assertions": [
+ "A service account converges and enforces its session policy without leaking its secret."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-replication-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "site-replication.peer-edit",
+ "source_status": "implemented",
+ "priority": "P1",
+ "channels": [
+ "admin-api"
+ ],
+ "assertions": [
+ "A deployment-ID-targeted rename converges without changing endpoint or deployment identity."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-replication-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "site-replication.resync",
+ "source_status": "implemented",
+ "priority": "P1",
+ "channels": [
+ "admin-api"
+ ],
+ "assertions": [
+ "Resync start and status expose a durable operation ID and cancellation or terminal state is explicit."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-replication-test.sh"
+ ],
+ "case_ids": [
+ "SITE-105"
+ ]
+ }
+ },
+ {
+ "id": "site-replication.outage-recovery",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "Peer outage is observable and queued bucket/object operations converge after recovery."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-replication-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "site-replication.restart-persistence",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "Peer membership and bidirectional replication survive both service restarts."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-replication-test.sh"
+ ],
+ "case_ids": []
+ }
+ },
+ {
+ "id": "site-replication.remove-all",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "Remove-all disables both sites and subsequent buckets no longer propagate."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-replication-test.sh"
+ ],
+ "case_ids": [
+ "SITE-106"
+ ]
+ }
+ },
+ {
+ "id": "site-replication.service-account-rotation",
+ "source_status": "implemented",
+ "priority": "P0",
+ "channels": [
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "A fresh post-removal group is created, then rotation settles in one request on both sites and bidirectional replication remains live."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-replication-test.sh"
+ ],
+ "case_ids": []
+ }
+ }
+ ]
+ },
+ {
+ "id": "kms",
+ "target_environments": [],
+ "required_capabilities": [
+ "kms"
+ ],
+ "cases": [
+ {
+ "id": "kms.sse-kms-round-trip",
+ "source_status": "planned",
+ "priority": "P0",
+ "channels": [
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "Encrypted objects round-trip with the expected key and metadata semantics.",
+ "Expected failures do not return plaintext or leak key material."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-kms-test.sh"
+ ],
+ "case_ids": [
+ "KMS-104",
+ "KMS-301",
+ "KMS-214"
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "id": "data-services",
+ "target_environments": [
+ "lifecycle-b"
+ ],
+ "required_capabilities": [
+ "lifecycle"
+ ],
+ "cases": [
+ {
+ "id": "tiering.transition-and-restore",
+ "source_status": "planned",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "Transitioned objects remain readable and can be restored."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-tier-test.sh"
+ ],
+ "case_ids": [
+ "TIER-105",
+ "TIER-107"
+ ]
+ }
+ },
+ {
+ "id": "events.delivery",
+ "source_status": "planned",
+ "priority": "P1",
+ "channels": [
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "Configured object events are delivered once according to the case contract."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-tier-test.sh"
+ ],
+ "case_ids": [
+ "EVT-103",
+ "EVT-104"
+ ]
+ }
+ },
+ {
+ "id": "audit.request-correlation",
+ "source_status": "planned",
+ "priority": "P1",
+ "channels": [
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "Customer operations produce correlated audit records without secrets."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-tier-test.sh"
+ ],
+ "case_ids": [
+ "AUD-102",
+ "AUD-104",
+ "AUD-106"
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "id": "identity-access",
+ "target_environments": [],
+ "required_capabilities": [
+ "oidc",
+ "iam"
+ ],
+ "cases": [
+ {
+ "id": "iam.policy-enforcement",
+ "source_status": "planned",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "Allowed actions succeed and explicitly denied actions fail closed."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-security-test.sh"
+ ],
+ "case_ids": [
+ "IAM-105",
+ "IAM-109"
+ ]
+ }
+ },
+ {
+ "id": "oidc.login-and-session",
+ "source_status": "planned",
+ "priority": "P1",
+ "channels": [
+ "admin-api",
+ "s3-api"
+ ],
+ "assertions": [
+ "OIDC identity and session claims enforce the configured access boundary."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-security-test.sh"
+ ],
+ "case_ids": [
+ "OIDC-103"
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "id": "s3-compatibility",
+ "target_environments": [
+ "single-1disk"
+ ],
+ "required_capabilities": [
+ "s3-basic"
+ ],
+ "cases": [
+ {
+ "id": "s3.basic-object-round-trip",
+ "source_status": "planned",
+ "priority": "P0",
+ "channels": [
+ "rc",
+ "s3-api"
+ ],
+ "assertions": [
+ "Bucket and object CRUD preserve payload, metadata, ETag, and error semantics."
+ ],
+ "probes": {},
+ "reuse_candidates": {
+ "suites": [
+ "rustfs-s3-compat-test.sh"
+ ],
+ "case_ids": [
+ "S3C-101",
+ "S3C-201",
+ "S3C-203",
+ "S3C-205",
+ "S3C-206",
+ "S3C-207"
+ ]
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/validation-migration-review.json b/validation-migration-review.json
new file mode 100644
index 00000000..e9ca52e9
--- /dev/null
+++ b/validation-migration-review.json
@@ -0,0 +1,3223 @@
+{
+ "schema_version": 1,
+ "as_of": "2026-10-28",
+ "source_commit": "524eb5b32c3236497fdd8a9271df0494fa3d9974",
+ "source_sha256": "463fdd02dee43e42fef4f3aacf7506709ce87af535e1a53d91c521bc2e361cad",
+ "reviewed_testing_commit": "f0744d198769feecb2cb038ab96b507a1784d44d",
+ "scope": "Test implementation migration and candidate live acceptance are independent. This review does not rewrite original results, change migration_status semantics or relax the canonical gate. Environment procedures remain disabled by default under the accepted delivery scope.",
+ "evidence_basis": "20261026-canonical-migration-ledger-7094.json plus later G01/G02 attempts and source review on 2026-10-28; historical result bytes and audit reports remain in the separate evidence store.",
+ "reviewed_files": {
+ "scripts/run_validation_case.sh": "ac51328ff860074f285999236a07ec8d2274d56607dc7c6c6389747fd7a0c498",
+ "scripts/validation_heal/authority.py": "8f2225f184427674de1f45aff4c4976c812f9d70fd4bc927ce460726a4ff74a0",
+ "scripts/validation_heal/authority_runner.py": "d1dce07019a37318f9fbfc9c4d7383fa259d8d6da881ebacf083a67d04fc2545",
+ "scripts/validation_heal/bounded_volume.py": "56f57d2bef882af81a7bf05365fa8b5ae01f9fcdd515b3bc930e1f36cf98e944",
+ "scripts/validation_heal/bucket_metadata.py": "3fba015f0c3ca60b979b404d51c3071a61324ba1d1e9558be85b178b8f2d19fb",
+ "scripts/validation_heal/bucket_records.py": "2b46fc9923eb9d5f6d913688885580e270ac697c68467f2ce63d484fa3d7c2e1",
+ "scripts/validation_heal/caller_abort.py": "908f666cd689f4b71c3a6fac0b795b26d9058c1e20efd1e8f4858dc2359810af",
+ "scripts/validation_heal/capacity.py": "67a2b0836603cb7ddbb526a528c5f7896527cec406ba30c92afed4fc8188c990",
+ "scripts/validation_heal/coordinator_crash.py": "9188d624e2429900c168e8500b740a4b3222831dc465eb74eab9fb62c11ed235",
+ "scripts/validation_heal/environment.py": "93f13101553e6d3052147f1b4848cead44c201dc8447b4de5c898f7c1201865a",
+ "scripts/validation_heal/environment_gate.py": "b54ccb7a100d111289380759eb4b10c65a324280d3f50551031d87243205fcb4",
+ "scripts/validation_heal/execution_policy.py": "b5582404e09f19d70255f8da71cea0895c154afe4e1b3f5497c2d76361e53b6a",
+ "scripts/validation_heal/flapping.py": "58ea6a0d480fa3188975658421f9dd8a92a89109fbaa2f34329f668f71bd1096",
+ "scripts/validation_heal/generation_fault.py": "5a35483a3e4d21cbfede1e1262bb6bf911bb077f7f08eaeb94b34204d19775c1",
+ "scripts/validation_heal/incarnation.py": "8ea507c13a471fbc52e9666c26a3ab3f6e276337d4c75579885ba9f929f277c9",
+ "scripts/validation_heal/incarnation_native.py": "1cd9d069953d833e1f37455ded67719d52a2dedc9bd57d1295de6167443002c0",
+ "scripts/validation_heal/incarnation_replay.py": "e53de3d51f910a4f8467f6bcfaeb3496a7bc8b655acc0e7cd4a906ad3f1ba364",
+ "scripts/validation_heal/journal_fault.py": "cc171b8f53f6faf54ef4ebf559a8cdfcf6ec0c59ca3e290cb636b57a9c5e1a12",
+ "scripts/validation_heal/lock_observer.py": "9f513ec659e27338b05a23447ce63dfaecbb69eb6a53852f9eacd86b3cc956d3",
+ "scripts/validation_heal/lock_source.py": "02881928da918e20ca1f184dfac91518e92974d0fbb408a9c0adde274e47d889",
+ "scripts/validation_heal/lock_successor.py": "05ce5bbe93cad2fa2bec5c7b6ea8c6d956e40690dff58ac850c4aa47a4d6a683",
+ "scripts/validation_heal/lock_timeout.py": "e7e672955cfdbe30579476989125b31ccb997014fdaabd7867749742abd37b39",
+ "scripts/validation_heal/lock_timeout_runner.py": "0b546bb38471d0005b6d01a56844d633cae938d4196f82e3177f4345f0065304",
+ "scripts/validation_heal/manager_fixture.py": "d73018ed9261b9d0545c5ca6a8db51c5a225bc196e085950320c1a4ab51045e5",
+ "scripts/validation_heal/manager_native.py": "40d77e6bd18384520659e1cbe20ba58892f22929ef694c6ad39ccfd6d2af3740",
+ "scripts/validation_heal/markers.py": "4ddb66c0a0defd041acaccdfbf7db6f79c51633b76143cf3557200c080e016b5",
+ "scripts/validation_heal/mpu_race_runner.py": "22cdb841747882a6666988fa916b85aaae548ebb5314927373ef181b6347d25d",
+ "scripts/validation_heal/mrf_records.py": "0a262ba5e0ac61605c48bc8c582b7f7903a03d51585ec8c242d242240f43a7c9",
+ "scripts/validation_heal/mrf_replay.py": "95d4419232bf12799f179abd12d2073e7831b093aa3b2dd4ef386c12826da879",
+ "scripts/validation_heal/multi_pool.py": "82397e5d5562af01e3c5010c28def365368a669bef82aa3b1efd5f5633561c20",
+ "scripts/validation_heal/multi_pool_layout.py": "365febcf07fe05ccd05af92e17d20b744e6603271e42555271208dfb96121671",
+ "scripts/validation_heal/native_regressions.py": "96ed809536d3275e72bdbcc64aa534f65976da5d6e0e85b7056623a14c7e3702",
+ "scripts/validation_heal/network.py": "c969e3acf791a86dc1d425306af421339e358bef02200bd9639fa5c65e317c29",
+ "scripts/validation_heal/network_gate.py": "f4291a98095aee46bf1213d5e70a59b83858b213a484cc2c452e82c514c02d1a",
+ "scripts/validation_heal/network_load.py": "169a7b59c5cf12b939446f778735ad7edcb939817f23420725349487c2a45d04",
+ "scripts/validation_heal/node_outage.py": "cf10109d7df387550a985bee9d65e4af19b3c6618563b19e3418d48da7b23fab",
+ "scripts/validation_heal/nolock.py": "be1812cd4f03a433f808b468e59e26868047a765a70e2b1890a5d61cd5af264b",
+ "scripts/validation_heal/outcome.py": "173ea18f85f9f340bf8bb9d97924a74d01697488fe9bb52896cab5a4441ee0af",
+ "scripts/validation_heal/outcome_grace.py": "a4cf21cd442be6280194796b179ff8d756322d557c108a3d559d9b0e7ed52374",
+ "scripts/validation_heal/outcome_recovery.py": "e623c993afcfebdf392fd54f7d2e958e8835828b37865b6452373229bffb80ea",
+ "scripts/validation_heal/performance.py": "81f85a2ea0ecad3b8ddec701eba776b98b1ace5f1e303631bc53cf5c73bf5a57",
+ "scripts/validation_heal/persistence.py": "a1f3d86cf359bbcbb6be1f60f01f3c8bb4c69dfc74cf678a85c4f0170a73ccda",
+ "scripts/validation_heal/persistence_mrf.py": "018fcbde989d2a2c4f9f4c18ce1b066a6acff323ac5fbc1f4818a6e3b2502c06",
+ "scripts/validation_heal/persistence_native.py": "368dcc4b6171b4aba119694dc7b510faf460cc5a4a7d8e36d1a28f2b0583355a",
+ "scripts/validation_heal/race.py": "ebae41951dea116549ef7f3082a5e023151e9769e016641c19ebbd91028cc86c",
+ "scripts/validation_heal/race_io.py": "b45930a0327e18716a1953210888760717621bf229981d0bd8ed7b3c0d4f1322",
+ "scripts/validation_heal/race_native.py": "fca55264c42ebad30fc796085512e959fa827753b3f0f52e14c09738f21464f6",
+ "scripts/validation_heal/race_native_contracts.json": "cdd6228d9c99dc1c550d4044743d78c58e95c22f0bb8076bc9611b2fc18e01a4",
+ "scripts/validation_heal/race_native_evidence.py": "caa2b5c63a419e2c0c29c6d5e38b50ab1ca34df03de10ccbd2e30954158339f4",
+ "scripts/validation_heal/recipes.json": "9dbfc7336e9cac1b8b8b7f8b5889eeb117cf4bca1678774249f553ddc8270a03",
+ "scripts/validation_heal/replace_drive.py": "dd7e13a85fdaf5e92b2f744167c6a8e4af9307ca827a78462e11082a2ba2349c",
+ "scripts/validation_heal/replacement.py": "051161cf09769f327596e30088fa0630014ddf5fac039dcca01e0b613ab84b98",
+ "scripts/validation_heal/replacement_preflight.py": "5440f25f5d9951e92513953733d4975f3893c883b0d4403ee6280131e2ca20d0",
+ "scripts/validation_heal/run_case.sh": "c8ae24b39c554c52a0f85689617c46d1eeba10d4525c403c82da11c06ac180e6",
+ "scripts/validation_heal/schema.py": "75a92ddea811e744a405ff1fde1968c92c8e2424b48e063a6ea23d07de529efa",
+ "scripts/validation_heal/schema_faults.py": "80a3f88920e6312950ef09f044ead08afbb5b7839a2f1c4895eba57cdf821e5d",
+ "scripts/validation_heal/status.py": "3c9af875227afafb3b9be61bb2c5a818fed247916b52e0fd4619cd8153c62cc0",
+ "scripts/validation_heal/unformatted.py": "70d00af8ac1ef7e9bbd1d500c5c4de06b7995bd5219dc59527c824a56cb8f2c2",
+ "scripts/validation_heal/version_race_runner.py": "5dd244ae2aa816907d3711a6a7c9bf3dac76265c76967f7803437195894ff0e0",
+ "scripts/validation_heal/versions.py": "fa77397a58b0db4499114c0e30a766ef67f997d1c88f22fcfe02752820b5eea0",
+ "scripts/validation_pool/case_plan.py": "277777e040501c8a79e7104471f411b054d901afb920d18f3e222d3a0b7b03bc",
+ "scripts/validation_pool/pool_behavior.py": "203bc3a2b7e85fb95482dd33efc937cff9f1f7e23f8ac2aa0b9ad2f40aff4632",
+ "scripts/validation_pool/run_case.sh": "a1de6d972964ae4232bdc1c7c4a61cb9e4a444825c7c1ced5c74cbc72f6a5ebe",
+ "test_validation_heal.py": "6e4e25d2437a49db1af0b3020bb3a2509496b21cb3c27216a6b0322aea9bfbdc",
+ "test_validation_heal_authority.py": "a4334246aa9572be05a3d6881779df1e2495d9706d3a9289ba9437ac866a688a",
+ "test_validation_heal_bucket_metadata.py": "331bb5aa9bce132bd2d69b81262dae0eeffd76761139c9a382dd3884f400fb1c",
+ "test_validation_heal_caller_abort.py": "0d67e6f3b399c4c0392c768cf7bf9d7dd8e6166d911b942d7ed3826cb0aacfc5",
+ "test_validation_heal_capacity.py": "a07caec36bba2522b7ad48a86a953f8a702fc5219e0dd63d34ecb8b6f6f139e5",
+ "test_validation_heal_coordinator_crash.py": "10b8ff58c618e85b4cc97e4780cc8abe1b183f1be6f64a8a5649fe36d8f3a691",
+ "test_validation_heal_environment.py": "6c3a22ae0d091db396b8f9875eed9779ac35e570c90496dffa262d4cb09c34c3",
+ "test_validation_heal_environment_gate.py": "2ecbb7fe0fe2e723aa93160f609b2c63e97c6aa807a217f67170b169a9e3f9a1",
+ "test_validation_heal_flapping.py": "e310601f83d75dd65f6fa268484f40d56a7f8e8412fa9499f942d86bd53e0e3d",
+ "test_validation_heal_g02_source.py": "e817c99de1c7b213ca8b6c1277064ed3de2f438b4f82fa1ece14588c67870ff4",
+ "test_validation_heal_g03_source.py": "9cf62d56f13decdb5665d4da6cd34840c17ae6277d3e6ce829640b1bcc4b02f3",
+ "test_validation_heal_incarnation.py": "e9ade5528ded36ad9d04ec5b7bff623b9f68a9e646a9ab118ff56420c0408455",
+ "test_validation_heal_incarnation_native.py": "2cb9ed6763f45c3e6f124ff4053a7b183f35b3327153ac67818d65b74442e0b6",
+ "test_validation_heal_incarnation_replay.py": "89aa38d154152b699026c790e65093f1e28d9c4c2b6e8b6bf0f742074a39c3d3",
+ "test_validation_heal_journal_fault.py": "712ae93218b99cf4a793784ac860fe3a8c4fe1ab3e3577a2c73371a4de9a5d27",
+ "test_validation_heal_lock_observer.py": "d4b450b9f8251390c1128123129b45e7c3ad3190b0de4f4f6c2ae4175cf03f51",
+ "test_validation_heal_lock_source.py": "6038f1f59e878c6d6607d531733475949f5e8f788d8aefccd1258def6e40c513",
+ "test_validation_heal_lock_successor.py": "d31a684464619470c0de8b3213f08aaf055d7f56b220b7c4c2ee725d31026b38",
+ "test_validation_heal_lock_timeout.py": "59cd644449ed038a269ec52d735566da2e79a81e3d36a887ed68c289af0c7741",
+ "test_validation_heal_lock_timeout_runner.py": "e07c3ed6955a5d5c004bb648b2884c6867520716a8619a8302f85e2c30f0f819",
+ "test_validation_heal_manager_native.py": "a5635fae08b57f2a7fff8725549c580ba7fe48c15df8421889c80f519ae46da4",
+ "test_validation_heal_mpu_race_runner.py": "d0831311db460fe476831718c7268334eba529dd2978add1569f0eeeb77c3ade",
+ "test_validation_heal_mrf.py": "f9c9eb680e8e4686290baa58dae7a0e390ad4026b1714a19669ec83610c1be01",
+ "test_validation_heal_multi_pool.py": "d2bf816363548378bf63eae21e09bf4770874538a471615d8e1d450828d79352",
+ "test_validation_heal_native.py": "207b043f91fb4ce64e3a535a353c1c81b99ebf62964f68a7f66eb81d99c6c7ec",
+ "test_validation_heal_network.py": "b1ed0b1416f9b7b0a1954056df304d3ec667f2e7af935a86589e3563c1756acf",
+ "test_validation_heal_nolock.py": "66ca7212e9fdb86ffdb2cd3a6e9186a2f484d57edda287a900972cb9409a1771",
+ "test_validation_heal_outcome.py": "6744c6f0f79aba06e9ac1a97e51b69e5f6cef86a361ee9e2a2d201156fd05631",
+ "test_validation_heal_outcome_grace.py": "391f5ad225f80661562f59be9cb5150a857c0d258e3aa852e00cc07b1068cf4e",
+ "test_validation_heal_outcome_recovery.py": "46b21a03ad7fc656273d930178c1ebefb212bcc9faa125b3834c6f87a102305c",
+ "test_validation_heal_performance.py": "92e3e09fca551c0a8a1daef4d7abdaba11e13dd4c021a33e601ad1e0bad7d924",
+ "test_validation_heal_persistence.py": "d258b82634bfd19db7c4b1f6fb5dadcf33fafc595e3663a3a30a4720d0079f4f",
+ "test_validation_heal_persistence_mrf.py": "61dfa042c6b237e00b058b2660962411e201cda8908027c6900649da344b4cbe",
+ "test_validation_heal_race.py": "1f2097aff5ad08c4d2834bfd38e1eb1d1b373ecde8f7e6a3455b713114a07d3d",
+ "test_validation_heal_race_native.py": "850c9683ac918834d22b002ec2f5a8c0d7fd2b74266461e4912776dfceb0d93d",
+ "test_validation_heal_schema.py": "5df440df29e7085d48590665129f87b0791f9fa8c7f64c8c0432000bc2002606",
+ "test_validation_heal_unformatted.py": "58bc43f3f0905c8c4b7d0f8e656d25f0a17e197b5a2dfb66a88b6af9747883a7",
+ "test_validation_pool.py": "300adcef396533b8cbb54864ccd043b9f1b897558042039ad9b9aec60ed2aade"
+ },
+ "cases": [
+ {
+ "case_id": "pool.status",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38047001285
+ ]
+ }
+ },
+ {
+ "case_id": "pool.expand-and-rebalance",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38054208114
+ ]
+ }
+ },
+ {
+ "case_id": "pool.expansion-restart-persistence",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38054287896
+ ]
+ }
+ },
+ {
+ "case_id": "pool.rebalance-stop-resume",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38110959836
+ ]
+ }
+ },
+ {
+ "case_id": "pool.rebalance-live-io",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38054696080
+ ]
+ }
+ },
+ {
+ "case_id": "pool.decommission",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38051227313
+ ]
+ }
+ },
+ {
+ "case_id": "pool.decommission-cancel-clear-resume",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38052304396
+ ]
+ }
+ },
+ {
+ "case_id": "pool.decommission-restart-recovery",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38053814812
+ ]
+ }
+ },
+ {
+ "case_id": "pool.decommission-versioned-integrity",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38054128863
+ ]
+ }
+ },
+ {
+ "case_id": "pool.decommission-multipart-integrity",
+ "implementation": "implemented",
+ "basis": "Source branches have execution paths and strict original oracles in the referenced controller/helpers. Contract tests cover those paths; this source review is not a claim that every live branch passed or that an unattributed failure is a product defect.",
+ "references": [
+ "scripts/validation_pool/run_case.sh",
+ "scripts/validation_pool/pool_behavior.py",
+ "scripts/validation_pool/case_plan.py",
+ "test_validation_pool.py"
+ ],
+ "source_variants": {
+ "catalog-assertions": [
+ "scripts/validation_pool/run_case.sh"
+ ]
+ },
+ "gaps": [],
+ "live": {
+ "state": "retained-failure",
+ "finding": "Retained failure or incomplete live evidence. Product behavior, environment and controller responsibility must be attributed separately; a FAIL alone does not demonstrate a missing test implementation.",
+ "retained_runs": [
+ 38053715867
+ ],
+ "original_result_sha256": "ac83aa195a66592971b36cff1734aad8ebf5f6231cd1f1cfe1a0f720fd0bc786"
+ }
+ },
+ {
+ "case_id": "pool.decommission-live-io",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38053444515
+ ]
+ }
+ },
+ {
+ "case_id": "pool.retired-config-removal",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38111198632
+ ]
+ }
+ },
+ {
+ "case_id": "pool.operation-guards",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38054540360
+ ]
+ }
+ },
+ {
+ "case_id": "heal.admin-healthy-noop",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38056636793
+ ]
+ }
+ },
+ {
+ "case_id": "heal.admin-scope-isolation",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38094094085
+ ]
+ }
+ },
+ {
+ "case_id": "heal.admin-token-cross-node-status",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38198122041
+ ]
+ }
+ },
+ {
+ "case_id": "heal.admin-dry-run",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38057908339
+ ]
+ }
+ },
+ {
+ "case_id": "heal.admin-overlap-and-force-start",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 39892106641
+ ]
+ }
+ },
+ {
+ "case_id": "heal.admin-stop-and-stale-token",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38099706345
+ ]
+ }
+ },
+ {
+ "case_id": "heal.admin-invalid-request",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38052868969,
+ 38053214984
+ ]
+ }
+ },
+ {
+ "case_id": "heal.admin-authorization",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38069648003
+ ]
+ }
+ },
+ {
+ "case_id": "heal.admin-nolock-safety",
+ "implementation": "implemented",
+ "basis": "Source branches have execution paths and strict original oracles in the referenced controller/helpers. Contract tests cover those paths; this source review is not a claim that every live branch passed or that an unattributed failure is a product defect.",
+ "references": [
+ "scripts/validation_heal/nolock.py",
+ "scripts/validation_heal/race_native_evidence.py",
+ "scripts/validation_heal/native_regressions.py",
+ "test_validation_heal_nolock.py",
+ "test_validation_heal_native.py"
+ ],
+ "source_variants": {
+ "overwrite-nolock": [
+ "scripts/validation_heal/nolock.py"
+ ],
+ "delete-nolock": [
+ "scripts/validation_heal/nolock.py"
+ ],
+ "native-lock-fence": [
+ "scripts/validation_heal/nolock.py"
+ ]
+ },
+ "gaps": [],
+ "live": {
+ "state": "pending",
+ "finding": "Retained failure or incomplete live evidence. Product behavior, environment and controller responsibility must be attributed separately; a FAIL alone does not demonstrate a missing test implementation."
+ }
+ },
+ {
+ "case_id": "heal.admin-option-contract",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 39197088183
+ ]
+ }
+ },
+ {
+ "case_id": "heal.disk-loss-and-repair",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 39899825839
+ ]
+ }
+ },
+ {
+ "case_id": "heal.disk-readonly-and-permission",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 39927051614
+ ]
+ }
+ },
+ {
+ "case_id": "heal.disk-replacement-formatted",
+ "implementation": "implemented",
+ "basis": "Source branches have execution paths and strict original oracles in the referenced controller/helpers. Contract tests cover those paths; this source review is not a claim that every live branch passed or that an unattributed failure is a product defect.",
+ "references": [
+ "scripts/validation_heal/versions.py",
+ "scripts/validation_heal/replace_drive.py",
+ "scripts/validation_heal/replacement_preflight.py",
+ "test_validation_heal.py",
+ "test_validation_heal_environment.py"
+ ],
+ "source_variants": {
+ "new-disk-data": [
+ "scripts/validation_heal/versions.py"
+ ],
+ "new-disk-versioned": [
+ "scripts/validation_heal/versions.py"
+ ]
+ },
+ "gaps": [],
+ "live": {
+ "state": "retained-failure",
+ "finding": "Environment admission: the current run-private endpoint is not an admitted exact mount and no reviewed mount-switch resource adapter is available. The source procedure and read-only predeployment refusal exist. Preserve old FAIL results; a new refused run is BLOCKED before fixture/deployment.",
+ "retained_runs": [
+ 38084312163
+ ],
+ "original_result_sha256": "be7474e0e78707362d32dd95e93aa090275afb3ce60dbe9b76dd0dbab6181916"
+ }
+ },
+ {
+ "case_id": "heal.disk-replacement-unformatted",
+ "implementation": "implemented",
+ "basis": "Source branches have execution paths and strict original oracles in the referenced controller/helpers. Contract tests cover those paths; this source review is not a claim that every live branch passed or that an unattributed failure is a product defect.",
+ "references": [
+ "scripts/validation_heal/unformatted.py",
+ "scripts/validation_heal/replace_drive.py",
+ "scripts/validation_heal/replacement_preflight.py",
+ "test_validation_heal_unformatted.py",
+ "test_validation_heal_environment.py"
+ ],
+ "source_variants": {
+ "blank-unformatted": [
+ "scripts/validation_heal/unformatted.py"
+ ],
+ "foreign-format": [
+ "scripts/validation_heal/unformatted.py"
+ ]
+ },
+ "gaps": [],
+ "live": {
+ "state": "retained-failure",
+ "finding": "Environment admission: the current run-private endpoint is not an admitted exact mount and no reviewed mount-switch resource adapter is available. The source procedure and read-only predeployment refusal exist. Preserve old FAIL results; a new refused run is BLOCKED before fixture/deployment.",
+ "retained_runs": [
+ 38087657754
+ ],
+ "original_result_sha256": "ba4b7bab6407b7bbf1965ca8c54f5fbdbb705c79728f02375680b67a867c7e7f"
+ }
+ },
+ {
+ "case_id": "heal.multiple-shard-loss",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 39843970314
+ ]
+ }
+ },
+ {
+ "case_id": "heal.read-quorum-boundary",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 39223475622
+ ]
+ }
+ },
+ {
+ "case_id": "heal.below-read-quorum",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38727279347
+ ]
+ }
+ },
+ {
+ "case_id": "heal.below-write-quorum",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 39915022106
+ ]
+ }
+ },
+ {
+ "case_id": "heal.node-outage-rejoin",
+ "implementation": "implemented",
+ "basis": "Source branches have execution paths and strict original oracles in the referenced controller/helpers. Contract tests cover those paths; this source review is not a claim that every live branch passed or that an unattributed failure is a product defect.",
+ "references": [
+ "scripts/validation_heal/node_outage.py",
+ "scripts/validation_heal/mrf_records.py",
+ "test_validation_heal.py"
+ ],
+ "source_variants": {
+ "one-node-stop": [
+ "scripts/validation_heal/node_outage.py"
+ ],
+ "rejoin-with-new-versions": [
+ "scripts/validation_heal/node_outage.py"
+ ]
+ },
+ "gaps": [],
+ "live": {
+ "state": "retained-failure",
+ "finding": "Retained failure or incomplete live evidence. Product behavior, environment and controller responsibility must be attributed separately; a FAIL alone does not demonstrate a missing test implementation.",
+ "retained_runs": [
+ 38202851374
+ ],
+ "original_result_sha256": "f2f215cb5859a5b03db631537e8795c03a001d3b91e506ff678ae836b29836b4"
+ }
+ },
+ {
+ "case_id": "heal.disk-and-node-flapping",
+ "implementation": "implemented",
+ "basis": "Source branches have execution paths and strict original oracles in the referenced controller/helpers. Contract tests cover those paths; this source review is not a claim that every live branch passed or that an unattributed failure is a product defect.",
+ "references": [
+ "scripts/validation_heal/flapping.py",
+ "scripts/validation_heal/replacement.py",
+ "scripts/validation_heal/mrf_records.py",
+ "test_validation_heal_flapping.py"
+ ],
+ "source_variants": {
+ "drive-flap-3": [
+ "scripts/validation_heal/flapping.py"
+ ],
+ "node-flap-3": [
+ "scripts/validation_heal/flapping.py"
+ ],
+ "interrupt-rebuild-again": [
+ "scripts/validation_heal/flapping.py"
+ ]
+ },
+ "gaps": [],
+ "live": {
+ "state": "retained-failure",
+ "finding": "Retained failure or incomplete live evidence. Product behavior, environment and controller responsibility must be attributed separately; a FAIL alone does not demonstrate a missing test implementation.",
+ "retained_runs": [
+ 38104306522
+ ],
+ "original_result_sha256": "3a41b150a39603eefce45f868588bdd2dca7b5136b1b13346803f968f22bd126"
+ }
+ },
+ {
+ "case_id": "heal.missing-part-file",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38057373420
+ ]
+ }
+ },
+ {
+ "case_id": "heal.data-shard-bitrot",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 39281951263
+ ]
+ }
+ },
+ {
+ "case_id": "heal.parity-shard-bitrot",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 39286997859
+ ]
+ }
+ },
+ {
+ "case_id": "heal.truncated-and-zeroed-shard",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38203454300
+ ]
+ }
+ },
+ {
+ "case_id": "heal.missing-or-corrupt-xlmeta",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 39322217398
+ ]
+ }
+ },
+ {
+ "case_id": "heal.stale-version-rejoin",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38085554143
+ ]
+ }
+ },
+ {
+ "case_id": "heal.conflicting-subquorum-authority",
+ "implementation": "implemented",
+ "basis": "Source branches have execution paths and strict original oracles in the referenced controller/helpers. Contract tests cover those paths; this source review is not a claim that every live branch passed or that an unattributed failure is a product defect.",
+ "references": [
+ "scripts/validation_heal/authority_runner.py",
+ "scripts/validation_heal/authority.py",
+ "scripts/validation_heal/generation_fault.py",
+ "test_validation_heal_authority.py"
+ ],
+ "source_variants": {
+ "disjoint-version-quorums": [
+ "scripts/validation_heal/authority_runner.py"
+ ],
+ "disjoint-incarnations": [
+ "scripts/validation_heal/authority_runner.py"
+ ],
+ "native-authority-fence": [
+ "scripts/validation_heal/authority_runner.py"
+ ]
+ },
+ "gaps": [],
+ "live": {
+ "state": "retained-failure",
+ "finding": "Retained failure or incomplete live evidence. Product behavior, environment and controller responsibility must be attributed separately; a FAIL alone does not demonstrate a missing test implementation.",
+ "retained_runs": [
+ 38119830051
+ ],
+ "original_result_sha256": "ccc86e06eea4bf6146af9f2ed35ee2d89ec495ebf3f61b219a6f62f927909576"
+ }
+ },
+ {
+ "case_id": "heal.metadata-payload-mismatch",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38235978152
+ ]
+ }
+ },
+ {
+ "case_id": "heal.foreign-format-and-unknown-schema",
+ "implementation": "implemented",
+ "basis": "Source branches have execution paths and strict original oracles in the referenced controller/helpers. Contract tests cover those paths; this source review is not a claim that every live branch passed or that an unattributed failure is a product defect.",
+ "references": [
+ "scripts/validation_heal/schema.py",
+ "scripts/validation_heal/schema_faults.py",
+ "test_validation_heal_schema.py"
+ ],
+ "source_variants": {
+ "foreign-disk": [
+ "scripts/validation_heal/schema.py"
+ ],
+ "foreign-set": [
+ "scripts/validation_heal/schema.py"
+ ],
+ "future-format": [
+ "scripts/validation_heal/schema.py"
+ ],
+ "future-object-schema": [
+ "scripts/validation_heal/schema.py"
+ ]
+ },
+ "gaps": [],
+ "live": {
+ "state": "retained-failure",
+ "finding": "Retained failure or incomplete live evidence. Product behavior, environment and controller responsibility must be attributed separately; a FAIL alone does not demonstrate a missing test implementation.",
+ "retained_runs": [
+ 38098069878
+ ],
+ "original_result_sha256": "c831443b952fa70fe26e8315527895f49aa9089912f9fc21122ffc5467dcfd59"
+ }
+ },
+ {
+ "case_id": "heal.dangling-and-orphan-safety",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38209676009
+ ]
+ }
+ },
+ {
+ "case_id": "heal.bucket-incarnation-recreation",
+ "implementation": "implemented",
+ "basis": "Source branches have execution paths and strict original oracles in the referenced controller/helpers. Contract tests cover those paths; this source review is not a claim that every live branch passed or that an unattributed failure is a product defect. All source branches are present in the explicit diagnostic route; canonical admission remains independently blocked.",
+ "references": [
+ "scripts/validation_heal/incarnation.py",
+ "scripts/validation_heal/incarnation_replay.py",
+ "scripts/validation_heal/incarnation_native.py",
+ "scripts/validation_heal/manager_native.py",
+ "test_validation_heal_incarnation.py",
+ "test_validation_heal_incarnation_replay.py",
+ "test_validation_heal_incarnation_native.py"
+ ],
+ "source_variants": {
+ "old-metadata-rejoin": [
+ "scripts/validation_heal/incarnation.py"
+ ],
+ "old-checkpoint-replay": [
+ "scripts/validation_heal/incarnation.py"
+ ],
+ "old-mrf-replay": [
+ "scripts/validation_heal/incarnation.py"
+ ],
+ "native-incarnation-fence": [
+ "scripts/validation_heal/incarnation.py"
+ ]
+ },
+ "gaps": [],
+ "live": {
+ "state": "blocked",
+ "finding": "Retained failure or incomplete live evidence. Product behavior, environment and controller responsibility must be attributed separately; a FAIL alone does not demonstrate a missing test implementation."
+ }
+ },
+ {
+ "case_id": "heal.object-size-boundaries",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38067139720
+ ]
+ }
+ },
+ {
+ "case_id": "heal.completed-multipart-object",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38204339623
+ ]
+ }
+ },
+ {
+ "case_id": "heal.historical-versions",
+ "implementation": "implemented",
+ "basis": "Source branches have execution paths and strict original oracles in the referenced controller/helpers. Contract tests cover those paths; this source review is not a claim that every live branch passed or that an unattributed failure is a product defect.",
+ "references": [
+ "scripts/validation_heal/versions.py",
+ "test_validation_heal.py"
+ ],
+ "source_variants": {
+ "old-version-part": [
+ "scripts/validation_heal/versions.py"
+ ],
+ "current-version-part": [
+ "scripts/validation_heal/versions.py"
+ ],
+ "recursive-all-versions": [
+ "scripts/validation_heal/versions.py"
+ ],
+ "multiple-version-targets": [
+ "scripts/validation_heal/versions.py"
+ ]
+ },
+ "gaps": [],
+ "live": {
+ "state": "retained-failure",
+ "finding": "Retained failure or incomplete live evidence. Product behavior, environment and controller responsibility must be attributed separately; a FAIL alone does not demonstrate a missing test implementation.",
+ "retained_runs": [
+ 38059278988
+ ],
+ "original_result_sha256": "9e7b278019acf80a56bcd395e2872ee6f227c5c0020c0b1be91246186441a9ee"
+ }
+ },
+ {
+ "case_id": "heal.delete-marker-semantics",
+ "implementation": "implemented",
+ "basis": "Source branches have execution paths and strict original oracles in the referenced controller/helpers. Contract tests cover those paths; this source review is not a claim that every live branch passed or that an unattributed failure is a product defect.",
+ "references": [
+ "scripts/validation_heal/versions.py",
+ "scripts/validation_heal/markers.py",
+ "test_validation_heal.py"
+ ],
+ "source_variants": {
+ "current-marker": [
+ "scripts/validation_heal/versions.py"
+ ],
+ "historical-marker": [
+ "scripts/validation_heal/versions.py"
+ ],
+ "missing-marker-replica": [
+ "scripts/validation_heal/versions.py"
+ ],
+ "stale-disk-marker": [
+ "scripts/validation_heal/versions.py"
+ ]
+ },
+ "gaps": [],
+ "live": {
+ "state": "retained-failure",
+ "finding": "Retained failure or incomplete live evidence. Product behavior, environment and controller responsibility must be attributed separately; a FAIL alone does not demonstrate a missing test implementation.",
+ "retained_runs": [
+ 38204764944
+ ],
+ "original_result_sha256": "33c0aa8fe51cc436a3e6fe9d8a948b570d06f336552500f1774ff0f84001a030"
+ }
+ },
+ {
+ "case_id": "heal.versioning-suspended-null",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38061843290
+ ]
+ }
+ },
+ {
+ "case_id": "heal.object-metadata-and-tags",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38061589839
+ ]
+ }
+ },
+ {
+ "case_id": "heal.range-head-and-conditions",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38205235080
+ ]
+ }
+ },
+ {
+ "case_id": "heal.encrypted-objects",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38229589497
+ ]
+ }
+ },
+ {
+ "case_id": "heal.object-lock-retention",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38205854024
+ ]
+ }
+ },
+ {
+ "case_id": "heal.special-keys-and-pagination",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38207381816
+ ]
+ }
+ },
+ {
+ "case_id": "heal.bucket-metadata-integrity",
+ "implementation": "implemented",
+ "basis": "Source branches have execution paths and strict original oracles in the referenced controller/helpers. Contract tests cover those paths; this source review is not a claim that every live branch passed or that an unattributed failure is a product defect.",
+ "references": [
+ "scripts/validation_heal/bucket_metadata.py",
+ "scripts/validation_heal/bucket_records.py",
+ "test_validation_heal_bucket_metadata.py"
+ ],
+ "source_variants": {
+ "versioning-config": [
+ "scripts/validation_heal/bucket_metadata.py"
+ ],
+ "bucket-policy-tags": [
+ "scripts/validation_heal/bucket_metadata.py"
+ ],
+ "lock-config": [
+ "scripts/validation_heal/bucket_metadata.py"
+ ],
+ "unknown-record-negative": [
+ "scripts/validation_heal/bucket_metadata.py"
+ ]
+ },
+ "gaps": [],
+ "live": {
+ "state": "retained-failure",
+ "finding": "Retained failure or incomplete live evidence. Product behavior, environment and controller responsibility must be attributed separately; a FAIL alone does not demonstrate a missing test implementation.",
+ "retained_runs": [
+ 38096389824
+ ],
+ "original_result_sha256": "92eb6b1c9dcfbfc5c2a6f8e7b896fed244e1d7dbd8b2cc510ac61cdd83bd2f31"
+ }
+ },
+ {
+ "case_id": "heal.manual-normal-attribution",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38207956101
+ ]
+ }
+ },
+ {
+ "case_id": "heal.manual-deep-attribution",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38208251180
+ ]
+ }
+ },
+ {
+ "case_id": "heal.read-repair-attribution",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38237270640
+ ]
+ }
+ },
+ {
+ "case_id": "heal.scanner-cold-object-repair",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 39863505276
+ ]
+ }
+ },
+ {
+ "case_id": "heal.mrf-offline-write-replay",
+ "implementation": "implemented",
+ "basis": "Source branches have execution paths and strict original oracles in the referenced controller/helpers. Contract tests cover those paths; this source review is not a claim that every live branch passed or that an unattributed failure is a product defect.",
+ "references": [
+ "scripts/validation_heal/mrf_replay.py",
+ "scripts/validation_heal/mrf_records.py",
+ "test_validation_heal_mrf.py"
+ ],
+ "source_variants": {
+ "offline-put": [
+ "scripts/validation_heal/mrf_replay.py"
+ ],
+ "offline-version-delete": [
+ "scripts/validation_heal/mrf_replay.py"
+ ],
+ "offline-marker": [
+ "scripts/validation_heal/mrf_replay.py"
+ ]
+ },
+ "gaps": [],
+ "live": {
+ "state": "retained-failure",
+ "finding": "Retained failure or incomplete live evidence. Product behavior, environment and controller responsibility must be attributed separately; a FAIL alone does not demonstrate a missing test implementation.",
+ "retained_runs": [
+ 38104630791
+ ],
+ "original_result_sha256": "cda6c4dd785e025c16b1ed7b2b62df6168a31b8528e9f7f0fc89495d70818fed"
+ }
+ },
+ {
+ "case_id": "heal.replacement-auto-rebuild",
+ "implementation": "implemented",
+ "basis": "Source branches have execution paths and strict original oracles in the referenced controller/helpers. Contract tests cover those paths; this source review is not a claim that every live branch passed or that an unattributed failure is a product defect.",
+ "references": [
+ "scripts/validation_heal/versions.py",
+ "scripts/validation_heal/replacement.py",
+ "test_validation_heal.py"
+ ],
+ "source_variants": {
+ "replacement-unversioned": [
+ "scripts/validation_heal/versions.py"
+ ],
+ "replacement-versioned-marker": [
+ "scripts/validation_heal/versions.py"
+ ]
+ },
+ "gaps": [],
+ "live": {
+ "state": "retained-failure",
+ "finding": "Retained failure or incomplete live evidence. Product behavior, environment and controller responsibility must be attributed separately; a FAIL alone does not demonstrate a missing test implementation.",
+ "retained_runs": [
+ 38085937937
+ ],
+ "original_result_sha256": "4836929d6e21e88da89dcc6ae645ce3722554991d1fc01e53a9fbd0b03c6c167"
+ }
+ },
+ {
+ "case_id": "heal.root-graceful-coordinator-restart",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38096950043
+ ]
+ }
+ },
+ {
+ "case_id": "heal.target-graceful-restart",
+ "implementation": "implemented",
+ "basis": "Source branches have execution paths and strict original oracles in the referenced controller/helpers. Contract tests cover those paths; this source review is not a claim that every live branch passed or that an unattributed failure is a product defect.",
+ "references": [
+ "scripts/validation_heal/versions.py",
+ "scripts/validation_heal/replacement.py",
+ "test_validation_heal.py"
+ ],
+ "source_variants": {
+ "target-partial-graceful": [
+ "scripts/validation_heal/versions.py"
+ ],
+ "versioned-target-partial": [
+ "scripts/validation_heal/versions.py"
+ ]
+ },
+ "gaps": [],
+ "live": {
+ "state": "retained-failure",
+ "finding": "Retained failure or incomplete live evidence. Product behavior, environment and controller responsibility must be attributed separately; a FAIL alone does not demonstrate a missing test implementation.",
+ "retained_runs": [
+ 38088240693
+ ],
+ "original_result_sha256": "609a60b2de601fcb3a9bc703f5d0cce7bddfefc08b5c951fe3830a29698975d4"
+ }
+ },
+ {
+ "case_id": "heal.target-process-crash",
+ "implementation": "implemented",
+ "basis": "Source branches have execution paths and strict original oracles in the referenced controller/helpers. Contract tests cover those paths; this source review is not a claim that every live branch passed or that an unattributed failure is a product defect.",
+ "references": [
+ "scripts/validation_heal/versions.py",
+ "scripts/validation_heal/replacement.py",
+ "test_validation_heal.py"
+ ],
+ "source_variants": {
+ "target-partial-sigkill": [
+ "scripts/validation_heal/versions.py"
+ ],
+ "target-partial-sigkill-versioned": [
+ "scripts/validation_heal/versions.py"
+ ]
+ },
+ "gaps": [],
+ "live": {
+ "state": "retained-failure",
+ "finding": "Retained failure or incomplete live evidence. Product behavior, environment and controller responsibility must be attributed separately; a FAIL alone does not demonstrate a missing test implementation.",
+ "retained_runs": [
+ 38088777413
+ ],
+ "original_result_sha256": "404c026c77dc6b4eb87c132a316b700eea620659b8ff44c98648d0e30c5605c9"
+ }
+ },
+ {
+ "case_id": "heal.coordinator-process-crash",
+ "implementation": "implemented",
+ "basis": "Source branches have execution paths and strict original oracles in the referenced controller/helpers. Contract tests cover those paths; this source review is not a claim that every live branch passed or that an unattributed failure is a product defect.",
+ "references": [
+ "scripts/validation_heal/coordinator_crash.py",
+ "scripts/validation_heal/replacement.py",
+ "test_validation_heal_coordinator_crash.py"
+ ],
+ "source_variants": {
+ "root-coordinator-kill": [
+ "scripts/validation_heal/coordinator_crash.py"
+ ],
+ "mrf-coordinator-kill": [
+ "scripts/validation_heal/coordinator_crash.py"
+ ],
+ "replacement-coordinator-kill": [
+ "scripts/validation_heal/coordinator_crash.py"
+ ]
+ },
+ "gaps": [],
+ "live": {
+ "state": "retained-failure",
+ "finding": "Retained failure or incomplete live evidence. Product behavior, environment and controller responsibility must be attributed separately; a FAIL alone does not demonstrate a missing test implementation.",
+ "retained_runs": [
+ 38105011828
+ ],
+ "original_result_sha256": "8100552121f4b50a621ebaa3012df968ec45b2e74038f49d53f34f085eed1fca"
+ }
+ },
+ {
+ "case_id": "heal.full-cluster-restart",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38227684435
+ ]
+ }
+ },
+ {
+ "case_id": "heal.cancelled-task-restart",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38102031029
+ ]
+ }
+ },
+ {
+ "case_id": "heal.checkpoint-and-mrf-corruption",
+ "implementation": "implemented",
+ "basis": "The five source live corruption/write-failure/rescan branches retain their original byte, responsibility and recovery oracles. The native-persist-fence route now executes and independently verifies six ECStore directory-fsync regressions and composes them with the ten existing manager publication/restart regressions, with manifest-before-execution and proof-after-verification checks plus a same-candidate composition guard. Manager successful reload ordering, actual commit interruptions and ECStore CAS persistence failures retain their separate scopes; no exhaustive file-fsync or physical power-loss coverage is claimed. Missing immutable artifacts limit execution, independently of implementation completion.",
+ "references": [
+ "scripts/validation_heal/persistence.py",
+ "scripts/validation_heal/persistence_mrf.py",
+ "scripts/validation_heal/journal_fault.py",
+ "scripts/validation_heal/manager_native.py",
+ "test_validation_heal_persistence.py",
+ "test_validation_heal_persistence_mrf.py",
+ "test_validation_heal_journal_fault.py",
+ "test_validation_heal_manager_native.py",
+ "scripts/validation_heal/persistence_native.py",
+ "scripts/validation_heal/manager_fixture.py",
+ "test_validation_heal_native.py"
+ ],
+ "source_variants": {
+ "checkpoint-truncate": [
+ "scripts/validation_heal/persistence.py"
+ ],
+ "mrf-checksum": [
+ "scripts/validation_heal/persistence.py"
+ ],
+ "future-schema": [
+ "scripts/validation_heal/persistence.py"
+ ],
+ "stale-generation": [
+ "scripts/validation_heal/persistence.py"
+ ],
+ "journal-write-failure": [
+ "scripts/validation_heal/persistence.py"
+ ],
+ "native-persist-fence": [
+ "scripts/validation_heal/persistence.py",
+ "scripts/validation_heal/manager_native.py",
+ "scripts/validation_heal/manager_fixture.py",
+ "scripts/validation_heal/persistence_native.py"
+ ]
+ },
+ "gaps": [],
+ "live": {
+ "state": "blocked",
+ "finding": "Retained failure or incomplete live evidence. Product behavior, environment and controller responsibility must be attributed separately; a FAIL alone does not demonstrate a missing test implementation."
+ }
+ },
+ {
+ "case_id": "heal.race-unversioned-overwrite",
+ "implementation": "implemented",
+ "basis": "Source branches have execution paths and strict original oracles in the referenced controller/helpers. Contract tests cover those paths; this source review is not a claim that every live branch passed or that an unattributed failure is a product defect.",
+ "references": [
+ "scripts/validation_heal/race.py",
+ "scripts/validation_heal/race_native_evidence.py",
+ "scripts/validation_heal/race_io.py",
+ "test_validation_heal_race.py",
+ "test_validation_heal_race_native.py"
+ ],
+ "source_variants": {
+ "overwrite-vs-deep": [
+ "scripts/validation_heal/race.py"
+ ],
+ "overwrite-vs-read-repair": [
+ "scripts/validation_heal/race.py"
+ ],
+ "native-commit-cleanup": [
+ "scripts/validation_heal/race.py"
+ ]
+ },
+ "gaps": [],
+ "live": {
+ "state": "retained-failure",
+ "finding": "Observed candidate PUT failure during overwrite-vs-deep. The Recorder retained ACK 000003 and refused to retry the ambiguous mutation. Recovery 40440199880 isolated the scene. Full live acceptance remains incomplete; this is not evidence of a missing overwrite test.",
+ "retained_runs": [
+ 40439620403
+ ]
+ }
+ },
+ {
+ "case_id": "heal.race-versioned-put",
+ "implementation": "implemented",
+ "basis": "Source branches have execution paths and strict original oracles in the referenced controller/helpers. Contract tests cover those paths; this source review is not a claim that every live branch passed or that an unattributed failure is a product defect.",
+ "references": [
+ "scripts/validation_heal/race.py",
+ "scripts/validation_heal/version_race_runner.py",
+ "scripts/validation_heal/race_native_evidence.py",
+ "test_validation_heal_g02_source.py",
+ "test_validation_heal_race_native.py"
+ ],
+ "source_variants": {
+ "new-version-vs-historical-heal": [
+ "scripts/validation_heal/race.py"
+ ],
+ "read-all-versions-during-heal": [
+ "scripts/validation_heal/race.py"
+ ],
+ "native-version-fence": [
+ "scripts/validation_heal/race.py"
+ ]
+ },
+ "gaps": [],
+ "live": {
+ "state": "retained-failure",
+ "finding": "Observed HTTP 503 ServiceUnavailable on the third versioned PUT, with server storage_error_code=Lock. The request is durably recorded with zero retries. Recovery 40444795627 isolated the scene. Exact root cause is not inferred from the status code alone.",
+ "retained_runs": [
+ 40444207551
+ ]
+ }
+ },
+ {
+ "case_id": "heal.race-version-delete",
+ "implementation": "implemented",
+ "basis": "Source branches have execution paths and strict original oracles in the referenced controller/helpers. Contract tests cover those paths; this source review is not a claim that every live branch passed or that an unattributed failure is a product defect.",
+ "references": [
+ "scripts/validation_heal/race.py",
+ "scripts/validation_heal/version_race_runner.py",
+ "scripts/validation_heal/race_native_evidence.py",
+ "test_validation_heal_g03_source.py",
+ "test_validation_heal_race_native.py"
+ ],
+ "source_variants": {
+ "delete-old-version-vs-deep": [
+ "scripts/validation_heal/race.py"
+ ],
+ "delete-current-version-vs-deep": [
+ "scripts/validation_heal/race.py"
+ ],
+ "native-delete-fence": [
+ "scripts/validation_heal/race.py"
+ ]
+ },
+ "gaps": [],
+ "live": {
+ "state": "pending",
+ "finding": "Retained failure or incomplete live evidence. Product behavior, environment and controller responsibility must be attributed separately; a FAIL alone does not demonstrate a missing test implementation."
+ }
+ },
+ {
+ "case_id": "heal.race-markers-and-batch-delete",
+ "implementation": "implemented",
+ "basis": "Source branches have execution paths and strict original oracles in the referenced controller/helpers. Contract tests cover those paths; this source review is not a claim that every live branch passed or that an unattributed failure is a product defect.",
+ "references": [
+ "scripts/validation_heal/race.py",
+ "scripts/validation_heal/race_native_evidence.py",
+ "scripts/validation_heal/race_io.py",
+ "test_validation_heal_race.py",
+ "test_validation_heal_race_native.py"
+ ],
+ "source_variants": {
+ "create-marker": [
+ "scripts/validation_heal/race.py"
+ ],
+ "delete-marker": [
+ "scripts/validation_heal/race.py"
+ ],
+ "batch-version-delete": [
+ "scripts/validation_heal/race.py"
+ ],
+ "unversioned-delete": [
+ "scripts/validation_heal/race.py"
+ ],
+ "native-marker-fence": [
+ "scripts/validation_heal/race.py"
+ ]
+ },
+ "gaps": [],
+ "live": {
+ "state": "pending",
+ "finding": "Retained failure or incomplete live evidence. Product behavior, environment and controller responsibility must be attributed separately; a FAIL alone does not demonstrate a missing test implementation."
+ }
+ },
+ {
+ "case_id": "heal.race-multipart-complete-abort",
+ "implementation": "implemented",
+ "basis": "Source branches have execution paths and strict original oracles in the referenced controller/helpers. Contract tests cover those paths; this source review is not a claim that every live branch passed or that an unattributed failure is a product defect.",
+ "references": [
+ "scripts/validation_heal/race.py",
+ "scripts/validation_heal/mpu_race_runner.py",
+ "scripts/validation_heal/race_native_evidence.py",
+ "test_validation_heal_mpu_race_runner.py",
+ "test_validation_heal_race_native.py"
+ ],
+ "source_variants": {
+ "upload-continue": [
+ "scripts/validation_heal/race.py"
+ ],
+ "complete": [
+ "scripts/validation_heal/race.py"
+ ],
+ "abort": [
+ "scripts/validation_heal/race.py"
+ ],
+ "native-mpu-fence": [
+ "scripts/validation_heal/race.py"
+ ]
+ },
+ "gaps": [],
+ "live": {
+ "state": "pending",
+ "finding": "Retained failure or incomplete live evidence. Product behavior, environment and controller responsibility must be attributed separately; a FAIL alone does not demonstrate a missing test implementation."
+ }
+ },
+ {
+ "case_id": "heal.race-copy-and-metadata-update",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 40427815826
+ ]
+ }
+ },
+ {
+ "case_id": "heal.race-ilm-expiry-and-transition",
+ "implementation": "environment-gated",
+ "basis": "Source procedure, controller contract, original assertions and capability refusal are implemented. This delivery keeps the case disabled by default; a supplied candidate-bound resource adapter is still needed for live execution.",
+ "references": [
+ "scripts/validation_heal/environment_gate.py",
+ "scripts/validation_heal/environment.py",
+ "scripts/validation_heal/recipes.json",
+ "test_validation_heal_environment.py",
+ "test_validation_heal_environment_gate.py"
+ ],
+ "required_capabilities": [
+ "dedicated_ilm_tier",
+ "real_expire_transition_restore_window",
+ "remote_version_mapping",
+ "isolated_rule_cleanup"
+ ],
+ "reason_code": "ENV_DEDICATED_ILM_TIER_WINDOW",
+ "gaps": [],
+ "live": {
+ "state": "disabled",
+ "finding": "需要隔离 ILM/tier 租户、真实 Expire/Transition/Restore 窗口和远端版本映射;共享 ILM 环境不能安全制造交叠窗口。"
+ }
+ },
+ {
+ "case_id": "heal.race-replication-resync",
+ "implementation": "environment-gated",
+ "basis": "Source procedure, controller contract, original assertions and capability refusal are implemented. This delivery keeps the case disabled by default; a supplied candidate-bound resource adapter is still needed for live execution.",
+ "references": [
+ "scripts/validation_heal/environment_gate.py",
+ "scripts/validation_heal/environment.py",
+ "scripts/validation_heal/recipes.json",
+ "test_validation_heal_environment.py",
+ "test_validation_heal_environment_gate.py"
+ ],
+ "required_capabilities": [
+ "isolated_replication_pair",
+ "resync_control",
+ "cross_endpoint_version_mapping",
+ "replication_overlap_window"
+ ],
+ "reason_code": "ENV_ISOLATED_REPLICATION_PAIR",
+ "gaps": [],
+ "live": {
+ "state": "disabled",
+ "finding": "需要隔离的复制两端、resync 控制、跨端版本映射和 Heal/resync 实际交叠窗口;当前固定 fleet 没有独占复制对。"
+ }
+ },
+ {
+ "case_id": "heal.slow-io-and-eio",
+ "implementation": "environment-gated",
+ "basis": "Source procedure, controller contract, original assertions and capability refusal are implemented. This delivery keeps the case disabled by default; a supplied candidate-bound resource adapter is still needed for live execution.",
+ "references": [
+ "scripts/validation_heal/environment_gate.py",
+ "scripts/validation_heal/environment.py",
+ "scripts/validation_heal/recipes.json",
+ "test_validation_heal_environment.py",
+ "test_validation_heal_environment_gate.py"
+ ],
+ "required_capabilities": [
+ "dedicated_faultfs_or_device",
+ "slow_io_short_read_eio",
+ "namespace_isolation",
+ "fault_layer_restore"
+ ],
+ "reason_code": "ENV_DEDICATED_FAULT_IO",
+ "gaps": [],
+ "live": {
+ "state": "disabled",
+ "finding": "需要专用 faultfs 或故障设备、可恢复的 slow/short-read/EIO 注入和 namespace 隔离;共享盘模拟会污染其他运行。"
+ }
+ },
+ {
+ "case_id": "heal.enospc-inodes-and-quota",
+ "implementation": "implemented",
+ "basis": "Source branches have execution paths and strict original oracles in the referenced controller/helpers. Contract tests cover those paths; this source review is not a claim that every live branch passed or that an unattributed failure is a product defect.",
+ "references": [
+ "scripts/validation_heal/capacity.py",
+ "scripts/validation_heal/bounded_volume.py",
+ "test_validation_heal_capacity.py"
+ ],
+ "source_variants": {
+ "bounded-volume-enospc": [
+ "scripts/validation_heal/capacity.py"
+ ],
+ "bounded-inode-exhaustion": [
+ "scripts/validation_heal/capacity.py"
+ ],
+ "bucket-quota": [
+ "scripts/validation_heal/capacity.py"
+ ]
+ },
+ "gaps": [],
+ "live": {
+ "state": "retained-failure",
+ "finding": "Retained failure or incomplete live evidence. Product behavior, environment and controller responsibility must be attributed separately; a FAIL alone does not demonstrate a missing test implementation.",
+ "retained_runs": [
+ 38106408335
+ ],
+ "original_result_sha256": "f779fa1f4a24e2bc4ebbc3b07b447a2134e0fca4a44faedd5be42b461c7520df"
+ }
+ },
+ {
+ "case_id": "heal.network-partition-and-lock-quorum",
+ "implementation": "implemented",
+ "basis": "Source branches have execution paths and strict original oracles in the referenced controller/helpers. Contract tests cover those paths; this source review is not a claim that every live branch passed or that an unattributed failure is a product defect.",
+ "references": [
+ "scripts/validation_heal/network.py",
+ "scripts/validation_heal/network_gate.py",
+ "scripts/validation_heal/network_load.py",
+ "test_validation_heal_network.py"
+ ],
+ "source_variants": {
+ "minority-partition": [
+ "scripts/validation_heal/network.py"
+ ],
+ "read-boundary": [
+ "scripts/validation_heal/network.py"
+ ],
+ "write-boundary": [
+ "scripts/validation_heal/network.py"
+ ],
+ "lock-boundary": [
+ "scripts/validation_heal/network.py"
+ ],
+ "rejoin": [
+ "scripts/validation_heal/network.py"
+ ]
+ },
+ "gaps": [],
+ "live": {
+ "state": "retained-failure",
+ "finding": "Retained failure or incomplete live evidence. Product behavior, environment and controller responsibility must be attributed separately; a FAIL alone does not demonstrate a missing test implementation.",
+ "retained_runs": [
+ 38117058670
+ ],
+ "original_result_sha256": "aac97fc7d2b328c628f9f42f2a6b426990ed20442015e1801ade7a18c3c5a61a"
+ }
+ },
+ {
+ "case_id": "heal.queue-pressure-and-admission",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 39354651483
+ ]
+ }
+ },
+ {
+ "case_id": "heal.lock-timeout-and-caller-abort",
+ "implementation": "implemented",
+ "basis": "The source-plan entry serially executes real timeout and queued-successor controllers on H1/H2 with distinct namespaces, original request/ACK evidence chains, peer version/content and physical integrity checks, exact server lock identities and bounded FD finalization. Before deployment it composes one immutable successful artifact's I05 owner/unlink/stale, G02 explicit-version append and G03 explicit-version delete native subsets. The latter two provide real final-rename reconstruction barriers, foreground ACK/physical tail drain, exact typed outcomes and complete surviving body/version/dataDir/shard authority in 4/2 and 16/4. Coordinator clock-domain ordering binds actual TCP disconnect independently from native ownership proof; local guard release never claims distributed release. First failure retains the original scene and stops subsequent instances. Full independent source-plan live acceptance remains pending; product PASS is not an implementation prerequisite.",
+ "references": [
+ "scripts/validation_heal/lock_timeout_runner.py",
+ "scripts/validation_heal/lock_timeout.py",
+ "scripts/validation_heal/caller_abort.py",
+ "scripts/validation_heal/lock_observer.py",
+ "scripts/validation_heal/lock_successor.py",
+ "scripts/validation_heal/native_regressions.py",
+ "test_validation_heal_lock_timeout_runner.py",
+ "test_validation_heal_lock_timeout.py",
+ "test_validation_heal_caller_abort.py",
+ "test_validation_heal_lock_observer.py",
+ "test_validation_heal_lock_successor.py",
+ "scripts/validation_heal/race_native.py",
+ "scripts/validation_heal/race_native_evidence.py",
+ "scripts/validation_heal/race_native_contracts.json",
+ "test_validation_heal_race_native.py",
+ "scripts/validation_heal/lock_source.py",
+ "test_validation_heal_lock_source.py"
+ ],
+ "source_variants": {
+ "object-lock-timeout": [
+ "scripts/validation_heal/lock_source.py",
+ "scripts/validation_heal/lock_timeout_runner.py",
+ "scripts/validation_heal/lock_timeout.py"
+ ],
+ "caller-disconnect": [
+ "scripts/validation_heal/lock_source.py",
+ "scripts/validation_heal/lock_timeout_runner.py",
+ "scripts/validation_heal/lock_successor.py",
+ "scripts/validation_heal/lock_observer.py"
+ ],
+ "stale-read-repair": [
+ "scripts/validation_heal/lock_source.py",
+ "scripts/validation_heal/race_native.py",
+ "scripts/validation_heal/race_native_evidence.py",
+ "scripts/validation_heal/race_native_contracts.json"
+ ],
+ "native-lock-owner-fence": [
+ "scripts/validation_heal/lock_source.py",
+ "scripts/validation_heal/race_native_contracts.json"
+ ]
+ },
+ "gaps": [],
+ "live": {
+ "state": "blocked",
+ "finding": "Retained failure or incomplete live evidence. Product behavior, environment and controller responsibility must be attributed separately; a FAIL alone does not demonstrate a missing test implementation."
+ }
+ },
+ {
+ "case_id": "heal.kms-and-tier-outage",
+ "implementation": "environment-gated",
+ "basis": "Source procedure, controller contract, original assertions and capability refusal are implemented. This delivery keeps the case disabled by default; a supplied candidate-bound resource adapter is still needed for live execution.",
+ "references": [
+ "scripts/validation_heal/environment_gate.py",
+ "scripts/validation_heal/environment.py",
+ "scripts/validation_heal/recipes.json",
+ "test_validation_heal_environment.py",
+ "test_validation_heal_environment_gate.py"
+ ],
+ "required_capabilities": [
+ "dedicated_kms_tier_tenant",
+ "dependency_network_isolation",
+ "key_redaction",
+ "tier_version_mapping"
+ ],
+ "reason_code": "ENV_DEDICATED_KMS_TIER_TENANT",
+ "gaps": [],
+ "live": {
+ "state": "disabled",
+ "finding": "需要专用 KMS/tier 租户、依赖网络隔离和安全的 key/tier 版本证据;不能在共享依赖上全局断网。"
+ }
+ },
+ {
+ "case_id": "heal.status-partial-disabled-unknown",
+ "implementation": "implemented",
+ "basis": "Source branches have execution paths and strict original oracles in the referenced controller/helpers. Contract tests cover those paths; this source review is not a claim that every live branch passed or that an unattributed failure is a product defect.",
+ "references": [
+ "scripts/validation_heal/status.py",
+ "test_validation_heal.py"
+ ],
+ "source_variants": {
+ "enabled-idle": [
+ "scripts/validation_heal/status.py"
+ ],
+ "disabled": [
+ "scripts/validation_heal/status.py"
+ ],
+ "uninitialized": [
+ "scripts/validation_heal/status.py"
+ ],
+ "missing-peer": [
+ "scripts/validation_heal/status.py"
+ ],
+ "recovered-complete": [
+ "scripts/validation_heal/status.py"
+ ]
+ },
+ "gaps": [],
+ "live": {
+ "state": "retained-failure",
+ "finding": "The startup listener returns 503 until runtime initialization, then status reports idle. The required externally visible 200/uninitialized source window was not observed. Preserve the original oracle and NOT_OBSERVED branch; implementation completion does not certify product compatibility.",
+ "retained_runs": [
+ 38209400619,
+ 39929436131
+ ],
+ "original_result_sha256": "817e04d80bf59d0ce369573b2f6500eb2ff0cc47662c830e3bb524e073d628fa"
+ }
+ },
+ {
+ "case_id": "heal.outcome-counters-and-error-truth",
+ "implementation": "implemented",
+ "basis": "Source branches have execution paths and strict original oracles in the referenced controller/helpers. Contract tests cover those paths; this source review is not a claim that every live branch passed or that an unattributed failure is a product defect. All source branches are present in the explicit diagnostic route; canonical admission remains independently blocked.",
+ "references": [
+ "scripts/validation_heal/outcome.py",
+ "scripts/validation_heal/outcome_grace.py",
+ "scripts/validation_heal/outcome_recovery.py",
+ "test_validation_heal_outcome.py",
+ "test_validation_heal_outcome_grace.py",
+ "test_validation_heal_outcome_recovery.py"
+ ],
+ "source_variants": {
+ "mixed-healthy-repaired-failed": [
+ "scripts/validation_heal/outcome.py"
+ ],
+ "dry-run": [
+ "scripts/validation_heal/outcome.py"
+ ],
+ "deferred": [
+ "scripts/validation_heal/outcome.py"
+ ],
+ "readonly-terminal-failure": [
+ "scripts/validation_heal/outcome.py"
+ ],
+ "unknown-negative": [
+ "scripts/validation_heal/outcome.py"
+ ]
+ },
+ "gaps": [],
+ "live": {
+ "state": "blocked",
+ "finding": "Retained failure or incomplete live evidence. Product behavior, environment and controller responsibility must be attributed separately; a FAIL alone does not demonstrate a missing test implementation."
+ }
+ },
+ {
+ "case_id": "heal.incremental-cursor-and-truncation",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38089238767
+ ]
+ }
+ },
+ {
+ "case_id": "heal.repeat-idempotency",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38059567206
+ ]
+ }
+ },
+ {
+ "case_id": "heal.performance-abba",
+ "implementation": "implemented",
+ "basis": "Source branches have execution paths and strict original oracles in the referenced controller/helpers. Contract tests cover those paths; this source review is not a claim that every live branch passed or that an unattributed failure is a product defect.",
+ "references": [
+ "scripts/validation_heal/performance.py",
+ "test_validation_heal_performance.py"
+ ],
+ "source_variants": {
+ "abba-repeat-1": [
+ "scripts/validation_heal/performance.py"
+ ],
+ "abba-repeat-2": [
+ "scripts/validation_heal/performance.py"
+ ],
+ "abba-repeat-3": [
+ "scripts/validation_heal/performance.py"
+ ]
+ },
+ "gaps": [],
+ "live": {
+ "state": "retained-failure",
+ "finding": "Retained failure or incomplete live evidence. Product behavior, environment and controller responsibility must be attributed separately; a FAIL alone does not demonstrate a missing test implementation.",
+ "retained_runs": [
+ 38195131282
+ ],
+ "original_result_sha256": "f3c6ec359b4ac5e3d8dc3776ddd75c760303a672c7ffed1385f482d3c79c3ac8"
+ }
+ },
+ {
+ "case_id": "scanner.usage-convergence",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38042200106
+ ]
+ }
+ },
+ {
+ "case_id": "heal.mixed-version-upgrade-rollback",
+ "implementation": "environment-gated",
+ "basis": "Source procedure, controller contract, original assertions and capability refusal are implemented. This delivery keeps the case disabled by default; a supplied candidate-bound resource adapter is still needed for live execution.",
+ "references": [
+ "scripts/validation_heal/environment_gate.py",
+ "scripts/validation_heal/environment.py",
+ "scripts/validation_heal/recipes.json",
+ "test_validation_heal_environment.py",
+ "test_validation_heal_environment_gate.py"
+ ],
+ "required_capabilities": [
+ "immutable_release_pair",
+ "mixed_version_peer_status",
+ "rolling_upgrade_rollback",
+ "rollback_compatibility_matrix"
+ ],
+ "reason_code": "ENV_IMMUTABLE_RELEASE_PAIR",
+ "gaps": [],
+ "live": {
+ "state": "disabled",
+ "finding": "需要两个已固定且相互兼容的 RustFS release、混合版本 peer 状态和经过审核的回滚兼容矩阵;当前 auto-testing Heal 节点没有这组不可变双版本资源。"
+ }
+ },
+ {
+ "case_id": "heal.dedicated-host-forced-restart",
+ "implementation": "environment-gated",
+ "basis": "Source procedure, controller contract, original assertions and capability refusal are implemented. This delivery keeps the case disabled by default; a supplied candidate-bound resource adapter is still needed for live execution.",
+ "references": [
+ "scripts/validation_heal/environment_gate.py",
+ "scripts/validation_heal/environment.py",
+ "scripts/validation_heal/recipes.json",
+ "test_validation_heal_environment.py",
+ "test_validation_heal_environment_gate.py"
+ ],
+ "required_capabilities": [
+ "dedicated_h6_host",
+ "platform_power_cycle",
+ "out_of_band_recovery",
+ "boot_id_evidence"
+ ],
+ "reason_code": "ENV_DEDICATED_H6_HOST",
+ "gaps": [],
+ "live": {
+ "state": "disabled",
+ "finding": "需要 H6 专用主机、平台强制停启权限、带外恢复和 boot_id/平台事件证据;共享 Heal 节点不能证明真实电源循环。"
+ }
+ },
+ {
+ "case_id": "heal.with-expansion-rebalance",
+ "implementation": "environment-gated",
+ "basis": "Source procedure, controller contract, original assertions and capability refusal are implemented. This delivery keeps the case disabled by default; a supplied candidate-bound resource adapter is still needed for live execution.",
+ "references": [
+ "scripts/validation_heal/environment_gate.py",
+ "scripts/validation_heal/environment.py",
+ "scripts/validation_heal/recipes.json",
+ "test_validation_heal_environment.py",
+ "test_validation_heal_environment_gate.py"
+ ],
+ "required_capabilities": [
+ "h5_pool_expansion",
+ "rebalance_task",
+ "per_object_pool_set_mapping",
+ "retired_scanner_evidence"
+ ],
+ "reason_code": "ENV_H5_EXPANSION_REBALANCE",
+ "gaps": [],
+ "live": {
+ "state": "disabled",
+ "finding": "需要 H5 pool 扩容、rebalance 和 Heal 同时活动的完整拓扑,并逐对象保存 pool/set 迁移证据;当前 auto-testing 未绑定扩容前后布局与专用操作窗口。"
+ }
+ },
+ {
+ "case_id": "heal.with-rebalance-stop-resume",
+ "implementation": "environment-gated",
+ "basis": "Source procedure, controller contract, original assertions and capability refusal are implemented. This delivery keeps the case disabled by default; a supplied candidate-bound resource adapter is still needed for live execution.",
+ "references": [
+ "scripts/validation_heal/environment_gate.py",
+ "scripts/validation_heal/environment.py",
+ "scripts/validation_heal/recipes.json",
+ "test_validation_heal_environment.py",
+ "test_validation_heal_environment_gate.py"
+ ],
+ "required_capabilities": [
+ "h5_rebalance",
+ "rebalance_stop_resume",
+ "independent_task_identity",
+ "service_restart_control"
+ ],
+ "reason_code": "ENV_H5_REBALANCE_LIFECYCLE",
+ "gaps": [],
+ "live": {
+ "state": "disabled",
+ "finding": "需要可观测的 H5 rebalance、停止/恢复/服务重启控制及独立 task 身份;当前 auto-testing 未绑定这两类任务的独立身份和操作窗口。"
+ }
+ },
+ {
+ "case_id": "heal.with-decommission",
+ "implementation": "environment-gated",
+ "basis": "Source procedure, controller contract, original assertions and capability refusal are implemented. This delivery keeps the case disabled by default; a supplied candidate-bound resource adapter is still needed for live execution.",
+ "references": [
+ "scripts/validation_heal/environment_gate.py",
+ "scripts/validation_heal/environment.py",
+ "scripts/validation_heal/recipes.json",
+ "test_validation_heal_environment.py",
+ "test_validation_heal_environment_gate.py"
+ ],
+ "required_capabilities": [
+ "h5_decommission",
+ "survivor_pool",
+ "decommission_write_guard",
+ "movement_ack_physical_proof"
+ ],
+ "reason_code": "ENV_H5_DECOMMISSION",
+ "gaps": [],
+ "live": {
+ "state": "disabled",
+ "finding": "需要 H5 pool decommission、survivor 目标、写入限制和移动 ACK/物理权威证据;当前 auto-testing 未绑定本例退役前后拓扑和恢复方案。"
+ }
+ },
+ {
+ "case_id": "heal.with-decommission-cancel-clear",
+ "implementation": "environment-gated",
+ "basis": "Source procedure, controller contract, original assertions and capability refusal are implemented. This delivery keeps the case disabled by default; a supplied candidate-bound resource adapter is still needed for live execution.",
+ "references": [
+ "scripts/validation_heal/environment_gate.py",
+ "scripts/validation_heal/environment.py",
+ "scripts/validation_heal/recipes.json",
+ "test_validation_heal_environment.py",
+ "test_validation_heal_environment_gate.py"
+ ],
+ "required_capabilities": [
+ "h5_decommission",
+ "cancel_clear_resume",
+ "capacity_lock_retry_window",
+ "generation_bound_ack_ledger"
+ ],
+ "reason_code": "ENV_H5_DECOMMISSION_CANCEL",
+ "gaps": [],
+ "live": {
+ "state": "disabled",
+ "finding": "需要退役取消/清理/新代次恢复以及 #2423 所需容量锁重试窗口;普通取消结果不能替代这个临界点。"
+ }
+ },
+ {
+ "case_id": "heal.after-retired-pool-removal",
+ "implementation": "environment-gated",
+ "basis": "Source procedure, controller contract, original assertions and capability refusal are implemented. This delivery keeps the case disabled by default; a supplied candidate-bound resource adapter is still needed for live execution.",
+ "references": [
+ "scripts/validation_heal/environment_gate.py",
+ "scripts/validation_heal/environment.py",
+ "scripts/validation_heal/recipes.json",
+ "test_validation_heal_environment.py",
+ "test_validation_heal_environment_gate.py"
+ ],
+ "required_capabilities": [
+ "h5_retired_pool_removal",
+ "source_offline_proof",
+ "survivor_authority",
+ "retired_pool_write_guard"
+ ],
+ "reason_code": "ENV_H5_RETIRED_POOL_REMOVAL",
+ "gaps": [],
+ "live": {
+ "state": "disabled",
+ "finding": "需要完整迁移后移除 retired pool、源保持离线和 survivor-only 访问;当前 auto-testing 未提供本例源持续离线和 survivor-only 的绑定证据。"
+ }
+ },
+ {
+ "case_id": "heal.multi-pool-target-isolation",
+ "implementation": "implemented",
+ "basis": "Source branches have execution paths and strict original oracles in the referenced controller/helpers. Contract tests cover those paths; this source review is not a claim that every live branch passed or that an unattributed failure is a product defect.",
+ "references": [
+ "scripts/validation_heal/multi_pool.py",
+ "scripts/validation_heal/multi_pool_layout.py",
+ "test_validation_heal_multi_pool.py"
+ ],
+ "source_variants": {
+ "selected-pool": [
+ "scripts/validation_heal/multi_pool.py"
+ ],
+ "selected-set": [
+ "scripts/validation_heal/multi_pool.py"
+ ],
+ "unselected-control-fault": [
+ "scripts/validation_heal/multi_pool.py"
+ ]
+ },
+ "gaps": [],
+ "live": {
+ "state": "retained-failure",
+ "finding": "Retained failure or incomplete live evidence. Product behavior, environment and controller responsibility must be attributed separately; a FAIL alone does not demonstrate a missing test implementation.",
+ "retained_runs": [
+ 38119207583
+ ],
+ "original_result_sha256": "e2c1a70ed32cf4db8815dfe3f397d82bde590489e8011fadf6d8ae6c2c90b967"
+ }
+ },
+ {
+ "case_id": "heal.ec84-multi-set-restart",
+ "implementation": "environment-gated",
+ "basis": "Source procedure, controller contract, original assertions and capability refusal are implemented. This delivery keeps the case disabled by default; a supplied candidate-bound resource adapter is still needed for live execution.",
+ "references": [
+ "scripts/validation_heal/environment_gate.py",
+ "scripts/validation_heal/environment.py",
+ "scripts/validation_heal/recipes.json",
+ "test_validation_heal_environment.py",
+ "test_validation_heal_environment_gate.py"
+ ],
+ "required_capabilities": [
+ "h3_ec84_12_independent_disks",
+ "h4_ec84_24_independent_endpoints",
+ "per_set_identity",
+ "per_set_restart_crash_evidence"
+ ],
+ "reason_code": "ENV_EC84_MULTI_SET_TOPOLOGY",
+ "gaps": [],
+ "live": {
+ "state": "disabled",
+ "finding": "需要 H3 的 12 个独立盘和 H4 的至少 24 个独立 endpoint,并按 set 分别记录重启/崩溃窗口;当前 H4 资源不可用。"
+ }
+ },
+ {
+ "case_id": "heal.long-soak",
+ "implementation": "environment-gated",
+ "basis": "Source procedure, controller contract, original assertions and capability refusal are implemented. This delivery keeps the case disabled by default; a supplied candidate-bound resource adapter is still needed for live execution.",
+ "references": [
+ "scripts/validation_heal/environment_gate.py",
+ "scripts/validation_heal/environment.py",
+ "scripts/validation_heal/recipes.json",
+ "test_validation_heal_environment.py",
+ "test_validation_heal_environment_gate.py"
+ ],
+ "required_capabilities": [
+ "dedicated_soak_lane",
+ "wall_clock_24h_72h",
+ "resource_budget_monitoring",
+ "scheduled_fault_recovery"
+ ],
+ "reason_code": "ENV_DEDICATED_LONG_SOAK_LANE",
+ "gaps": [],
+ "live": {
+ "state": "disabled",
+ "finding": "必须在独占 H2/H5 lane 真实运行 24 小时和 72 小时,包含限速、监控、到期回收和完整资源预算;短跑不能替代 soak 证据。"
+ }
+ },
+ {
+ "case_id": "scanner.status-readiness",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38044125099
+ ]
+ }
+ },
+ {
+ "case_id": "ilm.configuration-roundtrip",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38044956285
+ ]
+ }
+ },
+ {
+ "case_id": "ilm.invalid-configuration",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38045047518
+ ]
+ }
+ },
+ {
+ "case_id": "ilm.current-expiration",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38045154019
+ ]
+ }
+ },
+ {
+ "case_id": "ilm.tag-filter-expiration",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38045412209
+ ]
+ }
+ },
+ {
+ "case_id": "ilm.disabled-rule",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38045496042
+ ]
+ }
+ },
+ {
+ "case_id": "ilm.rule-removal",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38045714246
+ ]
+ }
+ },
+ {
+ "case_id": "ilm.versioned-current-expiration",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38051148291
+ ]
+ }
+ },
+ {
+ "case_id": "ilm.noncurrent-expiration",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38050040606
+ ]
+ }
+ },
+ {
+ "case_id": "ilm.newer-noncurrent-retention",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38049965929
+ ]
+ }
+ },
+ {
+ "case_id": "ilm.delete-all-versions",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38049740183
+ ]
+ }
+ },
+ {
+ "case_id": "ilm.delete-marker-expiration",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38049817172
+ ]
+ }
+ },
+ {
+ "case_id": "ilm.multipart-expiration",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38049890130
+ ]
+ }
+ },
+ {
+ "case_id": "ilm.transition-and-restore",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38050720293
+ ]
+ }
+ },
+ {
+ "case_id": "ilm.restart-persistence",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38050434607
+ ]
+ }
+ },
+ {
+ "case_id": "bucket-replication.source-versioning-required",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38049256196
+ ]
+ }
+ },
+ {
+ "case_id": "bucket-replication.target-versioning-required",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38107763760
+ ]
+ }
+ },
+ {
+ "case_id": "bucket-replication.configuration-roundtrip",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38107813352
+ ]
+ }
+ },
+ {
+ "case_id": "bucket-replication.put-object",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38107862949
+ ]
+ }
+ },
+ {
+ "case_id": "bucket-replication.sync-put-object",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38107913366
+ ]
+ }
+ },
+ {
+ "case_id": "bucket-replication.prefix-filter",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38107971953
+ ]
+ }
+ },
+ {
+ "case_id": "bucket-replication.tag-filter",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38108042411
+ ]
+ }
+ },
+ {
+ "case_id": "bucket-replication.zero-byte-special-key",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38108111726
+ ]
+ }
+ },
+ {
+ "case_id": "bucket-replication.multipart-object",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38109163901
+ ]
+ }
+ },
+ {
+ "case_id": "bucket-replication.copy-object",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38109217115
+ ]
+ }
+ },
+ {
+ "case_id": "bucket-replication.delete-marker",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38109278883
+ ]
+ }
+ },
+ {
+ "case_id": "bucket-replication.version-delete",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38109339732
+ ]
+ }
+ },
+ {
+ "case_id": "bucket-replication.existing-object-enabled",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38109405912
+ ]
+ }
+ },
+ {
+ "case_id": "bucket-replication.existing-object-disabled",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38109466432
+ ]
+ }
+ },
+ {
+ "case_id": "bucket-replication.target-check",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38109538351
+ ]
+ }
+ },
+ {
+ "case_id": "bucket-replication.status-observability",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38109591289
+ ]
+ }
+ },
+ {
+ "case_id": "bucket-replication.outage-recovery",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38109645139
+ ]
+ }
+ },
+ {
+ "case_id": "bucket-replication.source-restart-recovery",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38109704025
+ ]
+ }
+ },
+ {
+ "case_id": "site-replication.unconfigured-info",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38109764474
+ ]
+ }
+ },
+ {
+ "case_id": "site-replication.invalid-credentials",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38109818973
+ ]
+ }
+ },
+ {
+ "case_id": "site-replication.add-and-info",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38109868478
+ ]
+ }
+ },
+ {
+ "case_id": "site-replication.status-observability",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38109917609
+ ]
+ }
+ },
+ {
+ "case_id": "site-replication.bucket-create",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38109969984
+ ]
+ }
+ },
+ {
+ "case_id": "site-replication.object-bidirectional",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38110029711
+ ]
+ }
+ },
+ {
+ "case_id": "site-replication.versioning-metadata",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38110091819
+ ]
+ }
+ },
+ {
+ "case_id": "site-replication.lifecycle-expiry-toggle",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38110141316
+ ]
+ }
+ },
+ {
+ "case_id": "site-replication.bucket-policy-metadata",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38110223862
+ ]
+ }
+ },
+ {
+ "case_id": "site-replication.object-tags",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38110289545
+ ]
+ }
+ },
+ {
+ "case_id": "site-replication.delete-convergence",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38110361943
+ ]
+ }
+ },
+ {
+ "case_id": "site-replication.iam-user-policy",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38110422925
+ ]
+ }
+ },
+ {
+ "case_id": "site-replication.iam-group-policy",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38110479224
+ ]
+ }
+ },
+ {
+ "case_id": "site-replication.service-account",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38110530482
+ ]
+ }
+ },
+ {
+ "case_id": "site-replication.peer-edit",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38110586469
+ ]
+ }
+ },
+ {
+ "case_id": "site-replication.resync",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38110637512
+ ]
+ }
+ },
+ {
+ "case_id": "site-replication.outage-recovery",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38110690366
+ ]
+ }
+ },
+ {
+ "case_id": "site-replication.restart-persistence",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38110779315
+ ]
+ }
+ },
+ {
+ "case_id": "site-replication.remove-all",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38110839532
+ ]
+ }
+ },
+ {
+ "case_id": "site-replication.service-account-rotation",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38110909474
+ ]
+ }
+ },
+ {
+ "case_id": "kms.sse-kms-round-trip",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38042359938
+ ]
+ }
+ },
+ {
+ "case_id": "tiering.transition-and-restore",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38042759074
+ ]
+ }
+ },
+ {
+ "case_id": "events.delivery",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38046470999
+ ]
+ }
+ },
+ {
+ "case_id": "audit.request-correlation",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38050138583
+ ]
+ }
+ },
+ {
+ "case_id": "iam.policy-enforcement",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38043548625
+ ]
+ }
+ },
+ {
+ "case_id": "oidc.login-and-session",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38051425083
+ ]
+ }
+ },
+ {
+ "case_id": "s3.basic-object-round-trip",
+ "implementation": "implemented",
+ "basis": "Independent complete live acceptance already exercised the source procedure, assertions, evidence and finalization; accepted runs retain their own candidate/helper provenance.",
+ "references": [
+ "scripts/run_validation_case.sh"
+ ],
+ "gaps": [],
+ "live": {
+ "state": "accepted",
+ "basis": "independent-complete-live-audit",
+ "runs": [
+ 38042083422
+ ]
+ }
+ }
+ ],
+ "review_notes": "All source implementation gaps recorded in this review are closed. See docs/validation-migration-implementation-review.md for source semantics, exact candidate prerequisites and separate native/fleet proof scopes. Counts describe the accepted migration delivery scope; the 13 default-disabled environment procedures still require reviewed external resource adapters. Accepted live results remain historical independent audits across multiple candidates, never a single-candidate product certification. No historical FAIL is rewritten."
+}