feat: server 版本管理,sidecar 记版本,交接时版本不匹配强制交接

This commit is contained in:
2026-10-06 17:12:32 +00:00
parent 5700888e24
commit 5b4f58d295
7 changed files with 144 additions and 100 deletions
Generated
+18 -18
View File
@@ -1160,7 +1160,7 @@ dependencies = [
[[package]]
name = "oxide_builtins"
version = "0.8.34"
version = "0.8.35"
dependencies = [
"chrono",
"fast-float",
@@ -1185,7 +1185,7 @@ dependencies = [
[[package]]
name = "oxide_bytecode"
version = "0.8.34"
version = "0.8.35"
dependencies = [
"num-bigint",
"oxide_log",
@@ -1193,7 +1193,7 @@ dependencies = [
[[package]]
name = "oxide_cfg"
version = "0.8.34"
version = "0.8.35"
dependencies = [
"oxide_bytecode",
"oxide_emit",
@@ -1204,7 +1204,7 @@ dependencies = [
[[package]]
name = "oxide_cli"
version = "0.8.34"
version = "0.8.35"
dependencies = [
"ansi_term",
"bumpalo",
@@ -1225,7 +1225,7 @@ dependencies = [
[[package]]
name = "oxide_code_cache"
version = "0.8.34"
version = "0.8.35"
dependencies = [
"lru",
"oxide_bytecode",
@@ -1234,7 +1234,7 @@ dependencies = [
[[package]]
name = "oxide_compiler"
version = "0.8.34"
version = "0.8.35"
dependencies = [
"oxide_bytecode",
"oxide_cfg",
@@ -1253,7 +1253,7 @@ dependencies = [
[[package]]
name = "oxide_dce"
version = "0.8.34"
version = "0.8.35"
dependencies = [
"oxide_bytecode",
"oxide_cfg",
@@ -1267,7 +1267,7 @@ dependencies = [
[[package]]
name = "oxide_emit"
version = "0.8.34"
version = "0.8.35"
dependencies = [
"num-bigint",
"oxc_allocator",
@@ -1282,7 +1282,7 @@ dependencies = [
[[package]]
name = "oxide_ir"
version = "0.8.34"
version = "0.8.35"
dependencies = [
"oxide_bytecode",
"oxide_log",
@@ -1291,7 +1291,7 @@ dependencies = [
[[package]]
name = "oxide_kernel"
version = "0.8.34"
version = "0.8.35"
dependencies = [
"bumpalo",
"dashmap",
@@ -1306,7 +1306,7 @@ dependencies = [
[[package]]
name = "oxide_liveness"
version = "0.8.34"
version = "0.8.35"
dependencies = [
"oxide_bytecode",
"oxide_cfg",
@@ -1318,7 +1318,7 @@ dependencies = [
[[package]]
name = "oxide_log"
version = "0.8.34"
version = "0.8.35"
dependencies = [
"tracing",
"tracing-appender",
@@ -1327,7 +1327,7 @@ dependencies = [
[[package]]
name = "oxide_parser"
version = "0.8.34"
version = "0.8.35"
dependencies = [
"oxc_allocator",
"oxc_ast",
@@ -1342,7 +1342,7 @@ dependencies = [
[[package]]
name = "oxide_regalloc"
version = "0.8.34"
version = "0.8.35"
dependencies = [
"oxide_bytecode",
"oxide_cfg",
@@ -1358,7 +1358,7 @@ dependencies = [
[[package]]
name = "oxide_runtime_api"
version = "0.8.34"
version = "0.8.35"
dependencies = [
"num-bigint",
"num-traits",
@@ -1370,7 +1370,7 @@ dependencies = [
[[package]]
name = "oxide_test262"
version = "0.8.34"
version = "0.8.35"
dependencies = [
"oxide_bytecode",
"oxide_compiler",
@@ -1387,7 +1387,7 @@ dependencies = [
[[package]]
name = "oxide_types"
version = "0.8.34"
version = "0.8.35"
dependencies = [
"bumpalo",
"hashbrown 0.14.5",
@@ -1399,7 +1399,7 @@ dependencies = [
[[package]]
name = "oxide_vm"
version = "0.8.34"
version = "0.8.35"
dependencies = [
"bumpalo",
"chrono",
+1 -1
View File
@@ -22,7 +22,7 @@ members = [
]
[workspace.package]
version = "0.8.34"
version = "0.8.35"
edition = "2021"
license = "MIT"
rust-version = "1.80"
+5 -4
View File
@@ -15,10 +15,11 @@ pub mod eval;
/// 协议数据面:NDJSON 帧格式与 serde 结构体。
pub mod protocol;
/// server 进程主体:accept 循环、控制请求分派、执行请求路由、信号处理
/// (SIGINT/SIGTERM 置位关闭标志)、交接协议(向旧 server 发 yield 请求使其
/// 排空退出、新 server 接管 socket 路径)、--rm 独立模式(进程唯一 socket
/// 路径、不注册 sidecar、不碰全局路径、单连接、空闲超时或断开即退出)
/// 与优雅退出。
/// (SIGINT/SIGTERM 置位关闭标志)、版本管理(sidecar 记构建版本,启动时
/// 与存活 server 版本比对:匹配不交接、不匹配强制交接)、交接协议(向旧
/// server 发 yield 请求使其排空退出、新 server 接管 socket 路径)、--rm
/// 独立模式(进程唯一 socket 路径、不注册 sidecar、不碰全局路径、单连接、
/// 空闲超时或断开即退出)与优雅退出。
#[allow(clippy::module_inception)]
pub mod server;
/// 身份注册:sidecar 文件、liveness 检查与陈旧清理。
+57 -46
View File
@@ -39,7 +39,7 @@ use super::workers::{self, WorkerRouter};
/// 信号处理器注册失败、交接失败。
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ServerError {
/// sidecar 认领未通过(已有存活 server 或存活证据不足以裁决)。
/// sidecar 认领未通过(已有同版本存活 server 或存活证据不足以裁决)。
ClaimRefused(ClaimResult),
/// socket 绑定失败(含删除 stale socket 文件重试一次后仍失败)。
BindFailed(String),
@@ -80,15 +80,19 @@ pub struct ServerConfig {
pub sidecar_path: PathBuf,
/// 常驻 worker 线程数;缺省取宿主核数,下限钳制为 1。
pub worker_count: usize,
/// 本进程构建版本,版本比对与 sidecar 写入用;生产默认取构建期版本。
pub version: String,
}
impl ServerConfig {
/// 生产默认配置:socket 与 sidecar 取 well-known 位置,worker 数取宿主核数。
/// 生产默认配置:socket 与 sidecar 取 well-known 位置,worker 数取宿主
/// 核数,版本取构建期版本。
pub fn well_known() -> Self {
ServerConfig {
socket_path: sidecar::well_known_socket_path(),
sidecar_path: sidecar::well_known_sidecar_path(),
worker_count: default_worker_count(),
version: env!("CARGO_PKG_VERSION").to_string(),
}
}
}
@@ -147,9 +151,9 @@ struct ServerContext {
/// drop 内核 → 先删 socket 文件后删 sidecar。
///
/// # 步骤
/// 1. 认领 sidecar(启动顺序契约:先写 sidecar 再 bind socket);已有存活
/// server 时转入交接路径(向旧 server 发 yield 请求,等旧 server 排空
/// 退出后重新认领)。
/// 1. 认领 sidecar(启动顺序契约:先写 sidecar 再 bind socket);已有同版本
/// 存活 server 时拒绝启动,版本不匹配时转入交接路径(向旧 server 发
/// yield 请求,等旧 server 排空退出后重新认领)。
/// 2. 注册信号处理器(绑定 socket 之前;失败回滚 sidecar,此时无存活线程)。
/// 3. 绑定监听器;失败且路径存在时删 stale socket 文件重试一次。
/// 4. 建内核(标准配置)与共享聚合计数器。
@@ -157,7 +161,9 @@ struct ServerContext {
/// 6. 进入 accept 循环;关闭标志置位后按关闭序列退出。
///
/// # 边界与前提
/// - 认领得 `AlreadyRunning` 时走交接路径,交接失败以 `YieldFailed` 返回。
/// - 认领得 `AlreadyRunning`(同版本)立即返回 `ClaimRefused`,不交接。
/// - 认领得 `VersionMismatch`(异版本)走交接路径,交接失败以 `YieldFailed`
/// 返回。
/// - 认领得 `RefusedAmbiguous` 立即返回错误,不双注册。
/// - 信号处理器注册失败(系统级错误)回滚 sidecar 并以错误返回。
/// - stale socket 文件删除重试后再失败时回滚删除 sidecar 并以错误返回。
@@ -174,10 +180,11 @@ struct ServerContext {
/// - 关闭序列固定:清空发送端 → join 全部 worker → drop 内核(内核 drop
/// 的调试断言要求存活 VM 数为零,worker 池随线程退出先释放,前提满足)。
pub fn run_server(config: &ServerConfig) -> Result<(), ServerError> {
// 认领 sidecar:已有存活 server 走交接路径,存活证据不足即返回错误。
match sidecar::claim_sidecar(&config.sidecar_path, &config.socket_path) {
// 认领 sidecar:同版本拒绝启动,异版本走交接路径,存活证据不足即返回错误。
match sidecar::claim_sidecar(&config.sidecar_path, &config.socket_path, &config.version) {
ClaimResult::Registered => {}
ClaimResult::AlreadyRunning => yield_and_takeover(config)?,
ClaimResult::AlreadyRunning => return Err(ServerError::ClaimRefused(ClaimResult::AlreadyRunning)),
ClaimResult::VersionMismatch => yield_and_takeover(config)?,
ClaimResult::RefusedAmbiguous => return Err(ServerError::ClaimRefused(ClaimResult::RefusedAmbiguous)),
}
@@ -421,8 +428,9 @@ fn finish_shutdown(
/// - 失败时不触碰旧 server 的文件(socket 与 sidecar),人工兜底归清理命令。
///
/// # 注意事项
/// - 纯函数式入口:版本管理路径在版本不匹配强制交接时调用同一入口。
fn yield_and_takeover(config: &ServerConfig) -> Result<(), ServerError> {
/// - 纯函数式入口:版本管理路径(版本不匹配强制交接)与显式交接路径调用
/// 同一入口。
pub fn yield_and_takeover(config: &ServerConfig) -> Result<(), ServerError> {
// 连接旧 server:连接成功是交接的前提。
let mut stream = UnixStream::connect(&config.socket_path)
.map_err(|e| ServerError::YieldFailed(format!("连接旧 server 失败:{e}")))?;
@@ -457,7 +465,7 @@ fn yield_and_takeover(config: &ServerConfig) -> Result<(), ServerError> {
}
// 重新认领:sidecar 已消失,O_EXCL 创建应成功;其余态判交接失败。
match sidecar::claim_sidecar(&config.sidecar_path, &config.socket_path) {
match sidecar::claim_sidecar(&config.sidecar_path, &config.socket_path, &config.version) {
ClaimResult::Registered => Ok(()),
other => Err(ServerError::YieldFailed(format!("交接后重新认领未通过:{other:?}"))),
}
@@ -724,6 +732,7 @@ mod tests {
socket_path: dir.join("server.sock"),
sidecar_path: dir.join("sidecar.json"),
worker_count: 2,
version: env!("CARGO_PKG_VERSION").to_string(),
}
}
@@ -938,10 +947,11 @@ mod tests {
stop_server(&mut stream, handle);
}
/// 同一连接连续两帧(状态后版本)逐帧应答;同路径第二实例走交接路径接管
/// (第一 server 正常退出,第二实例接管 socket 路径)。
/// 同一连接连续两帧(状态后版本)逐帧应答;同路径同版本第二实例拒绝启动
/// (认领判 AlreadyRunning,run_server 返回 ClaimRefused),第一 server
/// 继续正常应答且文件原样。
#[test]
fn multi_frame_same_connection_and_second_instance_takeover() {
fn multi_frame_same_connection_and_second_instance_refused() {
let config = unique_paths("multi");
let handle = start_server(config.clone());
let mut stream = connect(&config);
@@ -952,33 +962,28 @@ mod tests {
let second = send_and_recv(&mut stream, &ServerRequest::Version);
assert!(matches!(second, ServerResponse::Version { .. }), "应得 Version 帧:{second:?}");
// 关闭测试连接,让第一 server 的在途排水快速归零。
drop(stream);
// 同路径第二实例:sidecar 已存在且 socket 存活,走交接路径接管。
// 同路径同版本第二实例:认领判 AlreadyRunning,run_server 在有界时间内
// 返回 ClaimRefused。
let config_b = config.clone();
let handle_b = std::thread::spawn(move || run_server(&config_b));
// 第一 server 应在有界时间内正常退出(yield 受理后走既有退出序列)。
let deadline = Instant::now() + Duration::from_secs(30);
while !handle.is_finished() {
assert!(Instant::now() < deadline, "第一 server 应在 30 秒内退出");
while !handle_b.is_finished() {
assert!(Instant::now() < deadline, "第二实例应在 30 秒内返回");
std::thread::sleep(Duration::from_millis(10));
}
let result = handle.join().expect("第一 server 线程应正常退出");
assert!(result.is_ok(), "第一 server 交接后应正常退出:{result:?}");
let result_b = handle_b.join().expect("第二实例线程应正常退出");
assert!(
matches!(result_b, Err(ServerError::ClaimRefused(ClaimResult::AlreadyRunning))),
"同版本第二实例应拒绝启动:{result_b:?}"
);
// 第二实例已接管:等 socket 文件出现(第二实例的绑定),核对 sidecar 身份。
let deadline = Instant::now() + Duration::from_secs(30);
while !config.socket_path.exists() {
assert!(Instant::now() < deadline, "第二实例的 socket 文件应出现");
std::thread::sleep(Duration::from_millis(10));
}
let id = sidecar::read_identity(&config.sidecar_path).expect("第二实例的 sidecar 应可读");
assert_eq!(id.pid, std::process::id(), "第二实例的 sidecar 应为本进程身份");
// 第一 server 继续正常应答,文件原样。
let response = send_and_recv(&mut stream, &ServerRequest::Version);
assert!(matches!(response, ServerResponse::Version { .. }), "第一 server 应继续应答:{response:?}");
assert!(config.sidecar_path.exists(), "第一 server 的 sidecar 应原样");
assert!(config.socket_path.exists(), "第一 server 的 socket 应原样");
let mut stream_b = connect(&config);
stop_server(&mut stream_b, handle_b);
stop_server(&mut stream, handle);
}
/// 端到端并发:两条连接各发 3 个 eval,6 个响应帧全部正确且逐连接顺序与请求一致。
@@ -1129,16 +1134,19 @@ mod tests {
assert!(!config.socket_path.exists(), "socket 文件应被删除");
}
/// 交接端到端:server A 运行中,server B 以同配置启动经 yield 路径接管;
/// A 正常退出,B 的 sidecar 为本进程身份(O_EXCL 重新认领通过即证明 A 的
/// 文件已删除),B 可正常应答版本请求,随后以 Shutdown 停 B。
/// 交接端到端:server A(版本 0.0.1)运行中,server B(版本 0.0.2)以同
/// 路径异版本启动经 yield 路径接管;A 正常退出,B 的 sidecar 为本进程
/// 身份且版本 0.0.2(O_EXCL 重新认领通过即证明 A 的文件已删除),B 可
/// 正常应答版本请求,随后以 Shutdown 停 B。
#[test]
fn yield_takeover_end_to_end() {
let config = unique_paths("yield_e2e");
let handle_a = start_server(config.clone());
// A 用版本 0.0.1。
let config_a = ServerConfig { version: "0.0.1".to_string(), ..config.clone() };
let handle_a = start_server(config_a);
// B 以同配置启动:认领得 AlreadyRunning 转交接路径。
let config_b = config.clone();
// B 用版本 0.0.2:认领得 VersionMismatch 转交接路径。
let config_b = ServerConfig { version: "0.0.2".to_string(), ..config.clone() };
let handle_b = std::thread::spawn(move || run_server(&config_b));
// A 应在 30 秒内有界退出(排水加退出序列)。
@@ -1158,6 +1166,7 @@ mod tests {
}
let id = sidecar::read_identity(&config.sidecar_path).expect("B 的 sidecar 应可读");
assert_eq!(id.pid, std::process::id(), "B 的 sidecar 应为本进程身份");
assert_eq!(id.version, "0.0.2", "B 的 sidecar 版本应为 0.0.2");
// B 正常应答版本请求。
let mut stream = connect(&config);
@@ -1166,18 +1175,20 @@ mod tests {
stop_server(&mut stream, handle_b);
}
/// 旧 server 不受理交接:裸监听者加手写 sidecar 模拟旧 server,
/// 旧 server 不受理交接:裸监听者加手写 sidecar 模拟旧 server(sidecar
/// 版本 0.0.0 与本进程版本不同,认领经 VersionMismatch 走交接路径),
/// 接受连接后回 Error 帧(模拟旧版本 binary 无 yield 变体);
/// run_server 判 YieldFailed 且不触碰旧 server 的文件。
#[test]
fn yield_failed_when_old_server_refuses() {
let config = unique_paths("yield_refused");
// 模拟旧 server:绑定 socket 并写本进程身份 sidecar(PID 存活且启动
// 时刻刻度匹配,认领判 AlreadyRunning)。
// 模拟旧 server:绑定 socket 并写本进程身份加异版本(0.0.0)的
// sidecar(PID 存活且启动时刻刻度匹配,版本不匹配,认领判
// VersionMismatch)。
let listener = UnixListener::bind(&config.socket_path).expect("绑定模拟旧 server 应成功");
sidecar::write_exclusive(
&sidecar::ServerIdentity::new(config.socket_path.to_str().unwrap()),
&sidecar::ServerIdentity::new(config.socket_path.to_str().unwrap(), "0.0.0"),
&config.sidecar_path,
)
.expect("写模拟 sidecar 应成功");
@@ -1208,7 +1219,7 @@ mod tests {
let listener = UnixListener::bind(&config.socket_path).expect("绑定模拟旧 server 应成功");
sidecar::write_exclusive(
&sidecar::ServerIdentity::new(config.socket_path.to_str().unwrap()),
&sidecar::ServerIdentity::new(config.socket_path.to_str().unwrap(), "0.0.0"),
&config.sidecar_path,
)
.expect("写模拟 sidecar 应成功");
+61 -31
View File
@@ -40,15 +40,17 @@ impl ServerIdentity {
/// 构造当前进程的身份(pid、版本、启动时间、启动时刻刻度)。
///
/// # 边界与前提
/// - 版本由调用方注入(生产默认取构建期版本,测试注入可区分值),
/// 本函数不读取构建期版本。
/// - 非 Linux 宿主(`/proc` 缺位)时 `starttime` 填 0,交叉核对退化,
/// liveness 由 socket 探活裁决。
pub fn new(socket_path: impl AsRef<Path>) -> Self {
pub fn new(socket_path: impl AsRef<Path>, version: &str) -> Self {
let pid = std::process::id();
let started_at = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
ServerIdentity {
pid,
socket_path: socket_path.as_ref().to_string_lossy().into_owned(),
version: env!("CARGO_PKG_VERSION").to_string(),
version: version.to_string(),
started_at,
starttime: process_starttime(pid).unwrap_or(0),
}
@@ -144,13 +146,15 @@ pub fn process_starttime(pid: u32) -> Option<u64> {
rest.split_whitespace().nth(19)?.parse().ok()
}
/// 认领结果:注册入口的三态。
/// 认领结果:注册入口的四态。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClaimResult {
/// 新注册成功(sidecar 已写入)。
Registered,
/// 已有存活 server,拒绝注册。
/// 已有同版本存活 server,拒绝注册(旧 server 继续运行)。
AlreadyRunning,
/// 已有存活 server 且版本与本进程不同(含旧 sidecar 不可读),调用方应强制交接。
VersionMismatch,
/// 存活证据不足以裁决(进程存活但 socket 不在),保守拒绝并提示跑清理命令。
RefusedAmbiguous,
}
@@ -159,7 +163,9 @@ pub enum ClaimResult {
///
/// # 步骤
/// 1. 尝试 O_EXCL 创建;sidecar 不存在即成功。
/// 2. sidecar 已存在:探活 socket,连接成功即有存活 server,拒绝。
/// 2. sidecar 已存在:探活 socket,连接成功即有存活 server,读回旧 sidecar
/// 与本进程版本比对:匹配判 `AlreadyRunning`,不匹配或不可读判
/// `VersionMismatch`。
/// 3. socket 死:按 sidecar 可读性分支。
/// - 可解析且 PID 死:陈旧残留,删 sidecar 与 socket 文件后重建。
/// - 可解析且 PID 活:交叉核对启动时刻刻度,匹配则保守拒绝(server
@@ -168,6 +174,9 @@ pub enum ClaimResult {
///
/// # 边界与前提
/// - 启动顺序契约是「先写 sidecar 再 bind socket」,存活 server 必有 sidecar。
/// - 版本比对是精确字符串相等(同一性判定,不是新旧次序判定);旧 sidecar
/// 不可读(损坏、缺 version 字段)按不匹配处理,方向安全(强制交接失败
/// 不触碰旧 server 的文件,旧 server 继续运行)。
///
/// # 副作用
/// - 可能创建、删除、重写 sidecar 与 socket 文件。
@@ -175,9 +184,9 @@ pub enum ClaimResult {
/// # 注意事项
/// - 意外 io 错误(权限等)保守判 `RefusedAmbiguous`;方向安全(拒绝注册
/// 不会双注册)。
pub fn claim_sidecar(sidecar_path: &Path, socket_path: &Path) -> ClaimResult {
pub fn claim_sidecar(sidecar_path: &Path, socket_path: &Path, new_version: &str) -> ClaimResult {
// 先尝试 O_EXCL 创建:sidecar 不存在即成功。
match write_exclusive(&ServerIdentity::new(socket_path), sidecar_path) {
match write_exclusive(&ServerIdentity::new(socket_path, new_version), sidecar_path) {
Ok(()) => return ClaimResult::Registered,
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {}
Err(_) => return ClaimResult::RefusedAmbiguous,
@@ -185,27 +194,32 @@ pub fn claim_sidecar(sidecar_path: &Path, socket_path: &Path) -> ClaimResult {
// socket 探活是存活的唯一事实源:连接成功即有存活 server。
if is_server_alive(socket_path) {
return ClaimResult::AlreadyRunning;
// 读回旧 sidecar 与本进程版本比对:匹配拒绝启动,不匹配或不可读
// 交调用方强制交接。
match read_identity(sidecar_path) {
Some(id) if id.version == new_version => return ClaimResult::AlreadyRunning,
_ => return ClaimResult::VersionMismatch,
}
}
// socket 死:按 sidecar 可读性分支。
match read_identity(sidecar_path) {
Some(id) => claim_dead_socket(&id, sidecar_path, socket_path),
Some(id) => claim_dead_socket(&id, sidecar_path, socket_path, new_version),
None => {
// 损坏文件:删 sidecar 与 socket 文件后重建(socket 探活已失败,
// 文件无监听者,不会误删存活 server 的 socket)。
remove_stale(sidecar_path, socket_path);
re_register(sidecar_path, socket_path)
re_register(sidecar_path, socket_path, new_version)
}
}
}
/// socket 死且 sidecar 可解析:按 PID 与启动时刻刻度裁决陈旧性。
fn claim_dead_socket(id: &ServerIdentity, sidecar_path: &Path, socket_path: &Path) -> ClaimResult {
fn claim_dead_socket(id: &ServerIdentity, sidecar_path: &Path, socket_path: &Path, new_version: &str) -> ClaimResult {
// PID 死:陈旧残留,删 sidecar 与 socket 文件后重建。
if !pid_alive(id.pid) {
remove_stale(sidecar_path, socket_path);
return re_register(sidecar_path, socket_path);
return re_register(sidecar_path, socket_path, new_version);
}
// PID 活:交叉核对启动时刻刻度裁决 PID 复用。
@@ -216,7 +230,7 @@ fn claim_dead_socket(id: &ServerIdentity, sidecar_path: &Path, socket_path: &Pat
// 刻度失配:PID 已被复用,sidecar 是陈旧残留。
remove_stale(sidecar_path, socket_path);
re_register(sidecar_path, socket_path)
re_register(sidecar_path, socket_path, new_version)
}
/// 删除 sidecar 与 socket 文件(socket 探活已失败,文件无监听者,不会误删
@@ -227,8 +241,8 @@ fn remove_stale(sidecar_path: &Path, socket_path: &Path) {
}
/// 清理后重建 sidecar,重建失败保守拒绝。
fn re_register(sidecar_path: &Path, socket_path: &Path) -> ClaimResult {
match write_exclusive(&ServerIdentity::new(socket_path), sidecar_path) {
fn re_register(sidecar_path: &Path, socket_path: &Path, new_version: &str) -> ClaimResult {
match write_exclusive(&ServerIdentity::new(socket_path, new_version), sidecar_path) {
Ok(()) => ClaimResult::Registered,
Err(_) => ClaimResult::RefusedAmbiguous,
}
@@ -301,7 +315,7 @@ mod tests {
fn identity_roundtrip() {
let dir = TestDir::new();
let path = dir.path("sidecar.json");
let id = ServerIdentity::new("/tmp/oxide-test.sock");
let id = ServerIdentity::new("/tmp/oxide-test.sock", "0.0.0");
write_exclusive(&id, &path).expect("写 sidecar 应成功");
let read = read_identity(&path).expect("读回 sidecar 应成功");
assert_eq!(read, id, "五字段应逐字段一致");
@@ -312,7 +326,7 @@ mod tests {
fn write_exclusive_second_create_fails() {
let dir = TestDir::new();
let path = dir.path("sidecar.json");
let id = ServerIdentity::new("/tmp/oxide-test.sock");
let id = ServerIdentity::new("/tmp/oxide-test.sock", "0.0.0");
write_exclusive(&id, &path).expect("首次写入应成功");
let err = write_exclusive(&id, &path).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::AlreadyExists, "二次 create_new 必得 AlreadyExists");
@@ -324,22 +338,37 @@ mod tests {
let dir = TestDir::new();
let sidecar = dir.path("sidecar.json");
let socket = dir.path("server.sock");
assert_eq!(claim_sidecar(&sidecar, &socket), ClaimResult::Registered);
assert_eq!(claim_sidecar(&sidecar, &socket, "0.0.0"), ClaimResult::Registered);
let id = read_identity(&sidecar).expect("sidecar 应已写入");
assert_eq!(id.pid, std::process::id());
assert_eq!(id.socket_path, socket.to_str().unwrap());
}
/// 认领分支:存活 server(真实监听者绑 socket 路径)拒绝注册。
/// 认领分支:存活监听者加同版本 sidecar 判 AlreadyRunning(旧 server 继续
/// 运行,新 server 拒绝启动)。
#[test]
fn claim_live_server_refused() {
fn claim_live_server_same_version_refused() {
let dir = TestDir::new();
let sidecar = dir.path("sidecar.json");
let socket = dir.path("server.sock");
// 存活 server 必有 sidecar(启动顺序契约:先写 sidecar 再 bind socket)。
let listener = UnixListener::bind(&socket).expect("绑定监听者应成功");
write_exclusive(&ServerIdentity::new(socket.to_str().unwrap()), &sidecar).expect("写 sidecar 应成功");
assert_eq!(claim_sidecar(&sidecar, &socket), ClaimResult::AlreadyRunning);
write_exclusive(&ServerIdentity::new(socket.to_str().unwrap(), "0.0.0"), &sidecar).expect("写 sidecar 应成功");
assert_eq!(claim_sidecar(&sidecar, &socket, "0.0.0"), ClaimResult::AlreadyRunning);
drop(listener);
}
/// 认领分支:存活监听者加异版本 sidecar 判 VersionMismatch(调用方应强制
/// 交接)。
#[test]
fn claim_live_server_version_mismatch() {
let dir = TestDir::new();
let sidecar = dir.path("sidecar.json");
let socket = dir.path("server.sock");
let listener = UnixListener::bind(&socket).expect("绑定监听者应成功");
// 旧 sidecar 版本与本进程版本不同:比对判不匹配。
write_exclusive(&ServerIdentity::new(socket.to_str().unwrap(), "0.0.0"), &sidecar).expect("写 sidecar 应成功");
assert_eq!(claim_sidecar(&sidecar, &socket, "0.0.1"), ClaimResult::VersionMismatch);
drop(listener);
}
@@ -350,7 +379,7 @@ mod tests {
let sidecar = dir.path("sidecar.json");
let socket = dir.path("server.sock");
write_exclusive(&stale_identity(dead_pid(), &socket), &sidecar).expect("写陈旧 sidecar 应成功");
assert_eq!(claim_sidecar(&sidecar, &socket), ClaimResult::Registered);
assert_eq!(claim_sidecar(&sidecar, &socket, "0.0.0"), ClaimResult::Registered);
let id = read_identity(&sidecar).expect("重建 sidecar 应可读");
assert_eq!(id.pid, std::process::id(), "重建 sidecar 应为本进程身份");
}
@@ -363,7 +392,7 @@ mod tests {
let socket = dir.path("server.sock");
// PID 是本进程(存活),启动时刻刻度为占位值:PID 已复用。
write_exclusive(&stale_identity(std::process::id(), &socket), &sidecar).expect("写陈旧 sidecar 应成功");
assert_eq!(claim_sidecar(&sidecar, &socket), ClaimResult::Registered);
assert_eq!(claim_sidecar(&sidecar, &socket, "0.0.0"), ClaimResult::Registered);
}
/// 认领分支:存活 PID 加匹配启动时刻保守拒绝。
@@ -373,9 +402,9 @@ mod tests {
let sidecar = dir.path("sidecar.json");
let socket = dir.path("server.sock");
// PID 与启动时刻刻度都是本进程的:进程活且刻度匹配。
let id = ServerIdentity::new(socket.to_str().unwrap());
let id = ServerIdentity::new(socket.to_str().unwrap(), "0.0.0");
write_exclusive(&id, &sidecar).expect("写 sidecar 应成功");
assert_eq!(claim_sidecar(&sidecar, &socket), ClaimResult::RefusedAmbiguous);
assert_eq!(claim_sidecar(&sidecar, &socket, "0.0.0"), ClaimResult::RefusedAmbiguous);
}
/// 认领分支:损坏文件加死 socket 判陈旧,sidecar 重建。
@@ -386,7 +415,7 @@ mod tests {
let socket = dir.path("server.sock");
// 损坏文件:半写 JSON。
fs::write(&sidecar, r#"{"pid":1,"socket_path":""#).expect("写损坏文件应成功");
assert_eq!(claim_sidecar(&sidecar, &socket), ClaimResult::Registered);
assert_eq!(claim_sidecar(&sidecar, &socket, "0.0.0"), ClaimResult::Registered);
let id = read_identity(&sidecar).expect("重建 sidecar 应可读");
assert_eq!(id.pid, std::process::id());
}
@@ -401,21 +430,22 @@ mod tests {
fs::write(&sidecar, "not json at all").expect("写损坏文件应成功");
// 陈旧 socket 文件:文件存在但无监听者。
fs::write(&socket, b"stale").expect("写陈旧 socket 文件应成功");
assert_eq!(claim_sidecar(&sidecar, &socket), ClaimResult::Registered);
assert_eq!(claim_sidecar(&sidecar, &socket, "0.0.0"), ClaimResult::Registered);
let id = read_identity(&sidecar).expect("重建 sidecar 应可读");
assert_eq!(id.pid, std::process::id(), "重建 sidecar 应为本进程身份");
assert!(!socket.exists(), "陈旧 socket 文件应被删除");
}
/// 认领分支:损坏文件加存活 socket 拒绝(socket 是存活事实源)。
/// 认领分支:损坏文件加存活 socket 判 VersionMismatch(不可读按不匹配
/// 处理,调用方应强制交接,socket 是存活事实源)。
#[test]
fn claim_corrupted_sidecar_live_socket_refused() {
fn claim_corrupted_sidecar_live_socket_mismatch() {
let dir = TestDir::new();
let sidecar = dir.path("sidecar.json");
let socket = dir.path("server.sock");
let listener = UnixListener::bind(&socket).expect("绑定监听者应成功");
fs::write(&sidecar, "not json at all").expect("写损坏文件应成功");
assert_eq!(claim_sidecar(&sidecar, &socket), ClaimResult::AlreadyRunning);
assert_eq!(claim_sidecar(&sidecar, &socket, "0.0.0"), ClaimResult::VersionMismatch);
drop(listener);
}
@@ -23,6 +23,7 @@ fn unique_paths(test_name: &str) -> ServerConfig {
socket_path: dir.join("server.sock"),
sidecar_path: dir.join("sidecar.json"),
worker_count: 2,
version: env!("CARGO_PKG_VERSION").to_string(),
}
}
@@ -23,6 +23,7 @@ fn unique_paths(test_name: &str) -> ServerConfig {
socket_path: dir.join("server.sock"),
sidecar_path: dir.join("sidecar.json"),
worker_count: 2,
version: env!("CARGO_PKG_VERSION").to_string(),
}
}