fix(preprocess): claim DWH roots atomically
This commit is contained in:
@@ -28,6 +28,7 @@ DWH_STAGE_IDS = ("introspect", "lsh")
|
||||
_RUN_ID = re.compile(r"^[0-9a-f]{32}$")
|
||||
_SAFE_FILE = re.compile(r"^[A-Za-z0-9_-]+\.(?:pkl|json)$")
|
||||
GENERATION_MANIFEST = "generation-manifest.json"
|
||||
OWNER_MARKER = "OWNER.json"
|
||||
|
||||
|
||||
def config_dwh_binding(cfg) -> dict[str, str]:
|
||||
@@ -42,6 +43,96 @@ def config_dwh_binding(cfg) -> dict[str, str]:
|
||||
}
|
||||
|
||||
|
||||
def _binding_digest(binding: dict[str, str]) -> str:
|
||||
payload = json.dumps(binding, sort_keys=True, separators=(",", ":"))
|
||||
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def _read_root_binding(workspace_root: Path) -> dict[str, str]:
|
||||
marker = workspace_root / ".tht-dwh" / OWNER_MARKER
|
||||
try:
|
||||
payload = json.loads(_read_owned(marker, readonly=True).decode("utf-8"))
|
||||
binding = payload["binding"]
|
||||
if (
|
||||
payload.get("schema_version") != 1
|
||||
or not isinstance(binding, dict)
|
||||
or set(binding) != {"workspace_id", "config_fingerprint", "input_fingerprint"}
|
||||
or payload.get("binding_sha256") != _binding_digest(binding)
|
||||
):
|
||||
raise ValueError
|
||||
return binding
|
||||
except (OSError, KeyError, TypeError, ValueError, UnicodeDecodeError) as error:
|
||||
raise CorruptCheckpointError("DWH workspace ownership marker is missing or invalid") from error
|
||||
|
||||
|
||||
def _validate_root_binding(workspace_root: Path, expected: dict[str, str]) -> None:
|
||||
if _read_root_binding(workspace_root) != expected:
|
||||
raise CorruptCheckpointError("DWH artifacts belong to a different workspace configuration")
|
||||
root = workspace_root / ".tht-dwh"
|
||||
generations = root / "generations"
|
||||
active_exists = (root / "ACTIVE").exists()
|
||||
try:
|
||||
generations_fd = os.open(
|
||||
generations, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW
|
||||
)
|
||||
except FileNotFoundError:
|
||||
generation_entries = []
|
||||
except OSError as error:
|
||||
raise CorruptCheckpointError("DWH generations directory is invalid") from error
|
||||
else:
|
||||
try:
|
||||
generation_entries = os.listdir(generations_fd)
|
||||
finally:
|
||||
os.close(generations_fd)
|
||||
if generation_entries and not active_exists:
|
||||
raise CorruptCheckpointError("DWH generations exist without a consistent ACTIVE pointer")
|
||||
|
||||
|
||||
def _claim_or_validate_root_binding(workspace_root: Path, binding: dict[str, str]) -> None:
|
||||
root = workspace_root / ".tht-dwh"
|
||||
marker = root / OWNER_MARKER
|
||||
if marker.exists() or marker.is_symlink():
|
||||
_validate_root_binding(workspace_root, binding)
|
||||
return
|
||||
generations = root / "generations"
|
||||
generations_nonempty = False
|
||||
try:
|
||||
generations_fd = os.open(
|
||||
generations, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW
|
||||
)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except OSError as error:
|
||||
raise CorruptCheckpointError("DWH generations directory is invalid") from error
|
||||
else:
|
||||
try:
|
||||
generations_nonempty = bool(os.listdir(generations_fd))
|
||||
finally:
|
||||
os.close(generations_fd)
|
||||
if (root / "ACTIVE").exists() or generations_nonempty:
|
||||
raise CorruptCheckpointError(
|
||||
"DWH artifacts are unbound; migrate them explicitly or use an empty root"
|
||||
)
|
||||
payload = {
|
||||
"schema_version": 1,
|
||||
"binding": binding,
|
||||
"binding_sha256": _binding_digest(binding),
|
||||
}
|
||||
temporary = root / f".{OWNER_MARKER}.{uuid.uuid4().hex}.tmp"
|
||||
fd = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as stream:
|
||||
stream.write(json.dumps(payload, sort_keys=True, separators=(",", ":")) + "\n")
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
temporary.chmod(0o400)
|
||||
os.replace(temporary, marker)
|
||||
DwhPreprocessPipeline._fsync(root)
|
||||
except BaseException:
|
||||
temporary.unlink(missing_ok=True)
|
||||
raise
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DwhArtifactSnapshot:
|
||||
generation: str | None
|
||||
@@ -56,9 +147,16 @@ class DwhSnapshotLease:
|
||||
self.snapshot: DwhArtifactSnapshot | None = None
|
||||
|
||||
def __enter__(self) -> DwhArtifactSnapshot:
|
||||
binding = config_dwh_binding(self.cfg)
|
||||
claim_fd = _acquire_generation_lock(self.cfg.paths.artifacts.parent, exclusive=True)
|
||||
try:
|
||||
_claim_or_validate_root_binding(self.cfg.paths.artifacts.parent, binding)
|
||||
finally:
|
||||
fcntl.flock(claim_fd, fcntl.LOCK_UN)
|
||||
os.close(claim_fd)
|
||||
self._fd = _acquire_generation_lock(self.cfg.paths.artifacts.parent, exclusive=False)
|
||||
try:
|
||||
self.snapshot = resolve_dwh_snapshot(self.cfg)
|
||||
self.snapshot = _resolve_dwh_snapshot_locked(self.cfg, binding)
|
||||
return self.snapshot
|
||||
except BaseException:
|
||||
self.__exit__(None, None, None)
|
||||
@@ -190,12 +288,31 @@ def validate_generation(
|
||||
|
||||
|
||||
def resolve_dwh_snapshot(cfg) -> DwhArtifactSnapshot:
|
||||
binding = config_dwh_binding(cfg)
|
||||
claim_fd = _acquire_generation_lock(cfg.paths.artifacts.parent, exclusive=True)
|
||||
try:
|
||||
_claim_or_validate_root_binding(cfg.paths.artifacts.parent, binding)
|
||||
finally:
|
||||
fcntl.flock(claim_fd, fcntl.LOCK_UN)
|
||||
os.close(claim_fd)
|
||||
lease_fd = _acquire_generation_lock(cfg.paths.artifacts.parent, exclusive=False)
|
||||
try:
|
||||
return _resolve_dwh_snapshot_locked(cfg, binding)
|
||||
finally:
|
||||
fcntl.flock(lease_fd, fcntl.LOCK_UN)
|
||||
os.close(lease_fd)
|
||||
|
||||
|
||||
def _resolve_dwh_snapshot_locked(
|
||||
cfg, binding: dict[str, str],
|
||||
) -> DwhArtifactSnapshot:
|
||||
_validate_root_binding(cfg.paths.artifacts.parent, binding)
|
||||
target = active_generation_dir(cfg.paths.artifacts.parent)
|
||||
if target is None:
|
||||
return DwhArtifactSnapshot(
|
||||
None, cfg.paths.artifacts / "mschema" / "physical.yaml", cfg.paths.indexes / "lsh"
|
||||
)
|
||||
validate_generation(target, config_dwh_binding(cfg))
|
||||
validate_generation(target, binding)
|
||||
return DwhArtifactSnapshot(target.name, target / "physical.yaml", target)
|
||||
|
||||
|
||||
@@ -263,6 +380,7 @@ class DwhPreprocessPipeline:
|
||||
}
|
||||
|
||||
def _assert_active_binding(self) -> None:
|
||||
_validate_root_binding(self.workspace_root, self.binding)
|
||||
active = active_generation_dir(self.workspace_root)
|
||||
if active is not None:
|
||||
validate_generation(active, self.binding)
|
||||
@@ -271,8 +389,9 @@ class DwhPreprocessPipeline:
|
||||
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)
|
||||
lease_fd = _acquire_generation_lock(self.workspace_root, exclusive=True)
|
||||
try:
|
||||
_claim_or_validate_root_binding(self.workspace_root, self.binding)
|
||||
self._assert_active_binding()
|
||||
finally:
|
||||
fcntl.flock(lease_fd, fcntl.LOCK_UN)
|
||||
@@ -399,6 +518,7 @@ class DwhPreprocessPipeline:
|
||||
seal_stage_artifacts(context, stage, required, spec)
|
||||
lease_fd = _acquire_generation_lock(self.workspace_root, exclusive=True)
|
||||
try:
|
||||
_validate_root_binding(self.workspace_root, self.binding)
|
||||
self._assert_active_binding()
|
||||
self._publish(context.run_id, artifacts, required)
|
||||
if self.after_publish is not None:
|
||||
@@ -509,6 +629,7 @@ class DwhPreprocessPipeline:
|
||||
root = self.workspace_root / ".tht-dwh" / "generations"
|
||||
if not root.exists():
|
||||
return
|
||||
_validate_root_binding(self.workspace_root, self.binding)
|
||||
active = active_generation_dir(self.workspace_root)
|
||||
if active is not None:
|
||||
validate_generation(active, self.binding)
|
||||
|
||||
Reference in New Issue
Block a user