# ============================================================ # 审计 append-only 文件锁辅助 v1(moduleId: core-audit-filelock, 可重生 ✅) # plan.md §3.6 / 矩阵「审计 append-only」剩余项:跨进程锁强化 # JSONL 追加写使用跨进程独占锁(Windows msvcrt;其他平台退化为进程内锁), # 防止多进程/多线程并发 append 行交错导致审计证据损坏;失败重试可配。 # ============================================================ from __future__ import annotations import os import threading import time from contextlib import contextmanager from pathlib import Path from typing import Any # 锁重试(秒):拿不到锁时轮询等待,避免瞬时竞争直接失败 _LOCK_RETRY_SECONDS = float(os.environ.get("APS_AUDIT_LOCK_RETRY_SECONDS") or "10.0") _LOCK_STEP = 0.02 try: # Windows:msvcrt 文件区域锁 import msvcrt _HAS_MSVCRT = True except ImportError: # 其他平台:仅进程内锁 _HAS_MSVCRT = False _lock_registry: dict[str, threading.Lock] = {} _lock_registry_guard = threading.Lock() def _process_lock_for(path: Path) -> threading.Lock: key = str(path) with _lock_registry_guard: lock = _lock_registry.get(key) if lock is None: lock = threading.Lock() _lock_registry[key] = lock return lock @contextmanager def locked_append(path: Path) -> Any: """跨进程安全的 JSONL 追加写上下文。 先取进程内锁(串行化同进程线程),再取跨进程文件锁(msvcrt 区域锁), 最后以追加模式打开文件并 yield file handle;退出时 flush+fsync。 """ proc = _process_lock_for(path) with proc: path.parent.mkdir(parents=True, exist_ok=True) with open(path, "a", encoding="utf-8") as fh: if _HAS_MSVCRT: _acquire_msvcrt(fh) try: yield fh finally: _release_msvcrt(fh) else: yield fh fh.flush() os.fsync(fh.fileno()) def _acquire_msvcrt(fh: Any) -> None: """对文件首字节区域加独占锁(重试至超时)。""" deadline = time.monotonic() + _LOCK_RETRY_SECONDS while True: try: fh.seek(0) msvcrt.locking(fh.fileno(), msvcrt.LK_LOCK, 1) return except OSError: if time.monotonic() >= deadline: raise time.sleep(_LOCK_STEP) def _release_msvcrt(fh: Any) -> None: try: fh.seek(0) msvcrt.locking(fh.fileno(), msvcrt.LK_UNLCK, 1) except OSError: pass