fix(preprocess): bind DWH artifacts to workspace
This commit is contained in:
@@ -30,6 +30,18 @@ _SAFE_FILE = re.compile(r"^[A-Za-z0-9_-]+\.(?:pkl|json)$")
|
||||
GENERATION_MANIFEST = "generation-manifest.json"
|
||||
|
||||
|
||||
def config_dwh_binding(cfg) -> dict[str, str]:
|
||||
workspace_id = getattr(cfg, "_workspace_id", None)
|
||||
config_source = getattr(cfg, "_config_source", None)
|
||||
if not isinstance(workspace_id, str) or not isinstance(config_source, str):
|
||||
raise CorruptCheckpointError("DWH workspace identity is unavailable; reload configuration")
|
||||
return {
|
||||
"workspace_id": workspace_id,
|
||||
"config_fingerprint": fingerprint(cfg.model_dump_json()),
|
||||
"input_fingerprint": fingerprint(config_source),
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DwhArtifactSnapshot:
|
||||
generation: str | None
|
||||
@@ -120,7 +132,9 @@ def _read_owned_at(directory_fd: int, name: str, *, readonly: bool) -> bytes:
|
||||
os.close(fd)
|
||||
|
||||
|
||||
def validate_generation_fd(directory_fd: int, generation: str) -> dict:
|
||||
def validate_generation_fd(
|
||||
directory_fd: int, generation: str, expected_binding: dict[str, str] | None = None,
|
||||
) -> dict:
|
||||
try:
|
||||
directory_info = os.fstat(directory_fd)
|
||||
if (
|
||||
@@ -138,8 +152,17 @@ def validate_generation_fd(directory_fd: int, generation: str) -> dict:
|
||||
or not isinstance(files, dict)
|
||||
or not re.fullmatch(r"sha256:[0-9a-f]{64}", manifest["job_spec_fingerprint"])
|
||||
or not re.fullmatch(r"[0-9a-f]{64}", manifest["artifact_manifest_sha256"])
|
||||
or not re.fullmatch(r"[a-z][a-z0-9_-]{0,63}", manifest["workspace_id"])
|
||||
or not re.fullmatch(r"sha256:[0-9a-f]{64}", manifest["config_fingerprint"])
|
||||
or not re.fullmatch(r"sha256:[0-9a-f]{64}", manifest["input_fingerprint"])
|
||||
):
|
||||
raise ValueError
|
||||
if expected_binding is not None and any(
|
||||
manifest.get(key) != value for key, value in expected_binding.items()
|
||||
):
|
||||
raise CorruptCheckpointError(
|
||||
"DWH artifacts belong to a different workspace configuration"
|
||||
)
|
||||
if set(os.listdir(directory_fd)) != set(files) | {GENERATION_MANIFEST}:
|
||||
raise ValueError
|
||||
for name, expected in files.items():
|
||||
@@ -153,13 +176,15 @@ def validate_generation_fd(directory_fd: int, generation: str) -> dict:
|
||||
raise CorruptCheckpointError("published DWH generation is invalid") from error
|
||||
|
||||
|
||||
def validate_generation(target: Path) -> dict:
|
||||
def validate_generation(
|
||||
target: Path, expected_binding: dict[str, str] | None = None,
|
||||
) -> dict:
|
||||
try:
|
||||
fd = os.open(target, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
|
||||
except OSError as error:
|
||||
raise CorruptCheckpointError("published DWH generation is invalid") from error
|
||||
try:
|
||||
return validate_generation_fd(fd, target.name)
|
||||
return validate_generation_fd(fd, target.name, expected_binding)
|
||||
finally:
|
||||
os.close(fd)
|
||||
|
||||
@@ -170,7 +195,7 @@ def resolve_dwh_snapshot(cfg) -> DwhArtifactSnapshot:
|
||||
return DwhArtifactSnapshot(
|
||||
None, cfg.paths.artifacts / "mschema" / "physical.yaml", cfg.paths.indexes / "lsh"
|
||||
)
|
||||
validate_generation(target)
|
||||
validate_generation(target, config_dwh_binding(cfg))
|
||||
return DwhArtifactSnapshot(target.name, target / "physical.yaml", target)
|
||||
|
||||
|
||||
@@ -229,10 +254,29 @@ class DwhPreprocessPipeline:
|
||||
):
|
||||
raise ValueError("LSH filenames must be unique flat safe names")
|
||||
|
||||
@property
|
||||
def binding(self) -> dict[str, str]:
|
||||
return {
|
||||
"workspace_id": self.workspace_id,
|
||||
"config_fingerprint": self.config_fingerprint,
|
||||
"input_fingerprint": self.input_fingerprint,
|
||||
}
|
||||
|
||||
def _assert_active_binding(self) -> None:
|
||||
active = active_generation_dir(self.workspace_root)
|
||||
if active is not None:
|
||||
validate_generation(active, self.binding)
|
||||
|
||||
def run(
|
||||
self, steps: tuple[str, ...] = DWH_STAGE_IDS, *, resume_run_id: str | None = None
|
||||
) -> JobReport:
|
||||
self._validate_steps(steps)
|
||||
lease_fd = _acquire_generation_lock(self.workspace_root, exclusive=False)
|
||||
try:
|
||||
self._assert_active_binding()
|
||||
finally:
|
||||
fcntl.flock(lease_fd, fcntl.LOCK_UN)
|
||||
os.close(lease_fd)
|
||||
if resume_run_id is not None:
|
||||
self._validate_resume_publication(resume_run_id)
|
||||
spec = JobSpec(
|
||||
@@ -290,7 +334,7 @@ class DwhPreprocessPipeline:
|
||||
if running is None:
|
||||
return set()
|
||||
target = self.workspace_root / ".tht-dwh" / "generations" / source.run_id
|
||||
validate_generation(target)
|
||||
validate_generation(target, self.binding)
|
||||
active = active_generation_dir(self.workspace_root)
|
||||
if active != target:
|
||||
raise CorruptCheckpointError("sealed DWH publication is not ACTIVE")
|
||||
@@ -355,6 +399,7 @@ class DwhPreprocessPipeline:
|
||||
seal_stage_artifacts(context, stage, required, spec)
|
||||
lease_fd = _acquire_generation_lock(self.workspace_root, exclusive=True)
|
||||
try:
|
||||
self._assert_active_binding()
|
||||
self._publish(context.run_id, artifacts, required)
|
||||
if self.after_publish is not None:
|
||||
self.after_publish(context.run_id)
|
||||
@@ -392,6 +437,7 @@ class DwhPreprocessPipeline:
|
||||
"artifact_manifest_sha256": _digest(
|
||||
artifacts / "artifact-manifest.json"
|
||||
),
|
||||
**self.binding,
|
||||
}
|
||||
manifest_path = temporary / GENERATION_MANIFEST
|
||||
manifest_path.write_text(
|
||||
@@ -429,9 +475,10 @@ class DwhPreprocessPipeline:
|
||||
pointer_tmp.unlink(missing_ok=True)
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def _validate_published(target: Path, artifacts: Path, required: tuple[str, ...]) -> None:
|
||||
manifest = validate_generation(target)
|
||||
def _validate_published(
|
||||
self, target: Path, artifacts: Path, required: tuple[str, ...],
|
||||
) -> None:
|
||||
manifest = validate_generation(target, self.binding)
|
||||
if set(manifest["files"]) != set(required):
|
||||
raise CorruptCheckpointError("published DWH generation is invalid")
|
||||
if manifest["artifact_manifest_sha256"] != _digest(
|
||||
@@ -463,6 +510,8 @@ class DwhPreprocessPipeline:
|
||||
if not root.exists():
|
||||
return
|
||||
active = active_generation_dir(self.workspace_root)
|
||||
if active is not None:
|
||||
validate_generation(active, self.binding)
|
||||
active_name = active.name if active else None
|
||||
protected = {active_name} if active_name else set()
|
||||
runs = self.workspace_root / ".tht-jobs" / "dwh" / "runs"
|
||||
@@ -487,7 +536,7 @@ class DwhPreprocessPipeline:
|
||||
continue
|
||||
try:
|
||||
info = os.fstat(candidate_fd)
|
||||
validate_generation_fd(candidate_fd, name)
|
||||
validate_generation_fd(candidate_fd, name, self.binding)
|
||||
except CorruptCheckpointError:
|
||||
continue
|
||||
finally:
|
||||
|
||||
Reference in New Issue
Block a user