"""Idempotent per-format updates to out/bench/storage_sizes.csv. Contract (docs/rules/build-pipeline-tasks.md section 4): columns format,variant,bytes; a task re-run REPLACES its own format's rows and never touches other formats' rows. """ from __future__ import annotations import logging import os import time from pathlib import Path logger = logging.getLogger(__name__) HEADER = "format,variant,bytes" LOCK_TIMEOUT_S = 30.0 # converters 04-07 may run in parallel; the update is a LOCK_POLL_S = 0.2 # read-modify-write, so it is serialized via a lock file def update_storage_sizes(bench_dir: Path, fmt: str, variants: list[tuple[str, int]]) -> Path: bench_dir.mkdir(parents=True, exist_ok=True) path = bench_dir / "storage_sizes.csv" lock = bench_dir / "storage_sizes.lock" fd = _acquire(lock) try: kept: list[str] = [] if path.exists(): lines = path.read_text(encoding="ascii").splitlines() if lines and lines[0] != HEADER: raise ValueError(f"{path}: unexpected header {lines[0]!r}") kept = [ln for ln in lines[1:] if ln and ln.split(",", 1)[0] != fmt] rows = kept + [f"{fmt},{variant},{nbytes}" for variant, nbytes in variants] with open(path, "w", encoding="ascii", newline="\n") as fh: fh.write(HEADER + "\n" + "\n".join(rows) + "\n") finally: os.close(fd) lock.unlink() logger.info("storage_sizes.csv: %s -> %s", fmt, ", ".join(f"{v}={b}" for v, b in variants)) return path def _acquire(lock: Path) -> int: deadline = time.monotonic() + LOCK_TIMEOUT_S while True: try: return os.open(lock, os.O_CREAT | os.O_EXCL | os.O_WRONLY) except FileExistsError: if time.monotonic() > deadline: raise TimeoutError(f"storage_sizes lock held too long: {lock}") time.sleep(LOCK_POLL_S)