fix(preprocess): reconcile durable DWH generations
This commit is contained in:
@@ -6,6 +6,7 @@ from typer.testing import CliRunner
|
||||
|
||||
from tht.cli import app
|
||||
from tht.jobs.dwh_pipeline import DwhPreprocessPipeline
|
||||
from tht.jobs.dwh_pipeline import resolve_dwh_snapshot
|
||||
from tht.jobs.locking import _lock_name
|
||||
|
||||
|
||||
@@ -36,7 +37,8 @@ def test_selected_dwh_stages_run_in_declared_order(tmp_path):
|
||||
published = tmp_path / ".tht-dwh" / "generations" / active
|
||||
assert (published / "physical.yaml").read_text() == "catalog"
|
||||
assert sorted(path.name for path in published.iterdir()) == [
|
||||
"demo_lsh.pkl", "demo_meta.json", "demo_minhashes.pkl", "physical.yaml"
|
||||
"demo_lsh.pkl", "demo_meta.json", "demo_minhashes.pkl",
|
||||
"generation-manifest.json", "physical.yaml",
|
||||
]
|
||||
|
||||
|
||||
@@ -139,3 +141,90 @@ def test_failed_multi_file_build_never_replaces_active_generation(tmp_path):
|
||||
],
|
||||
).run(("introspect", "lsh"), resume_run_id=failed.run_id)
|
||||
assert resumed.status == "succeeded"
|
||||
|
||||
|
||||
def test_unsafe_lsh_filename_is_rejected(tmp_path):
|
||||
import pytest
|
||||
|
||||
with pytest.raises(ValueError, match="flat safe"):
|
||||
DwhPreprocessPipeline(
|
||||
workspace_id="demo", workspace_root=tmp_path,
|
||||
config_fingerprint=FP, input_fingerprint=FP,
|
||||
introspect=lambda output: None, build_lsh=lambda physical, output: None,
|
||||
lsh_filenames=("../escape.pkl", "ok.pkl", "meta.json"),
|
||||
)
|
||||
|
||||
|
||||
def test_active_fsync_failure_restores_previous_pointer(monkeypatch, tmp_path):
|
||||
def build(physical, output):
|
||||
for name in ("demo_lsh.pkl", "demo_minhashes.pkl", "demo_meta.json"):
|
||||
(output / name).write_text(name)
|
||||
|
||||
first_pipeline = DwhPreprocessPipeline(
|
||||
workspace_id="demo", workspace_root=tmp_path,
|
||||
config_fingerprint=FP, input_fingerprint=FP,
|
||||
introspect=lambda output: output.write_text("old"), build_lsh=build,
|
||||
)
|
||||
first = first_pipeline.run()
|
||||
original_fsync = first_pipeline._fsync
|
||||
failed_once = False
|
||||
|
||||
def fail_active_once(path):
|
||||
nonlocal failed_once
|
||||
if path.name == ".tht-dwh" and not failed_once:
|
||||
failed_once = True
|
||||
raise OSError("injected directory fsync failure")
|
||||
original_fsync(path)
|
||||
|
||||
second = DwhPreprocessPipeline(
|
||||
workspace_id="demo", workspace_root=tmp_path,
|
||||
config_fingerprint=FP, input_fingerprint=FP,
|
||||
introspect=lambda output: output.write_text("new"), build_lsh=build,
|
||||
)
|
||||
monkeypatch.setattr(second, "_fsync", fail_active_once)
|
||||
failed = second.run()
|
||||
assert failed.status == "failed"
|
||||
assert (tmp_path / ".tht-dwh" / "ACTIVE").read_text().strip() == first.run_id
|
||||
|
||||
|
||||
def test_snapshot_stays_on_one_generation_across_publish(tmp_path):
|
||||
from types import SimpleNamespace
|
||||
|
||||
def pipeline(content):
|
||||
return DwhPreprocessPipeline(
|
||||
workspace_id="demo", workspace_root=tmp_path,
|
||||
config_fingerprint=FP, input_fingerprint=FP,
|
||||
introspect=lambda output: output.write_text(content),
|
||||
build_lsh=lambda physical, output: [
|
||||
(output / name).write_text(content)
|
||||
for name in ("demo_lsh.pkl", "demo_minhashes.pkl", "demo_meta.json")
|
||||
],
|
||||
)
|
||||
|
||||
first = pipeline("old").run()
|
||||
cfg = SimpleNamespace(paths=SimpleNamespace(
|
||||
artifacts=tmp_path / "artifacts", indexes=tmp_path / "indexes"
|
||||
))
|
||||
snapshot = resolve_dwh_snapshot(cfg)
|
||||
pipeline("new").run()
|
||||
assert snapshot.generation == first.run_id
|
||||
assert snapshot.physical.read_text() == "old"
|
||||
assert (snapshot.lsh_dir / "demo_meta.json").read_text() == "old"
|
||||
|
||||
|
||||
def test_generation_retention_keeps_active_and_one_rollback(tmp_path):
|
||||
run_ids = []
|
||||
for index in range(5):
|
||||
report = 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=2,
|
||||
).run()
|
||||
run_ids.append(report.run_id)
|
||||
remaining = {path.name for path in (tmp_path / ".tht-dwh" / "generations").iterdir()}
|
||||
assert remaining == set(run_ids[-2:])
|
||||
|
||||
@@ -100,6 +100,7 @@ def test_resume_rejects_tampered_succeeded_stage_artifact(tmp_path):
|
||||
|
||||
def test_post_publish_crash_reconciles_same_generation_on_resume(tmp_path):
|
||||
crashed = False
|
||||
builder_calls = 0
|
||||
|
||||
def crash_once(_generation):
|
||||
nonlocal crashed
|
||||
@@ -107,11 +108,16 @@ def test_post_publish_crash_reconciles_same_generation_on_resume(tmp_path):
|
||||
crashed = True
|
||||
raise KeyboardInterrupt("simulated process death")
|
||||
|
||||
def build(physical, output):
|
||||
nonlocal builder_calls
|
||||
builder_calls += 1
|
||||
_recover_lsh([], output)
|
||||
|
||||
pipeline = DwhPreprocessPipeline(
|
||||
workspace_id="demo", workspace_root=tmp_path,
|
||||
config_fingerprint=FP, input_fingerprint=FP,
|
||||
introspect=lambda output: output.write_text("catalog"),
|
||||
build_lsh=lambda physical, output: _recover_lsh([], output),
|
||||
build_lsh=build,
|
||||
after_publish=crash_once,
|
||||
)
|
||||
with pytest.raises(KeyboardInterrupt):
|
||||
@@ -123,7 +129,8 @@ def test_post_publish_crash_reconciles_same_generation_on_resume(tmp_path):
|
||||
|
||||
resumed = pipeline.run(("introspect", "lsh"), resume_run_id=source_run_id)
|
||||
assert resumed.status == "succeeded"
|
||||
assert (tmp_path / ".tht-dwh" / "ACTIVE").read_text().strip() == resumed.run_id
|
||||
assert builder_calls == 1
|
||||
assert (tmp_path / ".tht-dwh" / "ACTIVE").read_text().strip() == source_run_id
|
||||
|
||||
|
||||
def test_resume_of_succeeded_run_detects_tampered_published_file(tmp_path):
|
||||
@@ -137,7 +144,8 @@ def test_resume_of_succeeded_run_detects_tampered_published_file(tmp_path):
|
||||
published = (
|
||||
tmp_path / ".tht-dwh" / "generations" / succeeded.run_id / "demo_meta.json"
|
||||
)
|
||||
published.chmod(0o600)
|
||||
published.write_text("tampered")
|
||||
|
||||
with pytest.raises(Exception, match="digest mismatch"):
|
||||
with pytest.raises(Exception, match="published DWH"):
|
||||
pipeline.run(("introspect", "lsh"), resume_run_id=succeeded.run_id)
|
||||
|
||||
Reference in New Issue
Block a user