fix: server 测试跨二进制全局路径加 fcntl 文件锁串行

This commit is contained in:
2026-10-10 14:32:22 +00:00
parent 1633512d5f
commit 4d9c094b30
6 changed files with 132 additions and 35 deletions
+1
View File
@@ -22,6 +22,7 @@ oxide_engine = { path = "../oxide_engine" }
oxide_log = { path = "../oxide_log" }
bumpalo.workspace = true
clap.workspace = true
libc = "0.2"
ctrlc.workspace = true
rustyline.workspace = true
ansi_term.workspace = true
+3
View File
@@ -1317,6 +1317,9 @@ mod tests {
/// 正常退出,socket 文件被删除,全局 well-known 路径前后不变。
#[test]
fn rm_eval_then_eof_exits() {
// 跨进程文件锁:与写全局路径的集成测试二进制串行,保证前后不变断言
// 不被并发写污染。
let _file_lock = sidecar::WellKnownLock::acquire().expect("全局路径文件锁应可取");
let config = rm_config("eval_eof");
let well_known_sidecar = sidecar::well_known_sidecar_path();
let well_known_socket = sidecar::well_known_socket_path();
+74 -1
View File
@@ -11,8 +11,9 @@
//! - 日志文件由 sidecar 路径按扩展名派生(`.log`),与 sidecar、socket 同主名
//! 同目录;有意不随退出删除(跨重启的诊断工件),不在清理命令范围内。
use std::fs::{self, OpenOptions};
use std::fs::{self, File, OpenOptions};
use std::io::{self, Write};
use std::os::unix::io::AsRawFd;
use std::os::unix::net::UnixStream;
use std::path::{Path, PathBuf};
use std::process::Command;
@@ -93,6 +94,59 @@ pub fn well_known_log_path() -> PathBuf {
well_known_sidecar_path().with_extension("log")
}
/// well-known 全局路径排他锁文件:`$TMPDIR/oxide-<uid>.lock`(与 sidecar 同主名)。
pub fn well_known_lock_path() -> PathBuf {
well_known_sidecar_path().with_extension("lock")
}
/// well-known 全局路径排他锁:fcntl 文件锁,跨进程互斥。
///
/// 持锁期间保护 well-known 全局路径(sidecar、socket、日志)的读写;
/// 锁随文件描述符关闭(Drop)自动释放,进程消亡时内核一并释放。
///
/// # 边界与前提
/// - fcntl 锁以进程为单位:同进程多线程并发取锁不互斥,进程内串行化
/// 由调用方自己的进程内互斥负责。
///
/// # 副作用
/// - 创建锁文件(不存在时)。
///
/// # 注意事项
/// - 锁文件不随释放删除:删除会破坏他进程已打开描述符上的锁,互斥失效。
pub struct WellKnownLock {
/// 持有文件描述符:锁随描述符关闭(Drop)释放,字段存在即保活。
#[allow(dead_code)]
file: File,
}
impl WellKnownLock {
/// 取 well-known 全局路径排他锁(阻塞直至持有)。
///
/// # 步骤
/// 1. 打开锁文件(不存在则创建)。
/// 2. fcntl 阻塞取排他锁(F_SETLKW)。
pub fn acquire() -> io::Result<Self> {
let file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(well_known_lock_path())?;
// SAFETY: 文件描述符在 file 生命周期内有效,锁结构体按值传给内核只读。
let lock = libc::flock {
l_type: libc::F_WRLCK as _,
l_whence: libc::SEEK_SET as _,
l_start: 0,
l_len: 0,
l_pid: 0,
};
let rc = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_SETLKW, &lock) };
if rc < 0 {
return Err(io::Error::last_os_error());
}
Ok(WellKnownLock { file })
}
}
/// O_EXCL 原子创建 sidecar(文件已存在时创建失败)。
///
/// # 步骤
@@ -477,4 +531,23 @@ mod tests {
assert_eq!(log.file_stem(), sidecar.file_stem(), "日志与 sidecar 应同主名");
assert_eq!(log.parent(), sidecar.parent(), "日志与 sidecar 应同目录");
}
/// 锁路径:`.lock` 扩展名,与 sidecar 路径同主名同目录。
#[test]
fn well_known_lock_path_derives_from_sidecar() {
let lock = well_known_lock_path();
let sidecar = well_known_sidecar_path();
assert_eq!(lock.extension().and_then(|e| e.to_str()), Some("lock"), "锁路径应以 .lock 结尾");
assert_eq!(lock.file_stem(), sidecar.file_stem(), "锁与 sidecar 应同主名");
assert_eq!(lock.parent(), sidecar.parent(), "锁与 sidecar 应同目录");
}
/// 全局路径锁:取锁与释放后重取均成功(fcntl 锁以进程为单位,
/// 同进程重取不自我阻塞)。
#[test]
fn well_known_lock_acquire_release_reacquire() {
let lock = WellKnownLock::acquire().expect("取锁应成功");
drop(lock);
let _again = WellKnownLock::acquire().expect("释放后重取应成功");
}
}
+33 -14
View File
@@ -4,21 +4,40 @@
//!
//! start / restart / watchdog 端到端与 cleanup 走 well-known 全局路径
//! (每用户单例):测试先探活,socket 存活即 panic 不抢占存活 server;
//! 触碰全局路径的测试经同一把锁串行,避免相互干扰。
//! 触碰全局路径的测试经双重锁(进程内互斥加跨进程文件锁)串行,避免相互
//! 干扰(含与单测二进制的并行干扰)。
use std::io::{BufRead, BufReader, Write};
use std::os::unix::net::UnixStream;
use std::process::{Command, Output};
use std::sync::Mutex;
use std::sync::{Mutex, MutexGuard};
use std::time::{Duration, Instant};
use oxide_cli::server::client::send_control_request;
use oxide_cli::server::protocol::{self, FrameReader, ServerRequest, ServerResponse};
use oxide_cli::server::sidecar;
/// well-known 全局路径互斥锁:同测试二进制内触碰全局路径的测试串行执行。
/// well-known 全局路径进程内互斥锁:同测试二进制内触碰全局路径的测试串行
/// 执行(fcntl 锁以进程为单位,不覆盖同进程多线程)。
static WELL_KNOWN_LOCK: Mutex<()> = Mutex::new(());
/// well-known 全局路径双重锁守卫:进程内互斥(同二进制线程串行)加跨进程
/// fcntl 文件锁(nextest 并行独立进程串行)。
struct WellKnownGuard {
_in_process: MutexGuard<'static, ()>,
_cross_process: sidecar::WellKnownLock,
}
/// 取 well-known 全局路径双重锁,守卫随 drop 释放。
fn lock_well_known() -> WellKnownGuard {
let in_process = WELL_KNOWN_LOCK.lock().expect("全局路径锁不应中毒");
let cross_process = sidecar::WellKnownLock::acquire().expect("全局路径文件锁应可取");
WellKnownGuard {
_in_process: in_process,
_cross_process: cross_process,
}
}
/// 运行 oxide 二进制并返回输出。
fn oxide(args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_oxide"))
@@ -93,7 +112,7 @@ fn server_help_lists_subcommands() {
/// well-known 日志文件(无存活 server 时删除安全)。
#[test]
fn server_log_no_file_exit_1() {
let _guard = WELL_KNOWN_LOCK.lock().expect("全局路径锁不应中毒");
let _guard = lock_well_known();
assert!(
!sidecar::is_server_alive(&sidecar::well_known_socket_path()),
"存活 server 占用 well-known 路径,测试不抢占"
@@ -113,7 +132,7 @@ fn server_log_no_file_exit_1() {
/// 走 well-known 全局路径:先探活,存活 server 占用时 panic 不抢占。
#[test]
fn server_control_arms_no_server_exit_1() {
let _guard = WELL_KNOWN_LOCK.lock().expect("全局路径锁不应中毒");
let _guard = lock_well_known();
assert!(
!sidecar::is_server_alive(&sidecar::well_known_socket_path()),
"存活 server 占用 well-known 路径,测试不抢占"
@@ -132,7 +151,7 @@ fn server_control_arms_no_server_exit_1() {
/// 走 well-known 全局路径:先探活,存活 server 占用时 panic 不抢占。
#[test]
fn server_forge_no_server_exit_1() {
let _guard = WELL_KNOWN_LOCK.lock().expect("全局路径锁不应中毒");
let _guard = lock_well_known();
assert!(
!sidecar::is_server_alive(&sidecar::well_known_socket_path()),
"存活 server 占用 well-known 路径,测试不抢占"
@@ -147,7 +166,7 @@ fn server_forge_no_server_exit_1() {
/// cleanup 退 0(幂等:有残留则清理、无残留则报无残留),事后全局路径无文件。
#[test]
fn server_cleanup_exit_0() {
let _guard = WELL_KNOWN_LOCK.lock().expect("全局路径锁不应中毒");
let _guard = lock_well_known();
assert!(
!sidecar::is_server_alive(&sidecar::well_known_socket_path()),
"存活 server 占用 well-known 路径,测试不抢占"
@@ -164,7 +183,7 @@ fn server_cleanup_exit_0() {
/// 进程退出,socket 与 sidecar 文件被删。
#[test]
fn server_start_e2e() {
let _guard = WELL_KNOWN_LOCK.lock().expect("全局路径锁不应中毒");
let _guard = lock_well_known();
let socket = sidecar::well_known_socket_path();
let sidecar_path = sidecar::well_known_sidecar_path();
@@ -213,7 +232,7 @@ fn server_start_e2e() {
/// server 即通过)。
#[test]
fn server_start_when_running() {
let _guard = WELL_KNOWN_LOCK.lock().expect("全局路径锁不应中毒");
let _guard = lock_well_known();
let socket = sidecar::well_known_socket_path();
let sidecar_path = sidecar::well_known_sidecar_path();
@@ -251,7 +270,7 @@ fn server_start_when_running() {
/// 进程号不同且版本为构建期版本,新 server 健康与版本请求正常,收尾关闭后文件被删。
#[test]
fn server_restart_e2e() {
let _guard = WELL_KNOWN_LOCK.lock().expect("全局路径锁不应中毒");
let _guard = lock_well_known();
let socket = sidecar::well_known_socket_path();
let sidecar_path = sidecar::well_known_sidecar_path();
@@ -331,7 +350,7 @@ fn server_restart_e2e() {
/// (Info 级无 ERROR 行),关闭后日志文件有意保留。
#[test]
fn server_log_e2e() {
let _guard = WELL_KNOWN_LOCK.lock().expect("全局路径锁不应中毒");
let _guard = lock_well_known();
let socket = sidecar::well_known_socket_path();
let sidecar_path = sidecar::well_known_sidecar_path();
let log_path = sidecar::well_known_log_path();
@@ -412,7 +431,7 @@ fn server_log_e2e() {
/// 打印 gc 与清缓存行;关闭后 socket 与 sidecar 文件被删。
#[test]
fn server_forge_e2e() {
let _guard = WELL_KNOWN_LOCK.lock().expect("全局路径锁不应中毒");
let _guard = lock_well_known();
let socket = sidecar::well_known_socket_path();
let sidecar_path = sidecar::well_known_sidecar_path();
@@ -471,7 +490,7 @@ fn server_forge_e2e() {
/// 关闭后 watchdog 退 0 且 stdout 含崩溃提示与日志尾部。
#[test]
fn server_watchdog_e2e() {
let _guard = WELL_KNOWN_LOCK.lock().expect("全局路径锁不应中毒");
let _guard = lock_well_known();
let socket = sidecar::well_known_socket_path();
let sidecar_path = sidecar::well_known_sidecar_path();
@@ -574,7 +593,7 @@ fn server_watchdog_e2e() {
/// sidecar 消失、server 进程已退出(停 watchdog 即优雅停 server)。
#[test]
fn server_watchdog_sigint_stops_server() {
let _guard = WELL_KNOWN_LOCK.lock().expect("全局路径锁不应中毒");
let _guard = lock_well_known();
let socket = sidecar::well_known_socket_path();
let sidecar_path = sidecar::well_known_sidecar_path();