108 lines
4.2 KiB
Python
108 lines
4.2 KiB
Python
"""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()
|