mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
feat: server 版本管理,sidecar 记版本,交接时版本不匹配强制交接
This commit is contained in:
@@ -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 检查与陈旧清理。
|
||||
|
||||
@@ -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 应成功");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user