fix(dwh): anchor generation operations to lease fd

This commit is contained in:
2026-07-12 07:26:10 +02:00
parent 24e61d5713
commit 2f6daaaea6
3 changed files with 495 additions and 242 deletions
@@ -0,0 +1,20 @@
# Evidence Task 6 — final fd-anchored DWH correction
All DWH generation state below `.tht-dwh` is now accessed relative to the directory descriptor
retained by the shared/exclusive generation lease. ACTIVE reads, atomic temp writes, replacement,
fsync, and rollback use `openat`/`replaceat` operations. Generation staging, validation,
reconciliation, resume checks, retention classification, and recursive deletion likewise use owned
root/generations/candidate descriptors with `O_NOFOLLOW`; locked operations no longer reopen
generation paths through `workspace_root`.
Portable reader snapshots are copied from validated generation file descriptors into private 0700
process-owned temporary directories while the shared lease is held. This avoids Linux-only
`/proc/self/fd` paths and prevents a renamed/replaced `.tht-dwh` pathname from redirecting later
schema or LSH reads. Lease-scoped copies are removed on exit and standalone snapshots are removed
at process exit.
Deterministic adversarial tests rename the DWH root after lease acquisition during ACTIVE reads,
ACTIVE publication, and retention cleanup. Each test proves the replacement tree is never read,
written, or deleted; the descriptor-pinned original either completes consistently or fails closed.
Existing owner binding, legacy rejection, crash reconciliation, resume, atomic rollback, retention,
and reader/writer exclusion behavior remains covered.
+127 -5
View File
@@ -445,6 +445,8 @@ def test_unsafe_lsh_filename_is_rejected(tmp_path):
def test_active_fsync_failure_restores_previous_pointer(monkeypatch, tmp_path):
import os
import tht.jobs.dwh_pipeline as module
def build(physical, output):
for name in ("demo_lsh.pkl", "demo_minhashes.pkl", "demo_meta.json"):
(output / name).write_text(name)
@@ -455,27 +457,147 @@ def test_active_fsync_failure_restores_previous_pointer(monkeypatch, tmp_path):
introspect=lambda output: output.write_text("old"), build_lsh=build,
)
first = first_pipeline.run()
original_fsync = first_pipeline._fsync
root = tmp_path / ".tht-dwh"
root_identity = (root.stat().st_dev, root.stat().st_ino)
original_fsync = module.os.fsync
failed_once = False
def fail_active_once(path):
def fail_active_once(fd):
nonlocal failed_once
if path.name == ".tht-dwh" and not failed_once:
info = os.fstat(fd)
if (
(info.st_dev, info.st_ino) == root_identity
and "ACTIVE" in os.listdir(fd)
and not failed_once
):
failed_once = True
raise OSError("injected directory fsync failure")
original_fsync(path)
original_fsync(fd)
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)
monkeypatch.setattr(module.os, "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_root_swap_after_lease_never_reads_replacement(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("trusted"),
build_lsh=lambda physical, output: [
(output / name).write_text("trusted")
for name in ("demo_lsh.pkl", "demo_minhashes.pkl", "demo_meta.json")
],
)
first = pipeline.run()
assert first.status == "succeeded"
root = tmp_path / ".tht-dwh"
moved = tmp_path / "moved-read-root"
replacement = root
real_read = module._read_owned_at
swapped = False
def swapping_read(directory_fd, name, *, readonly):
nonlocal swapped
if name == "ACTIVE" and not swapped:
swapped = True
replacement.rename(moved)
replacement.mkdir(mode=0o700)
(replacement / "sentinel").write_text("replacement-secret")
return real_read(directory_fd, name, readonly=readonly)
monkeypatch.setattr(module, "_read_owned_at", swapping_read)
try:
with lease_dwh_snapshot(snapshot_config(tmp_path)) as snapshot:
assert snapshot.physical.read_text() == "trusted"
except Exception as error:
assert "ACTIVE" in str(error) or "root" in str(error)
assert swapped
assert (replacement / "sentinel").read_text() == "replacement-secret"
def test_publish_root_swap_after_lease_never_writes_replacement(monkeypatch, tmp_path):
import tht.jobs.dwh_pipeline as module
def make(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 = make("old").run()
assert first.status == "succeeded", first
root = tmp_path / ".tht-dwh"
moved = tmp_path / "moved-publish-root"
real_replace = module.os.replace
swapped = False
def swapping_replace(source, destination, *args, **kwargs):
nonlocal swapped
if destination == "ACTIVE" and kwargs.get("dst_dir_fd") is not None and not swapped:
swapped = True
root.rename(moved)
root.mkdir(mode=0o700)
(root / "sentinel").write_text("replacement-safe")
return real_replace(source, destination, *args, **kwargs)
monkeypatch.setattr(module.os, "replace", swapping_replace)
result = make("new").run()
assert result.status in {"succeeded", "failed"}
assert swapped
assert (root / "sentinel").read_text() == "replacement-safe"
moved_active = (moved / "ACTIVE").read_text().strip()
assert len(moved_active) == 32
assert (moved / "generations" / moved_active).is_dir()
def test_cleanup_root_swap_after_lease_never_deletes_replacement(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("trusted"),
build_lsh=lambda physical, output: [
(output / name).write_text("trusted")
for name in ("demo_lsh.pkl", "demo_minhashes.pkl", "demo_meta.json")
],
retain_generations=1,
)
pipeline.run()
root = tmp_path / ".tht-dwh"
moved = tmp_path / "moved-cleanup-root"
real_open = module.os.open
swapped = False
def swapping_open(path, flags, *args, **kwargs):
nonlocal swapped
if path == "generations" and kwargs.get("dir_fd") is not None and not swapped:
swapped = True
root.rename(moved)
root.mkdir(mode=0o700)
(root / "sentinel").write_text("replacement-safe")
return real_open(path, flags, *args, **kwargs)
monkeypatch.setattr(module.os, "open", swapping_open)
pipeline._cleanup_generations()
assert swapped
assert (root / "sentinel").read_text() == "replacement-safe"
def test_snapshot_stays_on_one_generation_across_publish(tmp_path):
def pipeline(content):
return DwhPreprocessPipeline(
+348 -237
View File
@@ -5,10 +5,12 @@ from __future__ import annotations
import hashlib
import json
import fcntl
import atexit
import os
import re
import shutil
import stat
import tempfile
import uuid
from collections.abc import Callable
from dataclasses import dataclass
@@ -29,6 +31,16 @@ _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"
_SNAPSHOT_DIRS: set[Path] = set()
def _cleanup_snapshot_dirs() -> None:
for path in tuple(_SNAPSHOT_DIRS):
shutil.rmtree(path, ignore_errors=True)
_SNAPSHOT_DIRS.discard(path)
atexit.register(_cleanup_snapshot_dirs)
def config_dwh_binding(cfg) -> dict[str, str]:
@@ -48,19 +60,6 @@ def _binding_digest(binding: dict[str, str]) -> str:
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
def _read_root_binding(workspace_root: Path) -> dict[str, str]:
try:
root_fd = os.open(
workspace_root / ".tht-dwh", os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW
)
try:
return _read_root_binding_fd(root_fd)
finally:
os.close(root_fd)
except OSError as error:
raise CorruptCheckpointError("DWH workspace ownership marker is missing or invalid") from error
def _read_root_binding_fd(root_fd: int) -> dict[str, str]:
try:
fd = os.open(OWNER_MARKER, os.O_RDONLY | os.O_NOFOLLOW, dir_fd=root_fd)
@@ -92,19 +91,6 @@ def _read_root_binding_fd(root_fd: int) -> dict[str, str]:
raise CorruptCheckpointError("DWH workspace ownership marker is missing or invalid") from error
def _validate_root_binding(workspace_root: Path, expected: dict[str, str]) -> None:
try:
root_fd = os.open(
workspace_root / ".tht-dwh", os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW
)
except OSError as error:
raise CorruptCheckpointError("DWH workspace ownership marker is missing or invalid") from error
try:
_validate_root_binding_fd(root_fd, expected)
finally:
os.close(root_fd)
def _validate_root_binding_fd(root_fd: int, expected: dict[str, str]) -> None:
if _read_root_binding_fd(root_fd) != expected:
raise CorruptCheckpointError("DWH artifacts belong to a different workspace configuration")
@@ -195,6 +181,7 @@ class DwhArtifactSnapshot:
generation: str | None
physical: Path
lsh_dir: Path
_holder: object | None = None
@dataclass
@@ -249,6 +236,9 @@ class DwhSnapshotLease:
if self._lease is not None:
lease, self._lease = self._lease, None
lease.close()
if self.snapshot is not None and isinstance(self.snapshot._holder, Path):
shutil.rmtree(self.snapshot._holder, ignore_errors=True)
_SNAPSHOT_DIRS.discard(self.snapshot._holder)
def lease_dwh_snapshot(cfg) -> DwhSnapshotLease:
@@ -323,25 +313,6 @@ def _digest(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def _read_owned(path: Path, *, readonly: bool) -> bytes:
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW)
try:
info = os.fstat(fd)
if (
not stat.S_ISREG(info.st_mode)
or info.st_uid != os.getuid()
or info.st_nlink != 1
or (readonly and bool(info.st_mode & 0o222))
):
raise OSError("unsafe DWH generation file")
chunks = []
while chunk := os.read(fd, 1024 * 1024):
chunks.append(chunk)
return b"".join(chunks)
finally:
os.close(fd)
def _read_owned_at(directory_fd: int, name: str, *, readonly: bool) -> bytes:
fd = os.open(name, os.O_RDONLY | os.O_NOFOLLOW, dir_fd=directory_fd)
try:
@@ -361,6 +332,94 @@ def _read_owned_at(directory_fd: int, name: str, *, readonly: bool) -> bytes:
os.close(fd)
def _open_generations_fd(root_fd: int, *, create: bool = False) -> int:
if create:
try:
os.mkdir("generations", 0o700, dir_fd=root_fd)
os.fsync(root_fd)
except FileExistsError:
pass
fd = os.open(
"generations", os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=root_fd
)
info = os.fstat(fd)
if (
not stat.S_ISDIR(info.st_mode)
or info.st_uid != os.getuid()
or stat.S_IMODE(info.st_mode) != 0o700
):
os.close(fd)
raise CorruptCheckpointError("DWH generations directory is invalid")
return fd
def _open_generation_fd(generations_fd: int, generation: str) -> int:
if not _RUN_ID.fullmatch(generation):
raise CorruptCheckpointError("DWH generation identity is invalid")
try:
return os.open(
generation, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
dir_fd=generations_fd,
)
except OSError as error:
raise CorruptCheckpointError("active DWH generation is missing") from error
def _active_generation_fd(
root_fd: int, expected_binding: dict[str, str]
) -> tuple[str, int] | None:
_validate_root_binding_fd(root_fd, expected_binding)
try:
generation = _read_owned_at(root_fd, "ACTIVE", readonly=False).decode().strip()
except FileNotFoundError:
try:
generations_fd = _open_generations_fd(root_fd)
except FileNotFoundError:
return None
try:
if os.listdir(generations_fd):
raise CorruptCheckpointError(
"DWH generations exist without a consistent ACTIVE pointer"
)
finally:
os.close(generations_fd)
return None
except (OSError, UnicodeDecodeError) as error:
raise CorruptCheckpointError("DWH ACTIVE pointer is invalid") from error
if not _RUN_ID.fullmatch(generation):
raise CorruptCheckpointError("DWH ACTIVE pointer is invalid")
generations_fd = _open_generations_fd(root_fd)
try:
generation_fd = _open_generation_fd(generations_fd, generation)
finally:
os.close(generations_fd)
try:
validate_generation_fd(generation_fd, generation, expected_binding)
return generation, generation_fd
except BaseException:
os.close(generation_fd)
raise
def _materialize_generation_fd(
generation_fd: int, generation: str, binding: dict[str, str]
) -> tuple[Path, Path]:
holder = Path(tempfile.mkdtemp(prefix="tht-dwh-snapshot-"))
_SNAPSHOT_DIRS.add(holder)
snapshot_root = holder
try:
manifest = validate_generation_fd(generation_fd, generation, binding)
for name in manifest["files"]:
destination = snapshot_root / name
destination.write_bytes(_read_owned_at(generation_fd, name, readonly=True))
destination.chmod(0o400)
return holder, snapshot_root
except BaseException:
shutil.rmtree(holder, ignore_errors=True)
_SNAPSHOT_DIRS.discard(holder)
raise
def validate_generation_fd(
directory_fd: int, generation: str, expected_binding: dict[str, str] | None = None,
) -> dict:
@@ -405,19 +464,6 @@ def validate_generation_fd(
raise CorruptCheckpointError("published DWH generation is invalid") from error
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, expected_binding)
finally:
os.close(fd)
def resolve_dwh_snapshot(cfg) -> DwhArtifactSnapshot:
binding = config_dwh_binding(cfg)
lease = _acquire_existing_generation_lock(
@@ -432,16 +478,21 @@ def resolve_dwh_snapshot(cfg) -> DwhArtifactSnapshot:
def _resolve_dwh_snapshot_locked(
cfg, binding: dict[str, str], root_fd: int,
) -> DwhArtifactSnapshot:
_validate_root_binding_fd(root_fd, binding)
target = _active_generation_dir_locked(
cfg.paths.artifacts.parent, binding, root_fd=root_fd
)
if target is None:
active = _active_generation_fd(root_fd, binding)
if active is None:
return DwhArtifactSnapshot(
None, cfg.paths.artifacts / "mschema" / "physical.yaml", cfg.paths.indexes / "lsh"
)
validate_generation(target, binding)
return DwhArtifactSnapshot(target.name, target / "physical.yaml", target)
generation, generation_fd = active
try:
holder, snapshot_root = _materialize_generation_fd(
generation_fd, generation, binding
)
finally:
os.close(generation_fd)
return DwhArtifactSnapshot(
generation, snapshot_root / "physical.yaml", snapshot_root, holder
)
def active_generation_dir(
@@ -449,36 +500,17 @@ def active_generation_dir(
) -> Path | None:
lease = _acquire_existing_generation_lock(workspace_root, exclusive=False)
try:
return _active_generation_dir_locked(
workspace_root, expected_binding, root_fd=lease.root_fd
)
active = _active_generation_fd(lease.root_fd, expected_binding)
if active is None:
return None
generation, generation_fd = active
os.close(generation_fd)
lease.assert_root_identity(workspace_root / ".tht-dwh")
return workspace_root / ".tht-dwh" / "generations" / generation
finally:
lease.close()
def _active_generation_dir_locked(
workspace_root: Path, expected_binding: dict[str, str], *, root_fd: int | None = None
) -> Path | None:
if root_fd is None:
_validate_root_binding(workspace_root, expected_binding)
else:
_validate_root_binding_fd(root_fd, expected_binding)
pointer = workspace_root / ".tht-dwh" / "ACTIVE"
try:
generation = _read_owned(pointer, readonly=False).decode("utf-8").strip()
except FileNotFoundError:
return None
except (OSError, UnicodeDecodeError) as error:
raise CorruptCheckpointError("DWH ACTIVE pointer is invalid") from error
if not _RUN_ID.fullmatch(generation):
raise CorruptCheckpointError("DWH ACTIVE pointer is invalid")
target = pointer.parent / "generations" / generation
if not target.is_dir() or target.is_symlink():
raise CorruptCheckpointError("active DWH generation is missing")
validate_generation(target, expected_binding)
return target
class DwhPreprocessPipeline:
"""Stage a complete artifact bundle, then publish it through one atomic pointer."""
@@ -509,6 +541,7 @@ class DwhPreprocessPipeline:
)
self.current_physical = current_physical
self.current_lsh_dir = current_lsh_dir
self._snapshot_holder = None
self.after_publish = after_publish
if isinstance(retain_generations, bool) or retain_generations < 1:
raise ValueError("retain_generations must be positive")
@@ -528,11 +561,10 @@ class DwhPreprocessPipeline:
def _assert_active_binding(self, root_fd: int) -> None:
_validate_root_binding_fd(root_fd, self.binding)
active = _active_generation_dir_locked(
self.workspace_root, self.binding, root_fd=root_fd
)
active = _active_generation_fd(root_fd, self.binding)
if active is not None:
validate_generation(active, self.binding)
_, generation_fd = active
os.close(generation_fd)
def run(
self, steps: tuple[str, ...] = DWH_STAGE_IDS, *, resume_run_id: str | None = None
@@ -544,12 +576,17 @@ class DwhPreprocessPipeline:
_claim_or_validate_root_binding(lease.root_fd, self.binding)
lease.assert_root_identity(self.workspace_root / ".tht-dwh")
self._assert_active_binding(lease.root_fd)
active = _active_generation_dir_locked(
self.workspace_root, self.binding, root_fd=lease.root_fd
)
active = _active_generation_fd(lease.root_fd, self.binding)
if active is not None:
self.current_physical = active / "physical.yaml"
self.current_lsh_dir = active
generation, generation_fd = active
try:
self._snapshot_holder, snapshot_root = _materialize_generation_fd(
generation_fd, generation, self.binding
)
finally:
os.close(generation_fd)
self.current_physical = snapshot_root / "physical.yaml"
self.current_lsh_dir = snapshot_root
finally:
lease.close()
if resume_run_id is not None:
@@ -608,12 +645,22 @@ class DwhPreprocessPipeline:
)
if running is None:
return set()
target = self.workspace_root / ".tht-dwh" / "generations" / source.run_id
validate_generation(target, self.binding)
active = active_generation_dir(self.workspace_root, self.binding)
if active != target:
raise CorruptCheckpointError("sealed DWH publication is not ACTIVE")
self._validate_published(target, run_dir / "artifacts", running.artifact_files)
lease = _acquire_existing_generation_lock(self.workspace_root, exclusive=True)
try:
generations_fd = _open_generations_fd(lease.root_fd)
try:
self._validate_published_fd(
generations_fd, source.run_id,
run_dir / "artifacts", running.artifact_files,
)
active = _active_generation_fd(lease.root_fd, self.binding)
if active is None or active[0] != source.run_id:
raise CorruptCheckpointError("sealed DWH publication is not ACTIVE")
os.close(active[1])
finally:
os.close(generations_fd)
finally:
lease.close()
return {running.name}
def _validate_resume_publication(self, run_id: str) -> None:
@@ -622,23 +669,41 @@ class DwhPreprocessPipeline:
checkpoint = json.loads((run_dir / "checkpoint.json").read_text(encoding="utf-8"))
except (OSError, ValueError, TypeError) as error:
raise CorruptCheckpointError("checkpoint is invalid and cannot be resumed") from error
target = self.workspace_root / ".tht-dwh" / "generations" / run_id
if not target.exists():
if checkpoint.get("status") == "succeeded":
raise CorruptCheckpointError("published DWH generation is missing")
return
lease = _acquire_existing_generation_lock(self.workspace_root, exclusive=True)
try:
manifest = json.loads(
(run_dir / "artifacts" / "artifact-manifest.json").read_text(encoding="utf-8")
)
required = tuple(
name
for stage in manifest["stages"].values()
for name in stage["required"]
)
except (OSError, KeyError, ValueError, TypeError) as error:
raise CorruptCheckpointError("resume artifact manifest is invalid") from error
self._validate_published(target, run_dir / "artifacts", required)
try:
generations_fd = _open_generations_fd(lease.root_fd)
except FileNotFoundError:
if checkpoint.get("status") == "succeeded":
raise CorruptCheckpointError("published DWH generation is missing")
return
try:
if run_id not in os.listdir(generations_fd):
if checkpoint.get("status") == "succeeded":
raise CorruptCheckpointError("published DWH generation is missing")
return
try:
manifest = json.loads(
(run_dir / "artifacts" / "artifact-manifest.json").read_text(
encoding="utf-8"
)
)
required = tuple(
name
for stage in manifest["stages"].values()
for name in stage["required"]
)
except (OSError, KeyError, ValueError, TypeError) as error:
raise CorruptCheckpointError(
"resume artifact manifest is invalid"
) from error
self._validate_published_fd(
generations_fd, run_id, run_dir / "artifacts", required
)
finally:
os.close(generations_fd)
finally:
lease.close()
def _assert_no_legacy_artifacts(self, root_fd: int) -> None:
if OWNER_MARKER in os.listdir(root_fd):
@@ -692,119 +757,165 @@ class DwhPreprocessPipeline:
try:
_validate_root_binding_fd(lease.root_fd, self.binding)
self._assert_active_binding(lease.root_fd)
self._publish(context.run_id, artifacts, required)
self._publish(context.run_id, artifacts, required, lease.root_fd)
if self.after_publish is not None:
self.after_publish(context.run_id)
self._cleanup_generations()
self._cleanup_generations_fd(lease.root_fd)
finally:
lease.close()
return StageArtifacts(required)
def _publish(self, generation: str, artifacts: Path, required: tuple[str, ...]) -> None:
root = self.workspace_root / ".tht-dwh"
generations = root / "generations"
self._ensure_owned_dir(root)
self._ensure_owned_dir(generations)
target = generations / generation
if target.exists():
self._validate_published(target, artifacts, required)
else:
temporary = generations / f".{generation}.{uuid.uuid4().hex}.tmp"
temporary.mkdir(mode=0o700)
try:
for name in required:
destination = temporary / name
shutil.copyfile(artifacts / name, destination)
with destination.open("rb") as stream:
os.fsync(stream.fileno())
destination.chmod(0o400)
manifest = {
"schema_version": 1,
"generation": generation,
"files": {name: _digest(artifacts / name) for name in required},
"job_spec_fingerprint": json.loads(
(artifacts / "artifact-manifest.json").read_text(encoding="utf-8")
)["spec_fingerprint"],
"artifact_manifest_sha256": _digest(
artifacts / "artifact-manifest.json"
),
**self.binding,
}
manifest_path = temporary / GENERATION_MANIFEST
manifest_path.write_text(
json.dumps(manifest, sort_keys=True, separators=(",", ":")) + "\n",
encoding="utf-8",
)
manifest_path.chmod(0o400)
with manifest_path.open("rb") as stream:
os.fsync(stream.fileno())
self._fsync(temporary)
os.replace(temporary, target)
self._fsync(generations)
except BaseException:
shutil.rmtree(temporary, ignore_errors=True)
raise
pointer = root / "ACTIVE"
try:
previous = pointer.read_text(encoding="utf-8")
except FileNotFoundError:
previous = None
pointer_tmp = root / f".ACTIVE.{uuid.uuid4().hex}.tmp"
fd = os.open(pointer_tmp, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
try:
with os.fdopen(fd, "w", encoding="utf-8") as stream:
stream.write(generation + "\n")
stream.flush()
os.fsync(stream.fileno())
os.replace(pointer_tmp, pointer)
try:
self._fsync(root)
except BaseException:
self._restore_pointer(root, pointer, previous)
raise
except BaseException:
pointer_tmp.unlink(missing_ok=True)
raise
def _validate_published(
self, target: Path, artifacts: Path, required: tuple[str, ...],
def _publish(
self, generation: str, artifacts: Path, required: tuple[str, ...], root_fd: int
) -> 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(
artifacts / "artifact-manifest.json"
):
raise CorruptCheckpointError("published DWH job manifest digest mismatch")
for name in required:
source, published = artifacts / name, target / name
if published.is_symlink() or not published.is_file():
raise CorruptCheckpointError("published DWH artifact is invalid")
if hashlib.sha256(source.read_bytes()).digest() != hashlib.sha256(
published.read_bytes()
).digest():
raise CorruptCheckpointError("published DWH artifact digest mismatch")
generations_fd = _open_generations_fd(root_fd, create=True)
temporary = f".{generation}.{uuid.uuid4().hex}.tmp"
try:
if generation in os.listdir(generations_fd):
self._validate_published_fd(generations_fd, generation, artifacts, required)
else:
os.mkdir(temporary, 0o700, dir_fd=generations_fd)
temporary_fd = os.open(
temporary, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
dir_fd=generations_fd,
)
try:
for name in required:
self._write_readonly_at(
temporary_fd, name, (artifacts / name).read_bytes()
)
artifact_manifest = artifacts / "artifact-manifest.json"
manifest = {
"schema_version": 1,
"generation": generation,
"files": {name: _digest(artifacts / name) for name in required},
"job_spec_fingerprint": json.loads(
artifact_manifest.read_text(encoding="utf-8")
)["spec_fingerprint"],
"artifact_manifest_sha256": _digest(artifact_manifest),
**self.binding,
}
self._write_readonly_at(
temporary_fd, GENERATION_MANIFEST,
(json.dumps(manifest, sort_keys=True, separators=(",", ":")) + "\n").encode(),
)
os.fsync(temporary_fd)
finally:
os.close(temporary_fd)
os.rename(
temporary, generation,
src_dir_fd=generations_fd, dst_dir_fd=generations_fd,
)
temporary = ""
os.fsync(generations_fd)
previous = None
try:
previous = _read_owned_at(root_fd, "ACTIVE", readonly=False)
except FileNotFoundError:
pass
self._replace_active_at(root_fd, (generation + "\n").encode(), previous)
finally:
if temporary:
self._safe_delete_generation(generations_fd, temporary, allow_temporary=True)
os.close(generations_fd)
def _restore_pointer(self, root: Path, pointer: Path, previous: str | None) -> None:
@staticmethod
def _write_readonly_at(directory_fd: int, name: str, payload: bytes) -> None:
fd = os.open(
name, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW,
0o600, dir_fd=directory_fd,
)
try:
offset = 0
while offset < len(payload):
offset += os.write(fd, payload[offset:])
os.fsync(fd)
os.fchmod(fd, 0o400)
os.fsync(fd)
finally:
os.close(fd)
def _replace_active_at(self, root_fd: int, payload: bytes, previous: bytes | None) -> None:
temporary = f".ACTIVE.{uuid.uuid4().hex}.tmp"
fd = os.open(
temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW,
0o600, dir_fd=root_fd,
)
try:
offset = 0
while offset < len(payload):
offset += os.write(fd, payload[offset:])
os.fsync(fd)
finally:
os.close(fd)
try:
os.replace(temporary, "ACTIVE", src_dir_fd=root_fd, dst_dir_fd=root_fd)
temporary = ""
try:
os.fsync(root_fd)
except BaseException:
self._restore_pointer_at(root_fd, previous)
raise
finally:
if temporary:
try:
os.unlink(temporary, dir_fd=root_fd)
except FileNotFoundError:
pass
def _validate_published_fd(
self, generations_fd: int, generation: str,
artifacts: Path, required: tuple[str, ...],
) -> None:
generation_fd = _open_generation_fd(generations_fd, generation)
try:
manifest = validate_generation_fd(generation_fd, generation, self.binding)
if set(manifest["files"]) != set(required):
raise CorruptCheckpointError("published DWH generation is invalid")
artifact_digest = _digest(artifacts / "artifact-manifest.json")
if manifest["artifact_manifest_sha256"] != artifact_digest:
raise CorruptCheckpointError("published DWH job manifest digest mismatch")
for name in required:
if hashlib.sha256((artifacts / name).read_bytes()).hexdigest() != manifest["files"][name]:
raise CorruptCheckpointError("published DWH artifact digest mismatch")
finally:
os.close(generation_fd)
def _restore_pointer_at(self, root_fd: int, previous: bytes | None) -> None:
if previous is None:
pointer.unlink(missing_ok=True)
try:
os.unlink("ACTIVE", dir_fd=root_fd)
except FileNotFoundError:
pass
else:
restore = root / f".ACTIVE.restore.{uuid.uuid4().hex}.tmp"
restore.write_text(previous, encoding="utf-8")
with restore.open("rb") as stream:
os.fsync(stream.fileno())
os.replace(restore, pointer)
self._fsync(root)
restore = f".ACTIVE.restore.{uuid.uuid4().hex}.tmp"
fd = os.open(
restore, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW,
0o600, dir_fd=root_fd,
)
try:
offset = 0
while offset < len(previous):
offset += os.write(fd, previous[offset:])
os.fsync(fd)
finally:
os.close(fd)
os.replace(restore, "ACTIVE", src_dir_fd=root_fd, dst_dir_fd=root_fd)
os.fsync(root_fd)
def _cleanup_generations(self) -> None:
root = self.workspace_root / ".tht-dwh" / "generations"
if not root.exists():
return
_validate_root_binding(self.workspace_root, self.binding)
active = _active_generation_dir_locked(self.workspace_root, self.binding)
lease = _acquire_existing_generation_lock(self.workspace_root, exclusive=True)
try:
self._cleanup_generations_fd(lease.root_fd)
finally:
lease.close()
def _cleanup_generations_fd(self, root_fd: int) -> None:
_validate_root_binding_fd(root_fd, self.binding)
active = _active_generation_fd(root_fd, self.binding)
active_name = active[0] if active else None
if active is not None:
validate_generation(active, self.binding)
active_name = active.name if active else None
os.close(active[1])
protected = {active_name} if active_name else set()
runs = self.workspace_root / ".tht-jobs" / "dwh" / "runs"
for checkpoint in runs.glob("*/checkpoint.json") if runs.exists() else ():
@@ -815,14 +926,18 @@ class DwhPreprocessPipeline:
except (OSError, KeyError, ValueError):
continue
generations = []
root_fd = os.open(root, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
try:
for name in os.listdir(root_fd):
generations_fd = _open_generations_fd(root_fd)
except FileNotFoundError:
return
try:
for name in os.listdir(generations_fd):
if not _RUN_ID.fullmatch(name):
continue
try:
candidate_fd = os.open(
name, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=root_fd
name, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
dir_fd=generations_fd,
)
except OSError:
continue
@@ -843,13 +958,17 @@ class DwhPreprocessPipeline:
for _, name in generations:
if name in protected | keep_recent:
continue
self._safe_delete_generation(root_fd, name)
os.fsync(root_fd)
self._safe_delete_generation(generations_fd, name)
os.fsync(generations_fd)
finally:
os.close(root_fd)
os.close(generations_fd)
@staticmethod
def _safe_delete_generation(root_fd: int, name: str) -> None:
def _safe_delete_generation(
root_fd: int, name: str, *, allow_temporary: bool = False
) -> None:
if not (_RUN_ID.fullmatch(name) or (allow_temporary and name.startswith("."))):
return
try:
generation_fd = os.open(
name, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=root_fd
@@ -905,14 +1024,6 @@ class DwhPreprocessPipeline:
raise OSError("unsafe DWH publication directory")
path.chmod(0o700)
@staticmethod
def _fsync(path: Path) -> None:
fd = os.open(path, os.O_RDONLY | os.O_DIRECTORY)
try:
os.fsync(fd)
finally:
os.close(fd)
@staticmethod
def _validate_steps(steps: tuple[str, ...]) -> None:
if not steps or len(steps) != len(set(steps)) or any(