pm imported runtime_state's private helpers (_lock, _atomic_bytes, _bytes, _digest) at a dozen sites while runtime_state imports pm.environments at module top. The primitives are pm's: move them into the stdlib-only pm.filesystem as lock_fd, durable_write_bytes, read_bytes_or_none and file_digest, and repoint every pm caller. runtime_state keeps the private names only as import aliases: it still calls them through its own globals, and pre-PM updaters load them by these names mid-swap (tests/compat/old_updater_surface.json). Boot-subset test fixtures now copy pm/filesystem.py, since runtime_state imports it at process boot; worker-injection tests patch the name pm.publication now reads.
54 lines
1.8 KiB
Python
54 lines
1.8 KiB
Python
"""Cross-process ownership of resumable partials and their GC boundary."""
|
|
from __future__ import annotations
|
|
|
|
from contextlib import contextmanager
|
|
import os
|
|
from pathlib import Path
|
|
import time
|
|
|
|
from pm.filesystem import lock_fd
|
|
|
|
|
|
@contextmanager
|
|
def partial_lock(root: Path, key: str, *, cancelled=None, wait: bool = True):
|
|
# Lock inodes must survive release: removing one lets a new owner lock a
|
|
# different inode while an older waiter still holds the original handle.
|
|
locks = root / ".locks"
|
|
locks.mkdir(parents=True, exist_ok=True)
|
|
fd = os.open(locks / key, os.O_CREAT | os.O_RDWR, 0o600)
|
|
try:
|
|
while not lock_fd(fd, wait=False):
|
|
if not wait:
|
|
yield False
|
|
return
|
|
if cancelled is not None and cancelled():
|
|
yield False
|
|
return
|
|
time.sleep(0.05)
|
|
yield True
|
|
finally:
|
|
os.close(fd)
|
|
|
|
|
|
GC_GRACE_SECONDS = 6 * 60 * 60
|
|
|
|
|
|
def collect_partials(root: Path, *, grace_seconds: float = GC_GRACE_SECONDS) -> None:
|
|
"""Do not let a GC snapshot race a downloader acquiring ownership."""
|
|
if not root.is_dir():
|
|
return
|
|
keys = {path.stem for path in root.iterdir() if path.suffix in {".part", ".ranges"}}
|
|
now = time.time()
|
|
for key in sorted(keys):
|
|
with partial_lock(root, key, wait=False) as acquired:
|
|
if not acquired:
|
|
continue
|
|
pair = [root / f"{key}{suffix}" for suffix in (".part", ".ranges")]
|
|
try:
|
|
if any(path.exists() and grace_seconds > 0 and now - path.stat().st_mtime < grace_seconds for path in pair):
|
|
continue
|
|
for path in pair:
|
|
path.unlink(missing_ok=True)
|
|
except OSError:
|
|
continue # An inaccessible partial is not evidence it is unused.
|