fix(preprocess): anchor DWH retention validation
This commit is contained in:
@@ -260,6 +260,71 @@ def test_corrupt_newer_directory_does_not_consume_rollback_slot(tmp_path):
|
||||
assert corrupt.is_dir()
|
||||
|
||||
|
||||
def test_retention_n_counts_active_plus_n_minus_one_rollbacks_even_if_active_is_old(tmp_path):
|
||||
import os
|
||||
|
||||
run_ids = []
|
||||
pipeline = None
|
||||
for index in range(3):
|
||||
pipeline = DwhPreprocessPipeline(
|
||||
workspace_id="demo", workspace_root=tmp_path,
|
||||
config_fingerprint=FP, input_fingerprint=FP,
|
||||
introspect=lambda output, i=index: output.write_text(str(i)),
|
||||
build_lsh=lambda physical, output, i=index: [
|
||||
(output / name).write_text(str(i))
|
||||
for name in ("demo_lsh.pkl", "demo_minhashes.pkl", "demo_meta.json")
|
||||
], retain_generations=3,
|
||||
)
|
||||
run_ids.append(pipeline.run().run_id)
|
||||
generations = tmp_path / ".tht-dwh" / "generations"
|
||||
os.utime(generations / run_ids[-1], ns=(1, 1))
|
||||
|
||||
pipeline.retain_generations = 2
|
||||
pipeline._cleanup_generations()
|
||||
|
||||
remaining = {path.name for path in generations.iterdir() if path.is_dir()}
|
||||
assert remaining == {run_ids[-1], run_ids[-2]}
|
||||
|
||||
|
||||
def test_retention_candidate_swap_to_symlink_is_never_followed(monkeypatch, tmp_path):
|
||||
import tht.jobs.dwh_pipeline as module
|
||||
|
||||
pipeline = DwhPreprocessPipeline(
|
||||
workspace_id="demo", workspace_root=tmp_path,
|
||||
config_fingerprint=FP, input_fingerprint=FP,
|
||||
introspect=lambda output: output.write_text("active"),
|
||||
build_lsh=lambda physical, output: [
|
||||
(output / name).write_text("active")
|
||||
for name in ("demo_lsh.pkl", "demo_minhashes.pkl", "demo_meta.json")
|
||||
], retain_generations=1,
|
||||
)
|
||||
pipeline.run()
|
||||
generations = tmp_path / ".tht-dwh" / "generations"
|
||||
candidate_name = "e" * 32
|
||||
candidate = generations / candidate_name
|
||||
candidate.mkdir(mode=0o700)
|
||||
external = tmp_path / "external-crafted"
|
||||
external.mkdir()
|
||||
sentinel = external / "sentinel"
|
||||
sentinel.write_text("must-not-read-or-mutate")
|
||||
real_open = module.os.open
|
||||
swapped = False
|
||||
|
||||
def swapping_open(path, flags, *args, **kwargs):
|
||||
nonlocal swapped
|
||||
if path == candidate_name and kwargs.get("dir_fd") is not None and not swapped:
|
||||
swapped = True
|
||||
candidate.rmdir()
|
||||
candidate.symlink_to(external, target_is_directory=True)
|
||||
return real_open(path, flags, *args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(module.os, "open", swapping_open)
|
||||
pipeline._cleanup_generations()
|
||||
assert swapped
|
||||
assert sentinel.read_text() == "must-not-read-or-mutate"
|
||||
assert candidate.is_symlink()
|
||||
|
||||
|
||||
def test_reader_lease_blocks_retain_one_publisher_until_file_reads_finish(tmp_path):
|
||||
import threading
|
||||
import time
|
||||
|
||||
Reference in New Issue
Block a user