Files
rustfs/scripts/test_conditional_read_abba.py
T
dd90bb7488 fix(s3): short-circuit conditional reads at metadata quorum (#8459)
* fix(s3): evaluate conditional reads before opening object bodies

Reuse locked metadata snapshots for conditional GETs and unify HEAD
preconditions and cache headers. Preserve body-cache hooks and validate
encryption metadata and key authorization before returning bodyless errors.

Co-Authored-By: heihutu <heihutu@gmail.com>
Co-Authored-By: zhi22915 <qiuzgang@gmail.com>

* refactor(s3): unify conditional reads at metadata quorum

Apply scoped metadata conditions to the existing reader pipeline instead
of maintaining parallel prepared-read branches. Preserve quorum checks,
early-stop, coalescing, and cache-hook behavior on condition misses.

Track metadata-only termination through selection and reject body access
or a changed decision. Keep concrete reader futures off request stacks.

Co-Authored-By: heihutu <heihutu@gmail.com>
Co-Authored-By: zhi22915 <qiuzgang@gmail.com>

* test(s3): strengthen conditional read acceptance evidence

Assert zero shard, decode, cache-fill, and remote-body calls on matching
conditions with live positive controls. Exercise GET and HEAD through
s3s and Hyper with conditional-header and precedence matrices.

Add reproducible paired application-read benchmarks with isolated
metadata controls and strict drift, artifact, and resource gates.

Co-Authored-By: heihutu <heihutu@gmail.com>
Co-Authored-By: zhi22915 <qiuzgang@gmail.com>

* refactor(s3): remove obsolete conditional comparison helper

Keep ETag comparison tests on the canonical read-condition evaluator,
remove unused date-trait imports, and name the HTTP case type.

Co-Authored-By: heihutu <heihutu@gmail.com>
Co-Authored-By: zhi22915 <qiuzgang@gmail.com>

* fix(s3): validate full conditional etag fields

Reject adjacent tags and malformed trailing list members before accepting a
match. Keep the existing request-scoped metadata read and authorization order,
while removing redundant validation wrappers and unused benchmark overrides.
Replace simulated phase coverage with real 304/412 cache-fill probes and extend
HTTP/1.1 and HTTP/2 ingress regressions.

Co-Authored-By: heihutu <heihutu@gmail.com>
Co-Authored-By: zhi22915 <qiuzgang@gmail.com>

* fix(ecstore): cache terminal conditional metadata safely

Preserve quorum-validated conditional metadata when early termination
cancels the remaining fanout. Restrict those entries to terminal GET
readers; body and full-layout consumers still obtain their own metadata.

Serialize cache publication per key so conditional entries cannot replace
complete layouts or renew their TTL. Retain generation fencing and the
final selected-metadata predicate check before any body access.

Cover real four-disk hedging, nonterminal body and range reads, consumer
isolation, generation races, atomic publication and actual cache expiry.

Co-Authored-By: heihutu <heihutu@gmail.com>
Co-Authored-By: zhi22915 <qiuzgang@gmail.com>

* fix(log-analyzer): align heal writer failure rule with current logs

Match the current structured all-targets failure, retain legacy log support, and reject partial or unrelated failures.

Co-Authored-By: heihutu <heihutu@gmail.com>
Co-Authored-By: zhi22915 <qiuzgang@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-10-12 16:59:21 +08:00

117 lines
5.2 KiB
Python

#!/usr/bin/env python3
"""Verify that paired conditional-read evidence cannot bypass acceptance gates."""
import copy
import json
from pathlib import Path
import subprocess
import unittest
from unittest.mock import MagicMock, patch
from run_conditional_read_abba import compare, parse_rows, run_case
def row():
return {"size": 4096, "kind": "miss", "cache": False, "slowtail_ms": "0",
"ops_per_sec": 1000, "p95_ms": 2, "p99_ms": 3,
"lock_p95_ms": 1, "lock_p99_ms": 2}
class AcceptanceTests(unittest.TestCase):
def test_stable_equivalent_legs_pass(self):
self.assertTrue(compare([{("case",): row()} for _ in range(4)])["accepted"])
def test_baseline_drift_blocks_candidate_attribution(self):
legs = [{("case",): row()} for _ in range(4)]
legs[3][("case",)]["ops_per_sec"] = 800
result = compare(legs)
self.assertFalse(result["accepted"])
self.assertEqual(result["reason"], "baseline_drift")
self.assertNotIn("cases", result)
def test_miss_throughput_and_lock_regressions_fail(self):
for metric, value in [("ops_per_sec", 900), ("lock_p99_ms", 4)]:
legs = [{("case",): row()} for _ in range(4)]
for index in (1, 2):
legs[index][("case",)][metric] = value
self.assertFalse(compare(legs)["accepted"], metric)
def test_missing_workload_is_rejected(self):
legs = [{("case",): row()} for _ in range(4)]
legs[1] = {}
with self.assertRaises(ValueError):
compare(legs)
def test_complete_rows_include_first_libtest_output_line(self):
rows = []
for size in (4096, 1_300_000):
for kind in ("unconditional", "hit", "miss"):
value = copy.copy(row())
value.update(size=size, kind=kind)
rows.append("CONDITIONAL_BENCH " + json.dumps(value))
output = "test app::benchmark ... " + "\n".join(rows)
self.assertEqual(len(parse_rows(output)), 6)
with self.assertRaises(ValueError):
parse_rows(output + "\n" + rows[0])
def test_wrong_shape_or_nonfinite_metrics_are_rejected(self):
rows = []
for size in (4096, 1_300_000):
for kind in ("unconditional", "hit", "miss"):
value = copy.copy(row())
value.update(size=size, kind=kind)
rows.append(value)
for field, value in [("kind", "unknown"), ("p99_ms", float("nan"))]:
changed = copy.deepcopy(rows)
changed[0][field] = value
output = "\n".join("CONDITIONAL_BENCH " + json.dumps(item) for item in changed)
with self.assertRaises(ValueError):
parse_rows(output)
class IsolationTests(unittest.TestCase):
def process(self):
process = MagicMock()
process.__enter__.return_value = process
process.communicate.return_value = ("saved output", None)
process.returncode = 0
return process
def test_changed_executable_is_rejected_before_launch(self):
with patch("run_conditional_read_abba.digest", return_value="changed"), \
patch("run_conditional_read_abba.subprocess.Popen") as launch:
with self.assertRaisesRegex(RuntimeError, "before execution"):
run_case(Path("binary"), "expected", {})
launch.assert_not_called()
def test_changed_executable_is_rejected_after_launch(self):
with patch("run_conditional_read_abba.digest", side_effect=["expected", "changed"]), \
patch("run_conditional_read_abba.check_resource_isolation"), \
patch("run_conditional_read_abba.subprocess.Popen", return_value=self.process()):
with self.assertRaisesRegex(RuntimeError, "during execution"):
run_case(Path("binary"), "expected", {})
def test_final_case_checks_resource_isolation_after_exit(self):
with patch("run_conditional_read_abba.digest", return_value="expected"), \
patch("run_conditional_read_abba.check_resource_isolation", side_effect=RuntimeError("concurrent build")), \
patch("run_conditional_read_abba.subprocess.Popen", return_value=self.process()):
with self.assertRaisesRegex(RuntimeError, "concurrent build"):
run_case(Path("binary"), "expected", {})
def test_concurrent_build_during_execution_kills_case_and_saves_log(self):
process = self.process()
process.communicate.side_effect = [subprocess.TimeoutExpired("binary", 1), ("saved output", None)]
log = MagicMock()
with patch("run_conditional_read_abba.digest", return_value="expected"), \
patch("run_conditional_read_abba.check_resource_isolation", side_effect=RuntimeError("concurrent build")) as check, \
patch("run_conditional_read_abba.subprocess.Popen", return_value=process):
with self.assertRaisesRegex(RuntimeError, "concurrent build"):
run_case(Path("binary"), "expected", {}, log)
check.assert_called_once_with(check_load=False)
process.kill.assert_called_once()
log.write_text.assert_called_once_with("saved output")
if __name__ == "__main__":
unittest.main()