"""Crash-safe interprocess locking scoped by workspace and job type.""" from __future__ import annotations import fcntl import hashlib import os import re from pathlib import Path from types import TracebackType class JobAlreadyRunningError(RuntimeError): pass _JOB_KEY = re.compile(r"^[a-z][a-z0-9_-]{0,63}$") def _lock_name(workspace_id: str, job_type: str) -> str: if not _JOB_KEY.fullmatch(workspace_id) or not _JOB_KEY.fullmatch(job_type): raise ValueError("lock identifiers must be lowercase filesystem-safe keys") workspace_key = hashlib.sha256(workspace_id.encode("utf-8")).hexdigest()[:16] return f"{workspace_key}-{job_type}.lock" class WorkspaceJobLock: """Advisory kernel lock; the inode remains stable and is never deleted by PID.""" def __init__(self, workspace_root: Path, workspace_id: str, job_type: str) -> None: self.path = workspace_root / ".tht-jobs" / ".locks" / _lock_name( workspace_id, job_type ) self._fd: int | None = None def acquire(self) -> "WorkspaceJobLock": if self._fd is not None: raise RuntimeError("job lock is already held by this object") self.path.parent.mkdir(parents=True, exist_ok=True) fd = os.open(self.path, os.O_RDWR | os.O_CREAT, 0o600) try: fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) except BlockingIOError as error: os.close(fd) raise JobAlreadyRunningError("this workspace job is already running") from error self._fd = fd return self def release(self) -> None: if self._fd is None: return fd, self._fd = self._fd, None try: fcntl.flock(fd, fcntl.LOCK_UN) finally: os.close(fd) def __enter__(self) -> "WorkspaceJobLock": return self.acquire() def __exit__( self, exc_type: type[BaseException] | None, exc: BaseException | None, traceback: TracebackType | None, ) -> None: self.release()