"""CHV state_dir 跨进程锁(feat-chv-p1fix,P1-05)。 问题:容量 / net / 磁盘的 check→act 原来只靠进程内 threading.Lock 护 `_vms` dict;多 worker 进程共用同一 state_dir 时,两路 wake 可双双通过容量/磁盘/net 门并 双双建 VM(check-then-act 竞态),`_purge_dead_registry` 的无锁 pop 与持锁遍历并发还可致字典迭代异常。 解法:同一把锁护住 check→act 全程——文件锁(flock) + 进程内可重入锁的组合: - 文件锁:state_dir/chv-state.lock 上的 `LOCK_EX`。 不同进程(多 worker)靠它互斥;同一进程内不同 executor 实例(各有自己的 StateLock 对象)同样靠它 互斥,因为 flock 按打开的文件描述符区分,两个实例 各自 open → 第二个会阻塞,直到第一个释放。 - 进程内可重入:同一 executor 实例内,wake 全程会 嵌套调用同样需要锁的方法(如 wake → 失败时 stop → cleanup_stale_vms 等路径),因此 StateLock 自带 threading.RLock + 深度计数:同一线程重入时不再重复 flock,只在最外层加锁/释放,避免同一线程把自己锁死。 注意:同一线程经“不同 StateLock 实例”嵌套加锁不在 保证内——代码里不存在这条路径(wake/restore/cleanup/ reconcile 各自只用本实例或本 registry 的锁,且 reconcile 不在 wake 持锁期间被调用)。 持有范围(见 executor.py):wake_vm 全程(prechecks → prepare → seed → boot 含 SSH 就绪等待)、restore_vm 全程、 cleanup_stale_vms 全程(含 gc/密钥扫描/socket 扫描/purge)、 orphans.reconcile 全程。锁内串行化 wake 是有意的代价: 容量门只有“查完立即占位、占位前不放行”才成立;把锁 提前释放到 vm.create 之后会在 socket 尚未绑定前留出 计数窗口,仍可超限。串行 wake 不影响已运行 VM 的任务 执行(_run_guest 不持此锁)。 本模块只依赖标准库,是叶子模块;fcntl 只在 POSIX 上 可用——本执行器只面向 Linux 宿主(KVM 路径本身即 Linux 专属),非 POSIX 平台 import 即失败,不静默回落 纯线程锁(回落会把多 worker 保证悄悄降格)。 """ import fcntl import os import threading LOCK_FILENAME = "chv-state.lock" def lock_path_for_state_dir(state_dir: str) -> str: """state_dir 对应的锁文件路径。""" return os.path.join(state_dir, LOCK_FILENAME) class StateLock: """state_dir 跨进程 + 进程内可重入锁(上下文管理器)。 用法:`with executor_state_lock:`。acquire 时先拿进程内 RLock,再在深度为 0 时 open 锁文件并 flock LOCK_EX (阻塞,直到其它进程/实例释放);release 时深度归零才 解锁并关闭文件描述符。锁文件本身永不删除(删文件会 破坏 flock 的互斥语义:两个进程可能各持一个已删文件 的锁而互不感知)。 """ def __init__(self, state_dir: str) -> None: self.state_dir = state_dir self.lock_path = lock_path_for_state_dir(state_dir) self._rlock = threading.RLock() self._depth = 0 self._fd: int | None = None def acquire(self) -> None: self._rlock.acquire() try: if self._depth == 0: os.makedirs(self.state_dir, exist_ok=True) fd = os.open(self.lock_path, os.O_RDWR | os.O_CREAT, 0o600) try: fcntl.flock(fd, fcntl.LOCK_EX) except Exception: os.close(fd) raise self._fd = fd self._depth += 1 except Exception: self._rlock.release() raise def release(self) -> None: try: self._depth -= 1 if self._depth == 0 and self._fd is not None: try: fcntl.flock(self._fd, fcntl.LOCK_UN) finally: os.close(self._fd) self._fd = None finally: self._rlock.release() def __enter__(self) -> "StateLock": self.acquire() return self def __exit__(self, *exc_info: object) -> None: self.release()