83 lines
2.7 KiB
Python
83 lines
2.7 KiB
Python
|
|
# ============================================================
|
|||
|
|
# 审计 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
|