fix(scanner): consume raw page owner resume oracle (#7391)

Treat persisted raw page owner entries as a validated set, not a read_dir-order prefix, so restart scans can consume committed owner pages without recounting them against the raw enumeration budget. Commit checkpointed partial pages, validate owner parent/generation/digests before the skip oracle, and fail closed on duplicate/corrupt page state.

Extend the real scanner restart driver to enforce fixed raw-entry and object budgets across fresh OS processes, and report owner-index coverage in each worker round.

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
This commit is contained in:
houseme
2026-09-07 19:30:15 +08:00
committed by GitHub
parent fc76099280
commit 0c6314babc
8 changed files with 242 additions and 50 deletions
@@ -30,15 +30,21 @@ def validate_report(report, *, round_number, pid, objects, budget):
if type(report.get(key)) is not int or report[key] != value:
raise ValueError(f"worker report mismatch: {key}")
for key in ("raw_entries", "raw_name_bytes", "objects_before", "objects_retained",
"versions_retained", "bytes_retained", "objects_processed"):
"versions_retained", "bytes_retained", "objects_processed",
"raw_page_index_committed_entries", "raw_page_index_indexed_entries"):
if type(report.get(key)) is not int or not 0 <= report[key] <= 1048576:
raise ValueError(f"invalid bounded counter: {key}")
if report["raw_entries"] == 0:
made_budgeted_object_progress = report["objects_processed"] > 0
if report["raw_entries"] == 0 and not made_budgeted_object_progress:
raise ValueError("nonempty fixture must observe raw entries; budget hook may not have run")
if report["raw_entries"] > budget:
raise ValueError("raw-entry budget exceeded; no unbudgeted tail is permitted")
if report["objects_processed"] > budget:
raise ValueError("object budget exceeded; no unbudgeted scan tail is permitted")
for key in ("raw_first_entry", "raw_last_entry"):
value = report.get(key)
if report["raw_entries"] == 0 and made_budgeted_object_progress and value is None:
continue
if type(value) is not str or not 0 < len(value.encode("utf-8")) <= 512:
raise ValueError(f"invalid raw entry marker: {key}")
if type(report.get("snapshot_complete")) is not bool:
@@ -10,6 +10,8 @@ class ReportTests(unittest.TestCase):
return dict(schema=1, round=0, pid=123, objects_expected=4, raw_entry_budget=16,
raw_entries=8, raw_name_bytes=64, objects_before=0, objects_retained=4,
versions_retained=4, bytes_retained=4, objects_processed=4,
raw_page_index_committed_entries=4,
raw_page_index_indexed_entries=4,
raw_first_entry="bucket/object-0000",
raw_last_entry="bucket/object-0003/xl.meta",
snapshot_complete=True, outcome="complete")
@@ -44,6 +46,11 @@ class ReportTests(unittest.TestCase):
with self.assertRaises(ValueError):
self.validate(report)
report = self.report()
report["objects_processed"] = 17
with self.assertRaises(ValueError):
self.validate(report)
def test_missing_or_oversized_raw_marker_rejected(self):
for value in (None, True, "", "x" * 513):
with self.subTest(value=value):
@@ -55,9 +62,19 @@ class ReportTests(unittest.TestCase):
def test_complete_coverage_without_entry_observation_rejected(self):
report = self.report()
report["raw_entries"] = 0
report["objects_processed"] = 0
report["raw_page_index_committed_entries"] = 3
with self.assertRaises(ValueError):
self.validate(report)
def test_budgeted_object_progress_can_consume_all_raw_entries(self):
report = self.report()
report["raw_entries"] = 0
report["raw_first_entry"] = None
report["raw_last_entry"] = None
report["objects_processed"] = 1
self.validate(report)
def test_missing_wrong_type_and_negative_counter_rejected(self):
for value in (None, True, -1, "8", 1048577):
with self.subTest(value=value):