feat: 客户端与探活三函数按 endpoint 形态分支 Unix 与 TCP 两臂

This commit is contained in:
2026-10-11 13:07:29 +00:00
parent d6fa71759b
commit 9d2790e036
9 changed files with 149 additions and 78 deletions
Generated
+20 -20
View File
@@ -1160,7 +1160,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_builtins" name = "oxide_builtins"
version = "0.15.5" version = "0.15.6"
dependencies = [ dependencies = [
"chrono", "chrono",
"fast-float", "fast-float",
@@ -1185,7 +1185,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_bytecode" name = "oxide_bytecode"
version = "0.15.5" version = "0.15.6"
dependencies = [ dependencies = [
"num-bigint", "num-bigint",
"oxide_log", "oxide_log",
@@ -1193,7 +1193,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_cfg" name = "oxide_cfg"
version = "0.15.5" version = "0.15.6"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_emit", "oxide_emit",
@@ -1204,7 +1204,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_cli" name = "oxide_cli"
version = "0.15.5" version = "0.15.6"
dependencies = [ dependencies = [
"ansi_term", "ansi_term",
"bumpalo", "bumpalo",
@@ -1228,7 +1228,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_code_cache" name = "oxide_code_cache"
version = "0.15.5" version = "0.15.6"
dependencies = [ dependencies = [
"lru", "lru",
"oxide_bytecode", "oxide_bytecode",
@@ -1237,7 +1237,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_compiler" name = "oxide_compiler"
version = "0.15.5" version = "0.15.6"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_cfg", "oxide_cfg",
@@ -1257,7 +1257,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_dce" name = "oxide_dce"
version = "0.15.5" version = "0.15.6"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_cfg", "oxide_cfg",
@@ -1271,7 +1271,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_emit" name = "oxide_emit"
version = "0.15.5" version = "0.15.6"
dependencies = [ dependencies = [
"num-bigint", "num-bigint",
"oxc_allocator", "oxc_allocator",
@@ -1286,7 +1286,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_engine" name = "oxide_engine"
version = "0.15.5" version = "0.15.6"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_compiler", "oxide_compiler",
@@ -1298,7 +1298,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_ir" name = "oxide_ir"
version = "0.15.5" version = "0.15.6"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_log", "oxide_log",
@@ -1307,7 +1307,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_kernel" name = "oxide_kernel"
version = "0.15.5" version = "0.15.6"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"dashmap", "dashmap",
@@ -1321,7 +1321,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_liveness" name = "oxide_liveness"
version = "0.15.5" version = "0.15.6"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_cfg", "oxide_cfg",
@@ -1333,7 +1333,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_log" name = "oxide_log"
version = "0.15.5" version = "0.15.6"
dependencies = [ dependencies = [
"tracing", "tracing",
"tracing-appender", "tracing-appender",
@@ -1342,11 +1342,11 @@ dependencies = [
[[package]] [[package]]
name = "oxide_net" name = "oxide_net"
version = "0.15.5" version = "0.15.6"
[[package]] [[package]]
name = "oxide_parser" name = "oxide_parser"
version = "0.15.5" version = "0.15.6"
dependencies = [ dependencies = [
"oxc_allocator", "oxc_allocator",
"oxc_ast", "oxc_ast",
@@ -1361,7 +1361,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_regalloc" name = "oxide_regalloc"
version = "0.15.5" version = "0.15.6"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_cfg", "oxide_cfg",
@@ -1377,7 +1377,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_runtime_api" name = "oxide_runtime_api"
version = "0.15.5" version = "0.15.6"
dependencies = [ dependencies = [
"num-bigint", "num-bigint",
"num-traits", "num-traits",
@@ -1390,7 +1390,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_test262" name = "oxide_test262"
version = "0.15.5" version = "0.15.6"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_compiler", "oxide_compiler",
@@ -1408,7 +1408,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_types" name = "oxide_types"
version = "0.15.5" version = "0.15.6"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"dashmap", "dashmap",
@@ -1421,7 +1421,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_vm" name = "oxide_vm"
version = "0.15.5" version = "0.15.6"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"chrono", "chrono",
+1 -1
View File
@@ -24,7 +24,7 @@ members = [
] ]
[workspace.package] [workspace.package]
version = "0.15.5" version = "0.15.6"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
rust-version = "1.80" rust-version = "1.80"
+3 -1
View File
@@ -23,6 +23,8 @@ use std::path::Path;
use std::process::Command; use std::process::Command;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use oxide_net::Endpoint;
use super::sidecar::{self, ServerIdentity}; use super::sidecar::{self, ServerIdentity};
/// 缺省杀进程等待:10 秒,长于 server 的在途排水安全超时 5 秒,正常优雅 /// 缺省杀进程等待:10 秒,长于 server 的在途排水安全超时 5 秒,正常优雅
@@ -151,7 +153,7 @@ pub fn cleanup_with_wait(sidecar_path: &Path, socket_path: &Path, kill_wait: Dur
// PID 存活与刻度匹配(PID 死时匹配位恒假)。 // PID 存活与刻度匹配(PID 死时匹配位恒假)。
let identity = sidecar::read_identity(sidecar_path); let identity = sidecar::read_identity(sidecar_path);
let socket_exists = socket_path.exists(); let socket_exists = socket_path.exists();
let socket_alive = socket_exists && sidecar::is_server_alive(socket_path); let socket_alive = socket_exists && sidecar::is_server_alive(&Endpoint::Unix(socket_path.to_path_buf()));
let (pid_alive, starttime_match) = match &identity { let (pid_alive, starttime_match) = match &identity {
Some(id) => { Some(id) => {
let alive = sidecar::pid_alive(id.pid); let alive = sidecar::pid_alive(id.pid);
+19 -11
View File
@@ -8,12 +8,15 @@
//! - 错误三分支:连接失败、协议错误、server 错误帧,各映射到可读消息, //! - 错误三分支:连接失败、协议错误、server 错误帧,各映射到可读消息,
//! 不 panic。 //! 不 panic。
use std::io::{BufReader, Write}; use std::io::BufReader;
use std::net::TcpStream;
use std::os::unix::net::UnixStream; use std::os::unix::net::UnixStream;
use std::path::Path; use std::path::{Path, PathBuf};
use std::sync::mpsc::RecvTimeoutError; use std::sync::mpsc::RecvTimeoutError;
use std::time::Duration; use std::time::Duration;
use oxide_net::{Conn, Endpoint};
use super::protocol::{self, FrameReader, ProtocolError, ServerRequest, ServerResponse}; use super::protocol::{self, FrameReader, ProtocolError, ServerRequest, ServerResponse};
use super::sidecar; use super::sidecar;
@@ -76,7 +79,7 @@ pub fn send_control_request_from(sidecar_path: &Path, request: &ServerRequest) -
})?; })?;
// 连接:5 秒超时是安全网,本地 socket 连接立即完成或立即失败。 // 连接:5 秒超时是安全网,本地 socket 连接立即完成或立即失败。
let mut stream = connect_with_timeout(Path::new(&identity.socket_path), CONNECT_TIMEOUT)?; let mut stream = connect_with_timeout(&Endpoint::Unix(PathBuf::from(identity.socket_path)), CONNECT_TIMEOUT)?;
// 读写超时各 5 秒:server 挂起(不回应)时不无限阻塞。 // 读写超时各 5 秒:server 挂起(不回应)时不无限阻塞。
let _ = stream.set_read_timeout(Some(CONNECT_TIMEOUT)); let _ = stream.set_read_timeout(Some(CONNECT_TIMEOUT));
@@ -102,29 +105,33 @@ pub fn send_control_request_from(sidecar_path: &Path, request: &ServerRequest) -
} }
} }
/// 带超时的连接:线程内连接、通道上带超时取结果。 /// 带超时的连接:线程内连接、通道上带超时取结果,两臂按 endpoint 形态分支。
/// ///
/// # 步骤 /// # 步骤
/// 1. 派生线程连接 socket。 /// 1. 派生线程连接 endpoint(Unix 路径或 TCP 地址)。
/// 2. 通道上带超时取结果;超时判连接失败。 /// 2. 通道上带超时取结果;超时判连接失败。
/// ///
/// # 边界与前提 /// # 边界与前提
/// - 超时后连接线程继续存活到连接完成或失败(本地 socket 立即出结果), /// - 超时后连接线程继续存活到连接完成或失败(本地 socket 立即出结果),
/// 发送端已被接收端丢弃,线程随即退出,无泄漏。 /// 发送端已被接收端丢弃,线程随即退出,无泄漏。
fn connect_with_timeout(path: &Path, timeout: Duration) -> Result<UnixStream, ClientError> { fn connect_with_timeout(endpoint: &Endpoint, timeout: Duration) -> Result<Conn, ClientError> {
let (tx, rx) = std::sync::mpsc::channel(); let (tx, rx) = std::sync::mpsc::channel();
let spawn_path = path.to_path_buf(); let spawn = endpoint.clone();
let display = path.display().to_string(); let display = endpoint.describe();
std::thread::spawn(move || { std::thread::spawn(move || {
let _ = tx.send(UnixStream::connect(&spawn_path)); let result = match &spawn {
Endpoint::Unix(path) => UnixStream::connect(path).map(Conn::Unix),
Endpoint::Tcp(addr) => TcpStream::connect(*addr).map(Conn::Tcp),
};
let _ = tx.send(result);
}); });
match rx.recv_timeout(timeout) { match rx.recv_timeout(timeout) {
Ok(Ok(stream)) => Ok(stream), Ok(Ok(stream)) => Ok(stream),
Ok(Err(e)) => Err(ClientError::Connect(format!( Ok(Err(e)) => Err(ClientError::Connect(format!(
"连接 server 失败:{e}(socket 路径 {display},可用 `oxide server cleanup` 清理陈旧文件)" "连接 server 失败:{e}(endpoint {display},可用 `oxide server cleanup` 清理陈旧文件)"
))), ))),
Err(RecvTimeoutError::Timeout) => Err(ClientError::Connect(format!( Err(RecvTimeoutError::Timeout) => Err(ClientError::Connect(format!(
"连接 server 超时({} 秒),socket 路径 {display}", "连接 server 超时({} 秒),endpoint {display}",
timeout.as_secs() timeout.as_secs()
))), ))),
Err(RecvTimeoutError::Disconnected) => Err(ClientError::Connect("连接线程异常退出".into())), Err(RecvTimeoutError::Disconnected) => Err(ClientError::Connect("连接线程异常退出".into())),
@@ -135,6 +142,7 @@ fn connect_with_timeout(path: &Path, timeout: Duration) -> Result<UnixStream, Cl
mod tests { mod tests {
use super::*; use super::*;
use std::fs; use std::fs;
use std::io::Write;
use std::os::unix::net::UnixListener; use std::os::unix::net::UnixListener;
use std::path::PathBuf; use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
+3 -1
View File
@@ -21,6 +21,8 @@
use std::path::Path; use std::path::Path;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use oxide_net::Endpoint;
use super::cleanup::{self, Decision}; use super::cleanup::{self, Decision};
use super::sidecar; use super::sidecar;
@@ -92,7 +94,7 @@ pub fn liveness_scan_with_waits(
// PID 存活与刻度匹配(PID 死时匹配位恒假)。 // PID 存活与刻度匹配(PID 死时匹配位恒假)。
let identity = sidecar::read_identity(sidecar_path); let identity = sidecar::read_identity(sidecar_path);
let socket_exists = socket_path.exists(); let socket_exists = socket_path.exists();
let socket_alive = socket_exists && sidecar::is_server_alive(socket_path); let socket_alive = socket_exists && sidecar::is_server_alive(&Endpoint::Unix(socket_path.to_path_buf()));
let (pid_alive, starttime_match) = match &identity { let (pid_alive, starttime_match) = match &identity {
Some(id) => { Some(id) => {
let alive = sidecar::pid_alive(id.pid); let alive = sidecar::pid_alive(id.pid);
+75 -29
View File
@@ -21,8 +21,7 @@
use std::fs; use std::fs;
use std::io; use std::io;
use std::io::Write; use std::net::{TcpListener, TcpStream};
use std::net::TcpListener;
use std::os::unix::net::{UnixListener, UnixStream}; use std::os::unix::net::{UnixListener, UnixStream};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
@@ -456,19 +455,23 @@ fn finish_shutdown(
/// 交接接管:连接旧 server、发 yield 请求、等旧 server 排空退出后重新认领。 /// 交接接管:连接旧 server、发 yield 请求、等旧 server 排空退出后重新认领。
/// ///
/// # 步骤 /// # 步骤
/// 1. 连接旧 server 的 socket;连接失败判 `YieldFailed`。 /// 1. 连接旧 server 的 endpoint(Unix 路径或 TCP 地址);连接失败判
/// `YieldFailed`。
/// 2. 设 5 秒读超时,写 yield 请求帧。 /// 2. 设 5 秒读超时,写 yield 请求帧。
/// 3. 读一帧应答:读超时(旧 server 挂起)或应答非 yield 确认(旧版本 /// 3. 读一帧应答:读超时(旧 server 挂起)或应答非 yield 确认(旧版本
/// binary 无该变体时以错误帧答复未知类型)判 `YieldFailed`。 /// binary 无该变体时以错误帧答复未知类型)判 `YieldFailed`。
/// 4. 关闭连接,轮询 sidecar 文件消失(50 毫秒间隔、10 秒上限);超时判 /// 4. 关闭连接,按 endpoint 形态等旧 server 退出:Unix 臂轮询 sidecar 文件
/// `YieldFailed`。 /// 消失,TCP 臂以端口释放(连接被拒)为退出信号;均 50 毫秒间隔、
/// 5. 重新认领 sidecar;得 `Registered` 即接管成功,其余态判 `YieldFailed`。 /// 10 秒上限,超时判 `YieldFailed`。
/// 5. Unix 臂重新认领 sidecar;得 `Registered` 即接管成功,其余态判
/// `YieldFailed`。TCP 臂无 sidecar 文件,不重认领。
/// ///
/// # 边界与前提 /// # 边界与前提
/// - 交接等待 10 秒长于旧 server 的排水安全超时 5 秒,正常路径必然先于 /// - 交接等待 10 秒长于旧 server 的排水安全超时 5 秒,正常路径必然先于
/// 等待上限完成。 /// 等待上限完成。
/// - 旧 server 先删 socket 文件后删 sidecar,sidecar 消失时 socket 文件必 /// - Unix 臂:旧 server 先删 socket 文件后删 sidecar,sidecar 消失时
/// 已删除,接管绑定无竞态。 /// socket 文件必已删除,接管绑定无竞态。
/// - TCP 臂:监听者 drop 即端口释放,连接被拒即旧 server 已退出。
/// ///
/// # 副作用 /// # 副作用
/// - 失败时不触碰旧 server 的文件(socket 与 sidecar),人工兜底归清理命令。 /// - 失败时不触碰旧 server 的文件(socket 与 sidecar),人工兜底归清理命令。
@@ -476,17 +479,18 @@ fn finish_shutdown(
/// # 注意事项 /// # 注意事项
/// - 纯函数式入口:版本管理路径(版本不匹配强制交接)与显式交接路径调用 /// - 纯函数式入口:版本管理路径(版本不匹配强制交接)与显式交接路径调用
/// 同一入口。 /// 同一入口。
/// - 交接协议在 Unix 文件面上;TCP 端点是防御性分支,不参与交接。 /// - 退出信号源按形态分:Unix 臂是 sidecar 文件消失,TCP 臂是端口释放。
pub fn yield_and_takeover(config: &ServerConfig) -> Result<(), ServerError> { pub fn yield_and_takeover(config: &ServerConfig) -> Result<(), ServerError> {
let socket_path = match &config.endpoint { // 连接旧 server:连接成功是交接的前提,两臂按 endpoint 形态分支。
Endpoint::Unix(path) => path, let mut stream = match &config.endpoint {
Endpoint::Tcp(_) => return Err(ServerError::YieldFailed("TCP 端点不支持交接".into())), Endpoint::Unix(path) => Conn::Unix(
UnixStream::connect(path).map_err(|e| ServerError::YieldFailed(format!("连接旧 server 失败:{e}")))?,
),
Endpoint::Tcp(addr) => Conn::Tcp(
TcpStream::connect(*addr).map_err(|e| ServerError::YieldFailed(format!("连接旧 server 失败:{e}")))?,
),
}; };
// 连接旧 server:连接成功是交接的前提。
let mut stream =
UnixStream::connect(socket_path).map_err(|e| ServerError::YieldFailed(format!("连接旧 server 失败:{e}")))?;
// 5 秒读超时:旧 server 挂起(不回应 yield)不无限阻塞。 // 5 秒读超时:旧 server 挂起(不回应 yield)不无限阻塞。
let _ = stream.set_read_timeout(Some(Duration::from_secs(5))); let _ = stream.set_read_timeout(Some(Duration::from_secs(5)));
stream stream
@@ -504,22 +508,39 @@ pub fn yield_and_takeover(config: &ServerConfig) -> Result<(), ServerError> {
if !matches!(response, ServerResponse::Yield) { if !matches!(response, ServerResponse::Yield) {
return Err(ServerError::YieldFailed(format!("旧 server 未受理交接:{response:?}"))); return Err(ServerError::YieldFailed(format!("旧 server 未受理交接:{response:?}")));
} }
// 关闭连接:旧 server 已受理,交接进入文件轮询阶段。 // 关闭连接:旧 server 已受理,交接进入退出等待阶段。
drop(reader); drop(reader);
// 轮询 sidecar 消失:旧 server 的退出序列以 sidecar 最后删除收口。 match &config.endpoint {
let deadline = Instant::now() + Duration::from_secs(10); // Unix 臂:轮询 sidecar 消失(旧 server 的退出序列以 sidecar 最后
while config.sidecar_path.exists() { // 删除收口),消失后重新认领。
if Instant::now() >= deadline { Endpoint::Unix(path) => {
return Err(ServerError::YieldFailed("旧 server 10 秒内未退出,sidecar 文件未消失".into())); let deadline = Instant::now() + Duration::from_secs(10);
} while config.sidecar_path.exists() {
std::thread::sleep(Duration::from_millis(50)); if Instant::now() >= deadline {
} return Err(ServerError::YieldFailed("旧 server 10 秒内未退出,sidecar 文件未消失".into()));
}
std::thread::sleep(Duration::from_millis(50));
}
// 重新认领:sidecar 已消失,O_EXCL 创建应成功;其余态判交接失败。 // 重新认领:sidecar 已消失,O_EXCL 创建应成功;其余态判交接失败。
match sidecar::claim_sidecar(&config.sidecar_path, socket_path, &config.version) { match sidecar::claim_sidecar(&config.sidecar_path, path, &config.version) {
ClaimResult::Registered => Ok(()), ClaimResult::Registered => Ok(()),
other => Err(ServerError::YieldFailed(format!("交接后重新认领未通过:{other:?}"))), other => Err(ServerError::YieldFailed(format!("交接后重新认领未通过:{other:?}"))),
}
}
// TCP 臂:无 sidecar 文件可轮询,以端口释放(连接被拒)为旧 server
// 退出信号,10 秒上限,不重认领。
Endpoint::Tcp(addr) => {
let deadline = Instant::now() + Duration::from_secs(10);
while TcpStream::connect(*addr).is_ok() {
if Instant::now() >= deadline {
return Err(ServerError::YieldFailed("旧 server 10 秒内未退出,端口仍被占用".into()));
}
std::thread::sleep(Duration::from_millis(50));
}
Ok(())
}
} }
} }
@@ -812,6 +833,7 @@ fn wait_in_flight_drained(in_flight: &AtomicUsize, timeout: Duration) {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use std::io::Write;
use std::net::TcpStream; use std::net::TcpStream;
use std::thread::JoinHandle; use std::thread::JoinHandle;
@@ -1349,6 +1371,30 @@ mod tests {
assert!(unix_path(&config.endpoint).exists(), "模拟 socket 文件不应被删除"); assert!(unix_path(&config.endpoint).exists(), "模拟 socket 文件不应被删除");
} }
/// 交接 TCP 臂:假 server 回 yield 确认帧后 drop 监听器(端口释放),
/// 交接以端口释放为退出信号返回成功(不重认领)。
#[test]
fn yield_takeover_tcp_arm() {
let listener = TcpListener::bind("127.0.0.1:0").expect("绑定假 server 应成功");
let addr = listener.local_addr().expect("取端口应成功");
std::thread::spawn(move || {
if let Ok((mut stream, _)) = listener.accept() {
let mut reader = FrameReader::new(io::BufReader::new(stream.try_clone().expect("克隆流应成功")));
let _ = reader.read_frame();
let frame = protocol::encode_response(&ServerResponse::Yield);
let _ = stream.write_all(frame.as_bytes());
}
});
let config = ServerConfig {
endpoint: Endpoint::Tcp(addr),
sidecar_path: std::env::temp_dir().join(format!("oxide_server_tcp_yield_{}_sidecar.json", std::process::id())),
worker_count: 2,
version: env!("CARGO_PKG_VERSION").to_string(),
};
yield_and_takeover(&config).expect("TCP 臂交接应成功");
}
/// --rm 加断开 EOF:eval 得完成值后客户端断开,server 在有界时间内 /// --rm 加断开 EOF:eval 得完成值后客户端断开,server 在有界时间内
/// 正常退出,socket 文件被删除,全局 well-known 路径前后不变。 /// 正常退出,socket 文件被删除,全局 well-known 路径前后不变。
#[test] #[test]
+9 -4
View File
@@ -13,12 +13,14 @@
use std::fs::{self, File, OpenOptions}; use std::fs::{self, File, OpenOptions};
use std::io::{self, Write}; use std::io::{self, Write};
use std::net::TcpStream;
use std::os::unix::io::AsRawFd; use std::os::unix::io::AsRawFd;
use std::os::unix::net::UnixStream; use std::os::unix::net::UnixStream;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Command; use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use oxide_net::Endpoint;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// server 身份快照:单行 JSON sidecar 文件,启动时写入一次。 /// server 身份快照:单行 JSON sidecar 文件,启动时写入一次。
@@ -174,12 +176,15 @@ pub fn read_identity(path: &Path) -> Option<ServerIdentity> {
serde_json::from_str(&text).ok() serde_json::from_str(&text).ok()
} }
/// socket 探活:连接成功即存在存活 server。 /// endpoint 探活:连接成功即存在存活 server,两臂按形态分支。
/// ///
/// 内核只把连接完成给存活监听者,该信号不依赖 PID、不受 PID 复用影响, /// 内核只把连接完成给存活监听者,该信号不依赖 PID、不受 PID 复用影响,
/// 是注册判定的唯一裁决。 /// 是注册判定的唯一裁决。
pub fn is_server_alive(socket_path: &Path) -> bool { pub fn is_server_alive(endpoint: &Endpoint) -> bool {
UnixStream::connect(socket_path).is_ok() match endpoint {
Endpoint::Unix(path) => UnixStream::connect(path).is_ok(),
Endpoint::Tcp(addr) => TcpStream::connect(*addr).is_ok(),
}
} }
/// PID 探活:`kill -0` 退出码 0 即进程存在。 /// PID 探活:`kill -0` 退出码 0 即进程存在。
@@ -254,7 +259,7 @@ pub fn claim_sidecar(sidecar_path: &Path, socket_path: &Path, new_version: &str)
} }
// socket 探活是存活的唯一事实源:连接成功即有存活 server。 // socket 探活是存活的唯一事实源:连接成功即有存活 server。
if is_server_alive(socket_path) { if is_server_alive(&Endpoint::Unix(socket_path.to_path_buf())) {
// 读回旧 sidecar 与本进程版本比对:匹配拒绝启动,不匹配或不可读 // 读回旧 sidecar 与本进程版本比对:匹配拒绝启动,不匹配或不可读
// 交调用方强制交接。 // 交调用方强制交接。
match read_identity(sidecar_path) { match read_identity(sidecar_path) {
+12 -11
View File
@@ -16,6 +16,7 @@ use std::time::{Duration, Instant};
use oxide_cli::server::client::send_control_request; use oxide_cli::server::client::send_control_request;
use oxide_cli::server::protocol::{self, FrameReader, ServerRequest, ServerResponse}; use oxide_cli::server::protocol::{self, FrameReader, ServerRequest, ServerResponse};
use oxide_cli::server::sidecar; use oxide_cli::server::sidecar;
use oxide_net::Endpoint;
/// well-known 全局路径进程内互斥锁:同测试二进制内触碰全局路径的测试串行 /// well-known 全局路径进程内互斥锁:同测试二进制内触碰全局路径的测试串行
/// 执行(fcntl 锁以进程为单位,不覆盖同进程多线程)。 /// 执行(fcntl 锁以进程为单位,不覆盖同进程多线程)。
@@ -114,7 +115,7 @@ fn server_help_lists_subcommands() {
fn server_log_no_file_exit_1() { fn server_log_no_file_exit_1() {
let _guard = lock_well_known(); let _guard = lock_well_known();
assert!( assert!(
!sidecar::is_server_alive(&sidecar::well_known_socket_path()), !sidecar::is_server_alive(&Endpoint::Unix(sidecar::well_known_socket_path())),
"存活 server 占用 well-known 路径,测试不抢占" "存活 server 占用 well-known 路径,测试不抢占"
); );
@@ -134,7 +135,7 @@ fn server_log_no_file_exit_1() {
fn server_control_arms_no_server_exit_1() { fn server_control_arms_no_server_exit_1() {
let _guard = lock_well_known(); let _guard = lock_well_known();
assert!( assert!(
!sidecar::is_server_alive(&sidecar::well_known_socket_path()), !sidecar::is_server_alive(&Endpoint::Unix(sidecar::well_known_socket_path())),
"存活 server 占用 well-known 路径,测试不抢占" "存活 server 占用 well-known 路径,测试不抢占"
); );
@@ -153,7 +154,7 @@ fn server_control_arms_no_server_exit_1() {
fn server_forge_no_server_exit_1() { fn server_forge_no_server_exit_1() {
let _guard = lock_well_known(); let _guard = lock_well_known();
assert!( assert!(
!sidecar::is_server_alive(&sidecar::well_known_socket_path()), !sidecar::is_server_alive(&Endpoint::Unix(sidecar::well_known_socket_path())),
"存活 server 占用 well-known 路径,测试不抢占" "存活 server 占用 well-known 路径,测试不抢占"
); );
@@ -168,7 +169,7 @@ fn server_forge_no_server_exit_1() {
fn server_cleanup_exit_0() { fn server_cleanup_exit_0() {
let _guard = lock_well_known(); let _guard = lock_well_known();
assert!( assert!(
!sidecar::is_server_alive(&sidecar::well_known_socket_path()), !sidecar::is_server_alive(&Endpoint::Unix(sidecar::well_known_socket_path())),
"存活 server 占用 well-known 路径,测试不抢占" "存活 server 占用 well-known 路径,测试不抢占"
); );
@@ -188,7 +189,7 @@ fn server_start_e2e() {
let sidecar_path = sidecar::well_known_sidecar_path(); let sidecar_path = sidecar::well_known_sidecar_path();
// 探活先行:存活 server 占用全局路径时 panic,不抢占。 // 探活先行:存活 server 占用全局路径时 panic,不抢占。
assert!(!sidecar::is_server_alive(&socket), "存活 server 占用 well-known 路径,测试不抢占"); assert!(!sidecar::is_server_alive(&Endpoint::Unix(socket.clone())), "存活 server 占用 well-known 路径,测试不抢占");
// start 为守护形态:CLI 即刻返回,server 是脱离的孙进程;worker 数压到 2 // start 为守护形态:CLI 即刻返回,server 是脱离的孙进程;worker 数压到 2
// 避免按宿主核数预热的成本。 // 避免按宿主核数预热的成本。
@@ -237,7 +238,7 @@ fn server_start_when_running() {
let sidecar_path = sidecar::well_known_sidecar_path(); let sidecar_path = sidecar::well_known_sidecar_path();
// 探活先行:存活 server 占用全局路径时 panic,不抢占。 // 探活先行:存活 server 占用全局路径时 panic,不抢占。
assert!(!sidecar::is_server_alive(&socket), "存活 server 占用 well-known 路径,测试不抢占"); assert!(!sidecar::is_server_alive(&Endpoint::Unix(socket.clone())), "存活 server 占用 well-known 路径,测试不抢占");
// 首次 start:CLI 即刻返回,server 是脱离的孙进程。 // 首次 start:CLI 即刻返回,server 是脱离的孙进程。
let output = oxide(&["server", "start", "--workers", "2"]); let output = oxide(&["server", "start", "--workers", "2"]);
@@ -275,7 +276,7 @@ fn server_restart_e2e() {
let sidecar_path = sidecar::well_known_sidecar_path(); let sidecar_path = sidecar::well_known_sidecar_path();
// 探活先行:存活 server 占用全局路径时 panic,不抢占。 // 探活先行:存活 server 占用全局路径时 panic,不抢占。
assert!(!sidecar::is_server_alive(&socket), "存活 server 占用 well-known 路径,测试不抢占"); assert!(!sidecar::is_server_alive(&Endpoint::Unix(socket.clone())), "存活 server 占用 well-known 路径,测试不抢占");
// 起子进程 A:worker 数压到 2,轮询 socket 文件出现(10 秒截止)判就绪。 // 起子进程 A:worker 数压到 2,轮询 socket 文件出现(10 秒截止)判就绪。
let mut child = Command::new(env!("CARGO_BIN_EXE_oxide")) let mut child = Command::new(env!("CARGO_BIN_EXE_oxide"))
@@ -356,7 +357,7 @@ fn server_log_e2e() {
let log_path = sidecar::well_known_log_path(); let log_path = sidecar::well_known_log_path();
// 探活先行:存活 server 占用全局路径时 panic,不抢占。 // 探活先行:存活 server 占用全局路径时 panic,不抢占。
assert!(!sidecar::is_server_alive(&socket), "存活 server 占用 well-known 路径,测试不抢占"); assert!(!sidecar::is_server_alive(&Endpoint::Unix(socket.clone())), "存活 server 占用 well-known 路径,测试不抢占");
// 删旧日志文件:server 即将全新启动,内容断言不受历史污染。 // 删旧日志文件:server 即将全新启动,内容断言不受历史污染。
let _ = std::fs::remove_file(&log_path); let _ = std::fs::remove_file(&log_path);
@@ -436,7 +437,7 @@ fn server_forge_e2e() {
let sidecar_path = sidecar::well_known_sidecar_path(); let sidecar_path = sidecar::well_known_sidecar_path();
// 探活先行:存活 server 占用全局路径时 panic,不抢占。 // 探活先行:存活 server 占用全局路径时 panic,不抢占。
assert!(!sidecar::is_server_alive(&socket), "存活 server 占用 well-known 路径,测试不抢占"); assert!(!sidecar::is_server_alive(&Endpoint::Unix(socket.clone())), "存活 server 占用 well-known 路径,测试不抢占");
// start 为守护形态:CLI 即刻返回,server 是脱离的孙进程;轮询 socket 文件 // start 为守护形态:CLI 即刻返回,server 是脱离的孙进程;轮询 socket 文件
// 出现判就绪(worker 数压到 2 避免按宿主核数预热的成本)。 // 出现判就绪(worker 数压到 2 避免按宿主核数预热的成本)。
@@ -495,7 +496,7 @@ fn server_watchdog_e2e() {
let sidecar_path = sidecar::well_known_sidecar_path(); let sidecar_path = sidecar::well_known_sidecar_path();
// 探活先行:存活 server 占用全局路径时 panic,不抢占。 // 探活先行:存活 server 占用全局路径时 panic,不抢占。
assert!(!sidecar::is_server_alive(&socket), "存活 server 占用 well-known 路径,测试不抢占"); assert!(!sidecar::is_server_alive(&Endpoint::Unix(socket.clone())), "存活 server 占用 well-known 路径,测试不抢占");
// 拉起 watchdog 前台进程(stdout 管道捕获崩溃诊断输出)。 // 拉起 watchdog 前台进程(stdout 管道捕获崩溃诊断输出)。
let mut child = Command::new(env!("CARGO_BIN_EXE_oxide")) let mut child = Command::new(env!("CARGO_BIN_EXE_oxide"))
@@ -598,7 +599,7 @@ fn server_watchdog_sigint_stops_server() {
let sidecar_path = sidecar::well_known_sidecar_path(); let sidecar_path = sidecar::well_known_sidecar_path();
// 探活先行:存活 server 占用全局路径时 panic,不抢占。 // 探活先行:存活 server 占用全局路径时 panic,不抢占。
assert!(!sidecar::is_server_alive(&socket), "存活 server 占用 well-known 路径,测试不抢占"); assert!(!sidecar::is_server_alive(&Endpoint::Unix(socket.clone())), "存活 server 占用 well-known 路径,测试不抢占");
// 拉起 watchdog 前台进程。 // 拉起 watchdog 前台进程。
let mut child = Command::new(env!("CARGO_BIN_EXE_oxide")) let mut child = Command::new(env!("CARGO_BIN_EXE_oxide"))
+7
View File
@@ -62,6 +62,13 @@ impl Endpoint {
} }
} }
impl std::fmt::Display for Endpoint {
/// 端点文本形态:Unix 臂为路径字符串,TCP 臂为 host:port 字符串。
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.describe())
}
}
impl Listener { impl Listener {
/// 设置监听者非阻塞模式。 /// 设置监听者非阻塞模式。
/// ///