fix(chv): 生产级审核 P1-01~P1-10 修复(堆叠于 #270 之上)

This commit is contained in:
muse-agent committed 2026-11-02 03:25:49 +00:00
1 parent 8c8d62f70d
commit 971e054938
12 files changed
+1545 -120

No files matched your search

+107
View File
@@ -0,0 +1,107 @@
"""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()