Files
codex-app-server/cloud/chv/locking.py
T

108 lines
4.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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()