fix(chv): 生产级审核 P1-01~P1-10 修复(堆叠于 #270 之上)
This commit is contained in:
1 parent
8c8d62f70d
commit
971e054938
12 files changed
+1545
-120
No files matched your search
@@ -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()
|
||||
Reference in new issue
Block a user