Files
OxideJS-New/crates/oxide_cli/src/server/server.rs
T

1355 lines
62 KiB
Rust
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.
//! server 进程主体:accept 循环、控制请求直接处理、执行请求路由与优雅退出。
//!
//! 关键约定:
//! - 启动顺序契约(与身份注册一致):先认领 sidecar,再绑定 socket;
//! 反向会让客户端在 sidecar 缺位时误判无 server 并启动第二实例。
//! - --rm 独立模式(`run_server_rm`):独立单进程入口,绑定进程唯一 socket
//! 路径,不注册 sidecar、不碰全局 well-known 路径、不参与交接协议;
//! 只服务一个客户端连接,空闲超时(无连接或连接上无数据)或客户端
//! 断开(EOF)即自动退出。
//! - accept 循环为非阻塞 accept 加 10 毫秒轮询加原子关闭标志:唤醒路径
//! 确定、无锁、无唤醒丢失。
//! - 并发模型:主线程只跑 accept 循环;每连接一个标准库线程只做 I/O
//! (帧读写);六类控制请求由连接线程直接应答、不占 worker;执行请求
//! 经 mpsc 路由到固定 N 个常驻 worker 执行(各持自己线程上的 VM 池)。
//! - 优雅退出顺序:关闭请求、SIGINT/SIGTERM 信号或 yield 请求置位关闭标志
//! → 在途归零 → 清空发送端 → join worker → drop 内核 → 先删 socket 文件
//! 后删 sidecar(新 server 以 sidecar 消失为接管信号)。
//! - 池本体不是 Send/Sync(Vm 不跨线程),上下文只持池的原子状态句柄;
//! 状态响应读全体 worker 池的共享聚合计数器。
use std::fs;
use std::io;
use std::io::Write;
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
use oxide_kernel::kernel::{KernelConfig, KernelCore};
use oxide_vm::vm_pool::PoolCounters;
use super::protocol::{self, FrameReader, ProtocolError, ServerRequest, ServerResponse};
use super::sidecar::{self, ClaimResult};
use super::workers::{self, WorkerRouter};
/// server 启动与运行错误:绑定失败、认领被拒、接受连接失败、读取身份失败、
/// 信号处理器注册失败、交接失败。
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ServerError {
/// sidecar 认领未通过(已有存活 server 或存活证据不足以裁决)。
ClaimRefused(ClaimResult),
/// socket 绑定失败(含删除 stale socket 文件重试一次后仍失败)。
BindFailed(String),
/// 接受连接失败(accept 返回非 WouldBlock 错误)。
AcceptFailed(String),
/// 读取身份失败(sidecar 写后读回解析失败)。
IdentityReadFailed(String),
/// worker 线程异常退出(panic 载荷文本)。
WorkerPanic(String),
/// 信号处理器注册失败(系统级错误,非"已注册"的幂等情形)。
SignalInstallFailed(String),
/// 交接失败(连不上旧 server、旧 server 不受理 yield、或交接后重新认领未通过)。
YieldFailed(String),
}
impl std::fmt::Display for ServerError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ServerError::ClaimRefused(result) => write!(f, "sidecar 认领被拒:{result:?}"),
ServerError::BindFailed(msg) => write!(f, "socket 绑定失败:{msg}"),
ServerError::AcceptFailed(msg) => write!(f, "接受连接失败:{msg}"),
ServerError::IdentityReadFailed(msg) => write!(f, "读取身份失败:{msg}"),
ServerError::WorkerPanic(msg) => write!(f, "worker 线程异常退出:{msg}"),
ServerError::SignalInstallFailed(msg) => write!(f, "信号处理器注册失败:{msg}"),
ServerError::YieldFailed(msg) => write!(f, "交接接管失败:{msg}"),
}
}
}
/// server 运行配置:socket 与 sidecar 路径可注入。
///
/// 生产默认取 well-known 位置;测试注入临时路径。
#[derive(Debug, Clone)]
pub struct ServerConfig {
/// Unix socket 路径。
pub socket_path: PathBuf,
/// sidecar 文件路径。
pub sidecar_path: PathBuf,
/// 常驻 worker 线程数;缺省取宿主核数,下限钳制为 1。
pub worker_count: usize,
}
impl ServerConfig {
/// 生产默认配置: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(),
}
}
}
/// 缺省 worker 数:宿主核数,下限钳制为 1。
fn default_worker_count() -> usize {
std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1).max(1)
}
/// --rm 模式缺省空闲超时:30 秒。
const RM_DEFAULT_IDLE_TIMEOUT: Duration = Duration::from_secs(30);
/// --rm 独立模式运行配置:唯一 socket 路径、worker 数、空闲超时。
///
/// 不含 sidecar 路径字段:「不注册 sidecar」的不变量在类型上成立,
/// 不需要运行时守卫。
#[derive(Debug, Clone)]
pub struct RmServerConfig {
/// Unix socket 路径;缺省为进程唯一路径。
pub socket_path: PathBuf,
/// 常驻 worker 线程数;缺省取宿主核数,下限钳制为 1。
pub worker_count: usize,
/// 空闲超时:绑定后该时长内无连接、或连接建立后该时长内无数据,
/// 进程即干净退出。
pub idle_timeout: Duration,
}
impl Default for RmServerConfig {
fn default() -> Self {
RmServerConfig {
socket_path: sidecar::rm_socket_path(),
worker_count: default_worker_count(),
idle_timeout: RM_DEFAULT_IDLE_TIMEOUT,
}
}
}
/// server 运行上下文:共享内核、启动时刻、关闭标志、在途计数、池聚合计数器。
///
/// 内核跨线程共享(`Arc<KernelCore>` 是 Send/Sync);池本体不进上下文
/// (Vm 不跨线程,池归各 worker 线程所有),状态响应读共享聚合计数器。
struct ServerContext {
// 只持不读:余留连接线程(在途排水超时后仍存活)持本上下文,内核句柄
// 随上下文存活,不悬垂。
#[allow(dead_code)]
kernel: Arc<KernelCore>,
started_at: Instant,
shutdown: Arc<AtomicBool>,
in_flight: Arc<AtomicUsize>,
pool_counters: Arc<PoolCounters>,
socket_path: String,
}
/// server 进程主体:认领 sidecar → 注册信号处理器 → 绑定 socket → 建内核 →
/// 派生 worker → accept 循环 → 在途归零 → 清空发送端 → join worker →
/// drop 内核 → 先删 socket 文件后删 sidecar。
///
/// # 步骤
/// 1. 认领 sidecar(启动顺序契约:先写 sidecar 再 bind socket);已有存活
/// server 时转入交接路径(向旧 server 发 yield 请求,等旧 server 排空
/// 退出后重新认领)。
/// 2. 注册信号处理器(绑定 socket 之前;失败回滚 sidecar,此时无存活线程)。
/// 3. 绑定监听器;失败且路径存在时删 stale socket 文件重试一次。
/// 4. 建内核(标准配置)与共享聚合计数器。
/// 5. 派生 N 个常驻 worker(各在自己线程上建自有池)。
/// 6. 进入 accept 循环;关闭标志置位后按关闭序列退出。
///
/// # 边界与前提
/// - 认领得 `AlreadyRunning` 时走交接路径,交接失败以 `YieldFailed` 返回。
/// - 认领得 `RefusedAmbiguous` 立即返回错误,不双注册。
/// - 信号处理器注册失败(系统级错误)回滚 sidecar 并以错误返回。
/// - stale socket 文件删除重试后再失败时回滚删除 sidecar 并以错误返回。
///
/// # 副作用
/// - 创建 sidecar 与 socket 文件;正常退出时删除两者。
/// - 注册进程级信号处理器(SIGINT、SIGTERM,termination 特性下含 SIGHUP)。
/// - 派生 N 个常驻 worker 线程与每连接一个 I/O 线程。
///
/// # 注意事项
/// - 在调用线程上运行(后续 CLI 骨架任务在主线程分派到它)。
/// - 关闭标志由关闭请求、SIGINT/SIGTERM 信号或 yield 请求置位(三个触发源
/// 汇合到同一条退出路径)。
/// - 关闭序列固定:清空发送端 → 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) {
ClaimResult::Registered => {}
ClaimResult::AlreadyRunning => yield_and_takeover(config)?,
ClaimResult::RefusedAmbiguous => return Err(ServerError::ClaimRefused(ClaimResult::RefusedAmbiguous)),
}
// 关闭标志提前创建并注册信号处理器:在绑定 socket 之前,socket 文件
// 出现即蕴含处理器已就位(集成测试的信号投递无竞态);失败回滚
// sidecar,此时进程内没有存活线程。
let shutdown = Arc::new(AtomicBool::new(false));
if let Err(err) = install_signal_handler(&shutdown) {
let _ = fs::remove_file(&config.sidecar_path);
return Err(err);
}
// 绑定监听器:stale socket 文件删一次重试,失败回滚 sidecar。
let listener = bind_listener(config)?;
// 建内核(标准配置)与共享聚合计数器(全体 worker 池的增减累加到同一聚合)。
let kernel = KernelCore::new(KernelConfig::standard());
let counters = PoolCounters::shared();
// 派生 N 个常驻 worker:各持自己线程上建的池(Vm 不跨线程)。
let (router, worker_handles) = workers::spawn_workers(
Arc::clone(&kernel),
Arc::clone(&counters),
kernel.config.min_pool_size,
kernel.config.max_pool_size,
config.worker_count,
);
let router = Arc::new(router);
let ctx = Arc::new(ServerContext {
kernel: Arc::clone(&kernel),
started_at: Instant::now(),
shutdown: Arc::clone(&shutdown),
in_flight: Arc::new(AtomicUsize::new(0)),
pool_counters: counters,
socket_path: config.socket_path.to_string_lossy().into_owned(),
});
// accept 循环:非阻塞 accept 加 10 毫秒轮询加原子关闭标志。
let accept_result = accept_loop(&listener, &ctx, &router);
// 关闭序列:在途归零、清空发送端、join worker、drop 内核、删文件。
finish_shutdown(&ctx, &router, worker_handles, kernel, &config.socket_path, Some(&config.sidecar_path))
.and(accept_result)
}
/// --rm 独立模式入口:独立单进程,不注册 sidecar、不碰全局 well-known
/// 路径,只服务一个客户端连接,空闲超时或客户端断开即自动退出。
///
/// # 步骤
/// 1. 建关闭标志并注册信号处理器(幂等);失败直接返回(无 sidecar 可回滚)。
/// 2. 绑定监听器(进程唯一路径,与持久路径共用同一绑定函数)。
/// 3. 建内核(标准配置)、共享聚合计数器,派生 worker(参数与持久 server 一致)。
/// 4. 空闲窗口一:等首个连接;空闲超时内无连接即干净退出(正常退出,非错误)。
/// 5. 首个连接派生连接线程:处理完毕(EOF 或读超时)后置关闭标志并减在途计数。
/// 6. 丢弃监听器:首个连接之后不再接受新连接,后续连接得连接被拒绝。
/// 7. 轮询关闭标志(10 毫秒间隔):触发源有四个——连接线程 EOF、连接线程
/// 读超时、信号、控制请求(关闭与 yield 臂经共享分派置位)。
/// 8. 走共享关闭序列(在途归零、清空发送端、join worker、drop 内核、
/// 删 socket 文件;不删 sidecar,因为从未写过)。
///
/// # 边界与前提
/// - 空闲超时内无连接时返回 `Ok(())`:客户端未来连接是预期场景,退出是
/// 正常退出而非错误。
/// - 首个连接被接受后第二个连接得连接被拒绝(监听器已丢弃)。
///
/// # 副作用
/// - 创建 socket 文件,正常退出时删除;不写 sidecar。
/// - 注册进程级信号处理器(SIGINT、SIGTERM,termination 特性下含 SIGHUP)。
/// - 派生 N 个常驻 worker 线程与每连接一个 I/O 线程。
///
/// # 注意事项
/// - 在调用线程上运行(前台入口在主线程调用它)。
/// - yield 控制帧在本模式语义等同关闭:置关闭标志并回确认帧(无 sidecar
/// 可删、无接管逻辑),是「不交接」而非「参与交接」。
pub fn run_server_rm(config: &RmServerConfig) -> Result<(), ServerError> {
// 关闭标志提前创建并注册信号处理器:在绑定 socket 之前,socket 文件
// 出现即蕴含处理器已就位(与持久 server 同一契约);失败直接返回,
// 无 sidecar 可回滚。
let shutdown = Arc::new(AtomicBool::new(false));
install_signal_handler(&shutdown)?;
// 绑定监听器:进程唯一路径不存在 stale 文件竞争,与持久路径共用同一
// 绑定函数(删除重试逻辑保留只为共用)。
let listener = bind_listener_path(&config.socket_path)?;
// 建内核(标准配置)与共享聚合计数器(全体 worker 池的增减累加到同一聚合)。
let kernel = KernelCore::new(KernelConfig::standard());
let counters = PoolCounters::shared();
// 派生 N 个常驻 worker:各持自己线程上建的池(Vm 不跨线程)。
let (router, worker_handles) = workers::spawn_workers(
Arc::clone(&kernel),
Arc::clone(&counters),
kernel.config.min_pool_size,
kernel.config.max_pool_size,
config.worker_count,
);
let router = Arc::new(router);
let ctx = Arc::new(ServerContext {
kernel: Arc::clone(&kernel),
started_at: Instant::now(),
shutdown: Arc::clone(&shutdown),
in_flight: Arc::new(AtomicUsize::new(0)),
pool_counters: counters,
socket_path: config.socket_path.to_string_lossy().into_owned(),
});
// 空闲窗口一:等首个连接;空闲超时内无连接即干净退出。
let stream = match rm_accept_first(&listener, config.idle_timeout)? {
Some(stream) => stream,
None => return finish_shutdown(&ctx, &router, worker_handles, kernel, &config.socket_path, None),
};
// 单连接模型:首个连接被接受后丢弃监听器,后续 connect 得连接被拒绝,
// 不进入任何处理路径。
drop(listener);
// 首个连接派生连接线程:处理完毕(对端断开 EOF 或读超时)即置关闭标志
// 并减在途计数,两个事件都映射为关闭标志置位。
ctx.in_flight.fetch_add(1, Ordering::SeqCst);
let ctx_conn = Arc::clone(&ctx);
let router_conn = Arc::clone(&router);
let read_timeout = config.idle_timeout;
std::thread::spawn(move || {
handle_connection(stream, &ctx_conn, &router_conn, read_timeout);
ctx_conn.shutdown.store(true, Ordering::SeqCst);
ctx_conn.in_flight.fetch_sub(1, Ordering::SeqCst);
});
// 轮询关闭标志:触发源有四个——连接线程 EOF、连接线程读超时、信号、
// 控制请求(关闭与 yield 臂经共享分派置位)。
while !shutdown.load(Ordering::SeqCst) {
std::thread::sleep(Duration::from_millis(10));
}
finish_shutdown(&ctx, &router, worker_handles, kernel, &config.socket_path, None)
}
/// 等首个连接(--rm 空闲窗口一):非阻塞 accept 加 10 毫秒轮询加截止时间。
///
/// # 步骤
/// 1. 监听器置非阻塞。
/// 2. 轮询 accept:成功返回连接;WouldBlock 睡 10 毫秒再查;其余错误以
/// `AcceptFailed` 返回。
/// 3. 截止时间(自调用时刻起算的空闲超时)内无连接返回 `None`(干净退出,
/// 非错误)。
///
/// # 边界与前提
/// - 截止时间是 `Instant` 单调时钟,无时钟回拨问题。
fn rm_accept_first(listener: &UnixListener, timeout: Duration) -> Result<Option<UnixStream>, ServerError> {
listener
.set_nonblocking(true)
.map_err(|e| ServerError::AcceptFailed(e.to_string()))?;
let deadline = Instant::now() + timeout;
loop {
match listener.accept() {
Ok((stream, _addr)) => return Ok(Some(stream)),
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
if Instant::now() >= deadline {
return Ok(None);
}
std::thread::sleep(Duration::from_millis(10));
}
Err(e) => return Err(ServerError::AcceptFailed(e.to_string())),
}
}
}
/// 关闭序列:在途归零 → 清空发送端 → join worker → drop 内核 → 删 socket
/// 文件(与 sidecar 文件)。
///
/// 持久 server 与 --rm 独立模式共用同一条固定序列,单点定义;`sidecar_path`
/// 为 `None` 时不删 sidecar(--rm 从未写过)。
///
/// # 步骤
/// 1. 在途归零:轮询至零,5 秒安全超时后记警告继续(余留连接线程持内核
/// 与路由器的共享句柄,不悬垂;其后续投递经清空的路由立即失败)。
/// 2. 清空发送端:worker 的 recv 断开退出。
/// 3. 逐一 join worker;panic 载荷以 `WorkerPanic` 返回。
/// 4. 释放内核句柄(worker 池已随线程退出释放);上下文由调用方持有,
/// 其内核句柄随调用方局部变量在函数返回时释放。
/// 5. 先删 socket 文件、后删 sidecar 文件(尽力而为,文件已不存在不视为
/// 错误)。
///
/// # 边界与前提
/// - `sidecar_path` 为 `None` 时跳过 sidecar 删除。
///
/// # 副作用
/// - 删除 socket 文件与(若传入)sidecar 文件。
fn finish_shutdown(
ctx: &Arc<ServerContext>, router: &Arc<WorkerRouter>, worker_handles: Vec<JoinHandle<()>>, kernel: Arc<KernelCore>,
socket_path: &Path, sidecar_path: Option<&Path>,
) -> Result<(), ServerError> {
// 在途归零:轮询至零,5 秒安全超时后记警告继续(余留连接线程持内核
// 与路由器的共享句柄,不悬垂;其后续投递经清空的路由立即失败)。
wait_in_flight_drained(&ctx.in_flight, Duration::from_secs(5));
// 关闭序列:清空发送端 → worker 的 recv 断开退出 → 逐一 join。
router.close();
for handle in worker_handles {
if let Err(payload) = handle.join() {
return Err(ServerError::WorkerPanic(panic_payload_str(&payload)));
}
}
// 释放内核句柄(worker 池已随线程退出释放);上下文由调用方持有,
// 其内核句柄随调用方局部变量在函数返回时释放。
drop(kernel);
// 先删 socket 文件、后删 sidecar(尽力而为,文件已不存在不视为错误):
// 新 server 以 sidecar 消失为接管信号,届时 socket 文件必已删除,
// 接管绑定无竞态。
let _ = fs::remove_file(socket_path);
if let Some(sidecar) = sidecar_path {
let _ = fs::remove_file(sidecar);
}
Ok(())
}
/// 交接接管:连接旧 server、发 yield 请求、等旧 server 排空退出后重新认领。
///
/// # 步骤
/// 1. 连接旧 server 的 socket;连接失败判 `YieldFailed`。
/// 2. 设 5 秒读超时,写 yield 请求帧。
/// 3. 读一帧应答:读超时(旧 server 挂起)或应答非 yield 确认(旧版本
/// binary 无该变体时以错误帧答复未知类型)判 `YieldFailed`。
/// 4. 关闭连接,轮询 sidecar 文件消失(50 毫秒间隔、10 秒上限);超时判
/// `YieldFailed`。
/// 5. 重新认领 sidecar;得 `Registered` 即接管成功,其余态判 `YieldFailed`。
///
/// # 边界与前提
/// - 交接等待 10 秒长于旧 server 的排水安全超时 5 秒,正常路径必然先于
/// 等待上限完成。
/// - 旧 server 先删 socket 文件后删 sidecar,sidecar 消失时 socket 文件必
/// 已删除,接管绑定无竞态。
///
/// # 副作用
/// - 失败时不触碰旧 server 的文件(socket 与 sidecar),人工兜底归清理命令。
///
/// # 注意事项
/// - 纯函数式入口:版本管理路径在版本不匹配强制交接时调用同一入口。
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}")))?;
// 5 秒读超时:旧 server 挂起(不回应 yield)不无限阻塞。
let _ = stream.set_read_timeout(Some(Duration::from_secs(5)));
stream
.write_all(protocol::encode_request(&ServerRequest::Yield).as_bytes())
.map_err(|e| ServerError::YieldFailed(format!("写 yield 请求帧失败:{e}")))?;
// 读确认帧:只有 yield 确认表示旧 server 已受理交接。
let mut reader = FrameReader::new(io::BufReader::new(stream));
let frame = reader
.read_frame()
.map_err(|e| ServerError::YieldFailed(format!("读 yield 确认帧失败:{e}")))?
.ok_or_else(|| ServerError::YieldFailed("旧 server 未应答即关闭连接".into()))?;
let response = protocol::parse_response(&frame)
.map_err(|e| ServerError::YieldFailed(format!("解析 yield 确认帧失败:{e}")))?;
if !matches!(response, ServerResponse::Yield) {
return Err(ServerError::YieldFailed(format!("旧 server 未受理交接:{response:?}")));
}
// 关闭连接:旧 server 已受理,交接进入文件轮询阶段。
drop(reader);
// 轮询 sidecar 消失:旧 server 的退出序列以 sidecar 最后删除收口。
let deadline = Instant::now() + Duration::from_secs(10);
while config.sidecar_path.exists() {
if Instant::now() >= deadline {
return Err(ServerError::YieldFailed("旧 server 10 秒内未退出,sidecar 文件未消失".into()));
}
std::thread::sleep(Duration::from_millis(50));
}
// 重新认领:sidecar 已消失,O_EXCL 创建应成功;其余态判交接失败。
match sidecar::claim_sidecar(&config.sidecar_path, &config.socket_path) {
ClaimResult::Registered => Ok(()),
other => Err(ServerError::YieldFailed(format!("交接后重新认领未通过:{other:?}"))),
}
}
/// 信号处理器体:置位关闭标志。
///
/// # 边界与前提
/// - 只做一次原子写,不分配、不取锁、不做 I/O;即使按最严格的信号上下文
/// 标准衡量也是安全的。
fn signal_shutdown(shutdown: &Arc<AtomicBool>) {
shutdown.store(true, Ordering::SeqCst);
}
/// 注册 SIGINT/SIGTERM 信号处理器:信号到达时置位关闭标志。
///
/// # 步骤
/// 1. 闭包捕获关闭标志的共享句柄,转调 `signal_shutdown`。
/// 2. `ctrlc::set_handler` 注册;"已注册"的幂等情形视为成功,其余系统
/// 错误映射为 `SignalInstallFailed`。
///
/// # 边界与前提
/// - 处理器是进程级全局的(同一进程只允许一次注册);生产路径一个进程
/// 只有一个 server,注册一次即成功。
///
/// # 副作用
/// - 注册进程级信号处理器(SIGINT、SIGTERM,termination 特性下含 SIGHUP)。
///
/// # 注意事项
/// - 须在绑定 socket 之前调用:socket 文件出现即蕴含处理器已就位,
/// 集成测试的信号投递无竞态。
/// - 测试进程内多 server 并存时,仅首个注册者接上信号,其余返回"已注册"
/// 幂等成功;单测不发真实信号,无干扰。
fn install_signal_handler(shutdown: &Arc<AtomicBool>) -> Result<(), ServerError> {
let flag = Arc::clone(shutdown);
match ctrlc::set_handler(move || signal_shutdown(&flag)) {
Ok(()) => Ok(()),
// 进程级处理器已注册(测试进程内多 server 并存的幂等情形)。
Err(ctrlc::Error::MultipleHandlers) => Ok(()),
Err(e) => Err(ServerError::SignalInstallFailed(e.to_string())),
}
}
/// 从 `catch_unwind` 的 panic payload 提取可读文本:`&str` 与 `String` 两种
/// 常见形态,其余返回占位文本。
fn panic_payload_str(payload: &Box<dyn std::any::Any + Send>) -> String {
if let Some(s) = payload.downcast_ref::<&str>() {
return (*s).to_string();
}
if let Some(s) = payload.downcast_ref::<String>() {
return s.clone();
}
"non-string payload".into()
}
/// 绑定监听器:失败且路径存在时删 stale socket 文件重试一次。
///
/// # 边界与前提
/// - stale socket 文件是崩溃残留(文件存在但无监听者)。
///
/// # 副作用
/// - 可能删除 socket 文件。
fn bind_listener_path(path: &Path) -> Result<UnixListener, ServerError> {
match UnixListener::bind(path) {
Ok(listener) => Ok(listener),
Err(_first) if path.exists() => {
// 路径存在:崩溃残留的 stale socket 文件,删一次重试。
let _ = fs::remove_file(path);
UnixListener::bind(path).map_err(|second| ServerError::BindFailed(format!("socket 绑定失败:{second}")))
}
Err(first) => Err(ServerError::BindFailed(format!("socket 绑定失败:{first}"))),
}
}
/// 绑定监听器:失败时删除 sidecar(回滚)并返回错误。
///
/// # 边界与前提
/// - stale socket 文件是崩溃残留(文件存在但无监听者)。
///
/// # 副作用
/// - 可能删除 socket 文件;重试仍失败时删除 sidecar(回滚)。
fn bind_listener(config: &ServerConfig) -> Result<UnixListener, ServerError> {
match bind_listener_path(&config.socket_path) {
Ok(listener) => Ok(listener),
Err(err) => {
let _ = fs::remove_file(&config.sidecar_path);
Err(err)
}
}
}
/// accept 循环:非阻塞 accept 加 10 毫秒轮询,关闭标志置位即退出。
///
/// # 步骤
/// 1. 检查关闭标志(原子布尔,序一致排序)。
/// 2. accept:成功则派生连接线程(在途计数加一);WouldBlock 睡 10 毫秒再查;
/// 其余错误以 `AcceptFailed` 返回。
///
/// # 副作用
/// - 每连接派生一个标准库线程(只做 I/O),线程结束时减一在途计数。
fn accept_loop(
listener: &UnixListener, ctx: &Arc<ServerContext>, router: &Arc<WorkerRouter>,
) -> Result<(), ServerError> {
listener
.set_nonblocking(true)
.map_err(|e| ServerError::AcceptFailed(e.to_string()))?;
loop {
// 关闭标志置位(来自任意连接线程)即退出。
if ctx.shutdown.load(Ordering::SeqCst) {
return Ok(());
}
match listener.accept() {
Ok((stream, _addr)) => {
ctx.in_flight.fetch_add(1, Ordering::SeqCst);
let ctx = Arc::clone(ctx);
let router = Arc::clone(router);
std::thread::spawn(move || {
// 持久 server 的读超时保持 30 秒(行为与参数化前一致)。
handle_connection(stream, &ctx, &router, Duration::from_secs(30));
ctx.in_flight.fetch_sub(1, Ordering::SeqCst);
});
}
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
std::thread::sleep(Duration::from_millis(10));
}
Err(e) => return Err(ServerError::AcceptFailed(e.to_string())),
}
}
}
/// 每连接处理:帧读取 → 分派 → 响应写回,循环至对端关闭。
///
/// # 步骤
/// 1. 克隆写端,原流交给帧读取器,设读超时(`read_timeout` 参数)。
/// 2. 循环读帧:EOF、读超时、非法 UTF-8 退出;帧长超限按协议约定关连接。
/// 3. 畸形帧以 `Error` 帧答复后继续;执行请求路由到 worker 并阻塞等回复;
/// 控制请求直接应答并写回。
/// 4. 关闭或 yield 确认帧写回即退出循环。
///
/// # 边界与前提
/// - 半开连接(对端静默不关)由读超时有界化:持久 server 传 30 秒,
/// --rm 独立模式传空闲超时(读超时即空闲语义)。
/// - 本线程只做 I/O 与分派,不建池(池归 worker 线程所有)。
///
/// # 副作用
/// - 关闭请求会置位关闭标志。
fn handle_connection(stream: UnixStream, ctx: &ServerContext, router: &WorkerRouter, read_timeout: Duration) {
// 克隆写端:原流交给帧读取器,写端独立用于响应写回。
let mut writer = match stream.try_clone() {
Ok(writer) => writer,
Err(_) => return,
};
// 读超时:半开连接不永久占住线程。
let _ = stream.set_read_timeout(Some(read_timeout));
let mut reader = FrameReader::new(io::BufReader::new(stream));
// 读帧:EOF、读超时、非法 UTF-8 均退出。
while let Ok(Some(frame)) = reader.read_frame() {
// 分派:帧长超限按协议约定关连接;畸形帧以 Error 帧答复后继续;
// 执行请求走 worker 路由,控制请求直接应答。
let response = match protocol::parse_request(&frame) {
Ok(ServerRequest::Eval { code, max_steps }) => dispatch_eval(&code, max_steps, router),
Ok(request) => dispatch_control(&request, ctx),
Err(ProtocolError::FrameTooLarge) => break,
Err(error) => error.to_error_frame(),
};
// 写回响应帧;写失败退出。
let encoded = protocol::encode_response(&response);
if writer.write_all(encoded.as_bytes()).is_err() {
break;
}
// 关闭或 yield 确认帧写回即退出循环。
if matches!(response, ServerResponse::Shutdown | ServerResponse::Yield) {
break;
}
}
}
/// 执行请求分派:路由到 worker 执行,阻塞在一次性回复通道上取回响应帧。
///
/// # 步骤
/// 1. 建一次性回复通道,发送端随任务进 worker。
/// 2. 轮询投递到 worker;发送端已清空(关闭序列进行中)时立即以错误帧答复。
/// 3. 阻塞 recv 至回复;worker 已退出(通道断开)时以错误帧答复,不挂起。
fn dispatch_eval(code: &str, max_steps: Option<u64>, router: &WorkerRouter) -> ServerResponse {
let (reply_tx, reply_rx) = std::sync::mpsc::channel();
let task = workers::WorkerTask::Eval {
code: code.to_string(),
max_steps,
reply: reply_tx,
};
match router.route(task) {
Ok(()) => reply_rx
.recv()
.unwrap_or_else(|_| ServerResponse::eval_err("worker 已退出,执行请求未处理")),
Err(_) => ServerResponse::eval_err("server 正在关闭,执行请求未受理"),
}
}
/// 控制请求分派:六类控制请求直接应答,不占 worker。
///
/// Status 响应读全体 worker 池的共享聚合计数器。
fn dispatch_control(request: &ServerRequest, ctx: &ServerContext) -> ServerResponse {
match request {
ServerRequest::Version => ServerResponse::Version {
version: env!("CARGO_PKG_VERSION").to_string(),
},
ServerRequest::Status => ServerResponse::Status {
pool_available: ctx.pool_counters.available(),
pool_total: ctx.pool_counters.total(),
pool_peak: ctx.pool_counters.peak(),
uptime_ms: ctx.started_at.elapsed().as_millis() as u64,
},
ServerRequest::Health => ServerResponse::Health { healthy: true },
ServerRequest::Info => ServerResponse::Info {
version: env!("CARGO_PKG_VERSION").to_string(),
socket_path: ctx.socket_path.clone(),
pid: std::process::id(),
},
ServerRequest::Shutdown => {
// 置关闭标志;主循环至多 10 毫秒内退出 accept 循环。
ctx.shutdown.store(true, Ordering::SeqCst);
ServerResponse::Shutdown
}
ServerRequest::Yield => {
// 置与关闭请求同一关闭标志,退出序列与关闭请求完全一致。
ctx.shutdown.store(true, Ordering::SeqCst);
ServerResponse::Yield
}
// 防御性兜底:正常路径 eval 在调用本函数前已被拦截走 worker 路由;
// 若到达此臂说明上游分流缺失,以错误帧答复而非静默执行。
ServerRequest::Eval { .. } => ServerResponse::eval_err("执行请求应经 worker 路由,不应到达控制分派"),
}
}
/// 在途归零:轮询在途计数至零,超时记警告继续。
fn wait_in_flight_drained(in_flight: &AtomicUsize, timeout: Duration) {
let deadline = Instant::now() + timeout;
while in_flight.load(Ordering::SeqCst) > 0 {
if Instant::now() >= deadline {
eprintln!("[oxide] server 关闭:在途连接未归零,超时继续");
break;
}
std::thread::sleep(Duration::from_millis(10));
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread::JoinHandle;
/// 唯一临时路径(进程号加测试名):单进程内唯一,进程间以进程号隔离。
/// worker 数取小值 2:测试在宿主核数上跑,大值徒增预热成本。
fn unique_paths(test_name: &str) -> ServerConfig {
let dir = std::env::temp_dir().join(format!("oxide_server_test_{}_{}", std::process::id(), test_name));
fs::create_dir_all(&dir).expect("测试目录创建应成功");
ServerConfig {
socket_path: dir.join("server.sock"),
sidecar_path: dir.join("sidecar.json"),
worker_count: 2,
}
}
/// 后台线程起真实 run_server,等 socket 文件出现(bind 成功即就绪)。
fn start_server(config: ServerConfig) -> JoinHandle<Result<(), ServerError>> {
let server_config = config.clone();
let handle = std::thread::spawn(move || run_server(&server_config));
let deadline = Instant::now() + Duration::from_secs(10);
while !config.socket_path.exists() {
assert!(Instant::now() < deadline, "socket 文件未出现");
std::thread::sleep(Duration::from_millis(10));
}
handle
}
/// 连接 server 并返回流。
fn connect(config: &ServerConfig) -> UnixStream {
UnixStream::connect(&config.socket_path).expect("连接 server 应成功")
}
/// 发送请求帧并读回一帧响应。
fn send_and_recv(stream: &mut UnixStream, request: &ServerRequest) -> ServerResponse {
stream
.write_all(protocol::encode_request(request).as_bytes())
.expect("写请求应成功");
let mut reader = FrameReader::new(io::BufReader::new(stream));
let frame = reader.read_frame().expect("读响应应成功").expect("应读到响应帧");
protocol::parse_response(&frame).expect("响应帧解析应成功")
}
/// 停 server:发关闭请求、等线程退出、断言正常退出。
fn stop_server(stream: &mut UnixStream, handle: JoinHandle<Result<(), ServerError>>) {
let response = send_and_recv(stream, &ServerRequest::Shutdown);
assert!(matches!(response, ServerResponse::Shutdown), "应得关闭确认帧:{response:?}");
let result = handle.join().expect("server 线程应正常退出");
assert!(result.is_ok(), "server 应正常退出:{result:?}");
}
/// --rm 配置:唯一临时路径、worker 数 1、300 毫秒空闲超时(不依赖真实
/// 超时值,断言带 5 秒裕量)。
fn rm_config(test_name: &str) -> RmServerConfig {
let dir = std::env::temp_dir().join(format!("oxide_server_rm_test_{}_{}", std::process::id(), test_name));
fs::create_dir_all(&dir).expect("测试目录创建应成功");
RmServerConfig {
socket_path: dir.join("rm.sock"),
worker_count: 1,
idle_timeout: Duration::from_millis(300),
}
}
/// 后台线程起真实 run_server_rm,等 socket 文件出现(bind 成功即就绪)。
fn start_rm_server(config: RmServerConfig) -> JoinHandle<Result<(), ServerError>> {
let server_config = config.clone();
let handle = std::thread::spawn(move || run_server_rm(&server_config));
let deadline = Instant::now() + Duration::from_secs(10);
while !config.socket_path.exists() {
assert!(Instant::now() < deadline, "socket 文件未出现");
std::thread::sleep(Duration::from_millis(10));
}
handle
}
/// 信号处理器体:置位关闭标志。
#[test]
fn signal_shutdown_sets_flag() {
let shutdown = Arc::new(AtomicBool::new(false));
signal_shutdown(&shutdown);
assert!(shutdown.load(Ordering::SeqCst), "标志应被置位");
}
/// 注册信号处理器:返回成功("已注册"的幂等情形同样视为成功)。
#[test]
fn install_signal_handler_registers() {
let shutdown = Arc::new(AtomicBool::new(false));
install_signal_handler(&shutdown).expect("信号处理器注册应成功");
}
/// 版本请求得 Version 帧,取值与构建期版本一致。
#[test]
fn version_response() {
let config = unique_paths("version");
let handle = start_server(config.clone());
let mut stream = connect(&config);
let response = send_and_recv(&mut stream, &ServerRequest::Version);
assert!(
matches!(response, ServerResponse::Version { ref version } if version == env!("CARGO_PKG_VERSION")),
"版本应与构建期版本一致:{response:?}"
);
stop_server(&mut stream, handle);
}
/// 状态请求得 Status 帧:空闲数不超过总数、总数不小于 1(标准配置每 worker 预热 1 个)、运行时长合理。
#[test]
fn status_response() {
let config = unique_paths("status");
let handle = start_server(config.clone());
let mut stream = connect(&config);
// worker 池异步预热:轮询至聚合总数不小于 1(预热完成)。
let deadline = Instant::now() + Duration::from_secs(5);
let response = loop {
let response = send_and_recv(&mut stream, &ServerRequest::Status);
if let ServerResponse::Status { pool_total, .. } = &response {
if *pool_total >= 1 {
break response;
}
}
assert!(Instant::now() < deadline, "worker 池预热应完成");
std::thread::sleep(Duration::from_millis(10));
};
match response {
ServerResponse::Status {
pool_available,
pool_total,
pool_peak,
uptime_ms,
} => {
assert!(pool_available <= pool_total, "空闲数应不超过总数:{pool_available}/{pool_total}");
assert!(pool_total >= 1, "总数应不小于 1(标准配置预热 1 个):{pool_total}");
assert!(pool_peak <= pool_total, "峰值应不超过总数:{pool_peak}/{pool_total}");
assert!(uptime_ms < 10_000, "刚启动的 server 运行时长应小于 10 秒:{uptime_ms}");
}
other => panic!("应得 Status 帧,实得 {other:?}"),
}
stop_server(&mut stream, handle);
}
/// 健康请求得 Health 帧(骨架阶段活着即健康)。
#[test]
fn health_response() {
let config = unique_paths("health");
let handle = start_server(config.clone());
let mut stream = connect(&config);
let response = send_and_recv(&mut stream, &ServerRequest::Health);
assert!(
matches!(response, ServerResponse::Health { healthy: true }),
"healthy 应为真:{response:?}"
);
stop_server(&mut stream, handle);
}
/// 信息请求得 Info 帧:socket 路径与配置一致、进程号为当前进程号。
#[test]
fn info_response() {
let config = unique_paths("info");
let handle = start_server(config.clone());
let mut stream = connect(&config);
let response = send_and_recv(&mut stream, &ServerRequest::Info);
match response {
ServerResponse::Info { version, socket_path, pid } => {
assert_eq!(version, env!("CARGO_PKG_VERSION"), "版本应与构建期版本一致");
assert_eq!(socket_path, config.socket_path.to_string_lossy().into_owned(), "socket 路径应与配置一致");
assert_eq!(pid, std::process::id(), "进程号应为当前进程号");
}
other => panic!("应得 Info 帧,实得 {other:?}"),
}
stop_server(&mut stream, handle);
}
/// 关闭请求得 Shutdown 帧,server 线程退出,sidecar 与 socket 文件均被删除。
#[test]
fn shutdown_response_and_cleanup() {
let config = unique_paths("shutdown");
let handle = start_server(config.clone());
let mut stream = connect(&config);
stop_server(&mut stream, handle);
assert!(!config.sidecar_path.exists(), "sidecar 文件应被删除");
assert!(!config.socket_path.exists(), "socket 文件应被删除");
}
/// 畸形帧得 Error 帧,连接保持可用(后续请求仍正常应答)。
#[test]
fn malformed_frame_error_response() {
let config = unique_paths("malformed");
let handle = start_server(config.clone());
let mut stream = connect(&config);
// 发非 JSON 帧:应得 Error 帧。
stream.write_all(b"not json at all\n").expect("写畸形帧应成功");
let mut reader = FrameReader::new(io::BufReader::new(&mut stream));
let frame = reader.read_frame().expect("读响应应成功").expect("应读到响应帧");
let response = protocol::parse_response(&frame).expect("响应帧解析应成功");
assert!(matches!(response, ServerResponse::Error { .. }), "应得 Error 帧:{response:?}");
// 连接保持可用:后续健康请求正常应答。
let response = send_and_recv(&mut stream, &ServerRequest::Health);
assert!(
matches!(response, ServerResponse::Health { healthy: true }),
"连接应保持可用:{response:?}"
);
stop_server(&mut stream, handle);
}
/// 执行请求得 EvalResult 帧:"1 + 1" 得完成值 "2",不 panic。
#[test]
fn eval_request_returns_value() {
let config = unique_paths("eval");
let handle = start_server(config.clone());
let mut stream = connect(&config);
let response = send_and_recv(
&mut stream,
&ServerRequest::Eval {
code: "1 + 1".into(),
max_steps: None,
},
);
assert!(
matches!(response, ServerResponse::EvalResult { ref value, .. } if value.as_deref() == Some("2")),
"应得完成值为 2 的 EvalResult 帧:{response:?}"
);
stop_server(&mut stream, handle);
}
/// 同一连接连续两帧(状态后版本)逐帧应答;同路径第二实例走交接路径接管
/// (第一 server 正常退出,第二实例接管 socket 路径)。
#[test]
fn multi_frame_same_connection_and_second_instance_takeover() {
let config = unique_paths("multi");
let handle = start_server(config.clone());
let mut stream = connect(&config);
// 连续两帧逐帧应答,顺序保持。
let first = send_and_recv(&mut stream, &ServerRequest::Status);
assert!(matches!(first, ServerResponse::Status { .. }), "应得 Status 帧:{first:?}");
let second = send_and_recv(&mut stream, &ServerRequest::Version);
assert!(matches!(second, ServerResponse::Version { .. }), "应得 Version 帧:{second:?}");
// 关闭测试连接,让第一 server 的在途排水快速归零。
drop(stream);
// 同路径第二实例:sidecar 已存在且 socket 存活,走交接路径接管。
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 秒内退出");
std::thread::sleep(Duration::from_millis(10));
}
let result = handle.join().expect("第一 server 线程应正常退出");
assert!(result.is_ok(), "第一 server 交接后应正常退出:{result:?}");
// 第二实例已接管:等 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 应为本进程身份");
let mut stream_b = connect(&config);
stop_server(&mut stream_b, handle_b);
}
/// 端到端并发:两条连接各发 3 个 eval,6 个响应帧全部正确且逐连接顺序与请求一致。
#[test]
fn concurrent_evals_across_connections() {
let config = unique_paths("concurrent_evals");
let handle = start_server(config.clone());
let mut stream_a = connect(&config);
let mut stream_b = connect(&config);
// 两条连接各发 3 个 eval,响应逐帧按请求顺序送达。
for code in ["1 + 1", "2 * 3", "10 - 4"] {
let response = send_and_recv(
&mut stream_a,
&ServerRequest::Eval {
code: code.into(),
max_steps: None,
},
);
assert!(
matches!(response, ServerResponse::EvalResult { ref value, .. } if value.is_some()),
"连接 A 应得完成值:{response:?}"
);
}
for code in ["21 / 7", "2 + 30", "100 % 9"] {
let response = send_and_recv(
&mut stream_b,
&ServerRequest::Eval {
code: code.into(),
max_steps: None,
},
);
assert!(
matches!(response, ServerResponse::EvalResult { ref value, .. } if value.is_some()),
"连接 B 应得完成值:{response:?}"
);
}
// 完成值逐条核对(顺序与请求一致)。
let expected_a = ["2", "6", "6"];
let expected_b = ["3", "32", "1"];
for (code, expected) in [("1 + 1", expected_a[0]), ("2 * 3", expected_a[1]), ("10 - 4", expected_a[2])] {
let response = send_and_recv(
&mut stream_a,
&ServerRequest::Eval {
code: code.into(),
max_steps: None,
},
);
match response {
ServerResponse::EvalResult { ref value, .. } => {
assert_eq!(value.as_deref(), Some(expected), "{code} 应得 {expected}")
}
other => panic!("应得 EvalResult 帧:{other:?}"),
}
}
for (code, expected) in [("21 / 7", expected_b[0]), ("2 + 30", expected_b[1]), ("100 % 9", expected_b[2])] {
let response = send_and_recv(
&mut stream_b,
&ServerRequest::Eval {
code: code.into(),
max_steps: None,
},
);
match response {
ServerResponse::EvalResult { ref value, .. } => {
assert_eq!(value.as_deref(), Some(expected), "{code} 应得 {expected}")
}
other => panic!("应得 EvalResult 帧:{other:?}"),
}
}
drop(stream_a);
stop_server(&mut stream_b, handle);
}
/// 控制请求不占 worker:一条连接发长 eval 占住 worker 期间,
/// 另一连接发 Status 立即返回(不受 eval 阻塞)。
#[test]
fn control_request_not_blocked_by_eval() {
let config = unique_paths("control_not_blocked");
let handle = start_server(config.clone());
let mut stream_a = connect(&config);
let mut stream_b = connect(&config);
// 连接 A 发长 eval(步数上限使执行有界,占住 worker 数百毫秒)。
stream_a
.write_all(
protocol::encode_request(&ServerRequest::Eval {
code: "for(;;){}".into(),
max_steps: Some(20_000_000),
})
.as_bytes(),
)
.expect("写长 eval 应成功");
// 连接 B 发 Status:控制请求直接应答,须在有界时间内返回。
let started = Instant::now();
let response = send_and_recv(&mut stream_b, &ServerRequest::Status);
assert!(started.elapsed() < Duration::from_secs(3), "Status 不应被 eval 阻塞");
assert!(matches!(response, ServerResponse::Status { .. }), "应得 Status 帧:{response:?}");
// 连接 A 的长 eval 最终得步数超限错误帧。
let mut reader = FrameReader::new(io::BufReader::new(&mut stream_a));
let frame = reader.read_frame().expect("读响应应成功").expect("应读到响应帧");
let response = protocol::parse_response(&frame).expect("响应帧解析应成功");
match response {
ServerResponse::EvalResult { ref error, .. } => {
let err = error.as_deref().expect("长 eval 应得步数超限错误");
assert!(err.contains("step limit"), "错误应含 step limit:{err}");
}
other => panic!("应得 EvalResult 帧:{other:?}"),
}
drop(stream_a);
stop_server(&mut stream_b, handle);
}
/// 关闭序列:在有在途 eval 时发关闭请求,server 线程正常退出,
/// sidecar 与 socket 文件均被删除。
#[test]
fn shutdown_with_in_flight_eval() {
let config = unique_paths("shutdown_inflight");
let handle = start_server(config.clone());
let mut stream_a = connect(&config);
let mut stream_b = connect(&config);
// 连接 A 发长 eval(在途);连接 B 发关闭请求。
stream_a
.write_all(
protocol::encode_request(&ServerRequest::Eval {
code: "for(;;){}".into(),
max_steps: Some(20_000_000),
})
.as_bytes(),
)
.expect("写长 eval 应成功");
let response = send_and_recv(&mut stream_b, &ServerRequest::Shutdown);
assert!(matches!(response, ServerResponse::Shutdown), "应得关闭确认帧:{response:?}");
// 关闭连接 A:在途连接线程写回响应后退出,在途归零。
drop(stream_a);
drop(stream_b);
let result = handle.join().expect("server 线程应正常退出");
assert!(result.is_ok(), "server 应正常退出:{result:?}");
assert!(!config.sidecar_path.exists(), "sidecar 文件应被删除");
assert!(!config.socket_path.exists(), "socket 文件应被删除");
}
/// 交接端到端:server A 运行中,server B 以同配置启动经 yield 路径接管;
/// A 正常退出,B 的 sidecar 为本进程身份(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());
// B 以同配置启动:认领得 AlreadyRunning 转交接路径。
let config_b = config.clone();
let handle_b = std::thread::spawn(move || run_server(&config_b));
// A 应在 30 秒内有界退出(排水加退出序列)。
let deadline = Instant::now() + Duration::from_secs(30);
while !handle_a.is_finished() {
assert!(Instant::now() < deadline, "A 应在 30 秒内退出");
std::thread::sleep(Duration::from_millis(10));
}
let result_a = handle_a.join().expect("server A 线程应正常退出");
assert!(result_a.is_ok(), "server A 交接后应正常退出:{result_a:?}");
// B 已接管:等 socket 文件出现(B 的绑定),再核对 sidecar 身份。
let deadline = Instant::now() + Duration::from_secs(30);
while !config.socket_path.exists() {
assert!(Instant::now() < deadline, "B 的 socket 文件应出现");
std::thread::sleep(Duration::from_millis(10));
}
let id = sidecar::read_identity(&config.sidecar_path).expect("B 的 sidecar 应可读");
assert_eq!(id.pid, std::process::id(), "B 的 sidecar 应为本进程身份");
// B 正常应答版本请求。
let mut stream = connect(&config);
let response = send_and_recv(&mut stream, &ServerRequest::Version);
assert!(matches!(response, ServerResponse::Version { .. }), "B 应应答版本请求:{response:?}");
stop_server(&mut stream, handle_b);
}
/// 旧 server 不受理交接:裸监听者加手写 sidecar 模拟旧 server,
/// 接受连接后回 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)。
let listener = UnixListener::bind(&config.socket_path).expect("绑定模拟旧 server 应成功");
sidecar::write_exclusive(
&sidecar::ServerIdentity::new(config.socket_path.to_str().unwrap()),
&config.sidecar_path,
)
.expect("写模拟 sidecar 应成功");
// 模拟连接处理:读一帧后回 Error 帧。
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::Error {
message: "未知协议类型:yield".into(),
});
let _ = stream.write_all(frame.as_bytes());
}
});
let err = run_server(&config).unwrap_err();
assert!(matches!(err, ServerError::YieldFailed(_)), "应判 YieldFailed:{err:?}");
assert!(config.sidecar_path.exists(), "模拟 sidecar 文件不应被删除");
assert!(config.socket_path.exists(), "模拟 socket 文件不应被删除");
}
/// 旧 server 挂起:模拟旧 server 接受连接后不回应;run_server 在约 5 秒
/// (读超时)后判 YieldFailed,且不触碰旧 server 的文件。
#[test]
fn yield_failed_when_old_server_hangs() {
let config = unique_paths("yield_hang");
let listener = UnixListener::bind(&config.socket_path).expect("绑定模拟旧 server 应成功");
sidecar::write_exclusive(
&sidecar::ServerIdentity::new(config.socket_path.to_str().unwrap()),
&config.sidecar_path,
)
.expect("写模拟 sidecar 应成功");
// 模拟连接处理:接受连接后挂住不回应(模拟挂起的旧 server)。
std::thread::spawn(move || {
if let Ok((stream, _)) = listener.accept() {
std::thread::sleep(Duration::from_secs(10));
drop(stream);
}
});
let started = Instant::now();
let err = run_server(&config).unwrap_err();
let elapsed = started.elapsed();
assert!(matches!(err, ServerError::YieldFailed(_)), "应判 YieldFailed:{err:?}");
assert!(elapsed >= Duration::from_secs(5), "应命中 5 秒读超时:{elapsed:?}");
assert!(config.sidecar_path.exists(), "模拟 sidecar 文件不应被删除");
assert!(config.socket_path.exists(), "模拟 socket 文件不应被删除");
}
/// --rm 加断开 EOF:eval 得完成值后客户端断开,server 在有界时间内
/// 正常退出,socket 文件被删除,全局 well-known 路径前后不变。
#[test]
fn rm_eval_then_eof_exits() {
let config = rm_config("eval_eof");
let well_known_sidecar = sidecar::well_known_sidecar_path();
let well_known_socket = sidecar::well_known_socket_path();
let sidecar_before = well_known_sidecar.exists();
let socket_before = well_known_socket.exists();
let handle = start_rm_server(config.clone());
let mut stream = UnixStream::connect(&config.socket_path).expect("连接 rm server 应成功");
let response = send_and_recv(
&mut stream,
&ServerRequest::Eval {
code: "1 + 1".into(),
max_steps: None,
},
);
assert!(
matches!(response, ServerResponse::EvalResult { ref value, .. } if value.as_deref() == Some("2")),
"应得完成值为 2 的 EvalResult 帧:{response:?}"
);
// 客户端断开:连接线程读到 EOF 退出,server 应在空闲超时加裕量内正常退出。
drop(stream);
let deadline = Instant::now() + config.idle_timeout + Duration::from_secs(5);
while !handle.is_finished() {
assert!(Instant::now() < deadline, "rm server 应在空闲超时加裕量内退出");
std::thread::sleep(Duration::from_millis(10));
}
let result = handle.join().expect("rm server 线程应正常退出");
assert!(result.is_ok(), "rm server 应正常退出:{result:?}");
assert!(!config.socket_path.exists(), "socket 文件应被删除");
assert_eq!(well_known_sidecar.exists(), sidecar_before, "全局 well-known sidecar 应前后不变");
assert_eq!(well_known_socket.exists(), socket_before, "全局 well-known socket 路径应前后不变");
}
/// --rm 空闲窗口一:不连接,空闲超时后 server 干净退出,socket 文件被删除。
///
/// 计时起点是 socket 文件出现(就绪探测轮询,至多 10 毫秒延迟),而空闲
/// 超时窗口在绑定后的内核与 worker 搭建完成时才起算,故实测值可略低于
/// 超时值,下界留 20 毫秒裕量。
#[test]
fn rm_idle_kill_without_connection() {
let config = rm_config("idle_no_conn");
let handle = start_rm_server(config.clone());
let started = Instant::now();
let result = handle.join().expect("rm server 线程应正常退出");
assert!(result.is_ok(), "rm server 应正常退出:{result:?}");
assert!(
started.elapsed() >= config.idle_timeout - Duration::from_millis(20),
"应在空闲超时后退出(留就绪探测裕量):{:?}",
started.elapsed()
);
assert!(!config.socket_path.exists(), "socket 文件应被删除");
}
/// --rm 空闲窗口二:连接建立后不发任何数据,读超时后 server 正常退出,
/// socket 文件被删除。
///
/// 计时起点是 socket 文件出现(就绪探测轮询,至多 10 毫秒延迟),而读
/// 超时窗口在连接线程建立流读超时时才起算,故实测值可略低于超时值,
/// 下界留 20 毫秒裕量。
#[test]
fn rm_idle_kill_silent_client() {
let config = rm_config("idle_silent");
let handle = start_rm_server(config.clone());
let stream = UnixStream::connect(&config.socket_path).expect("连接 rm server 应成功");
// 保持连接打开但不发数据:连接线程在读超时后退出。
let started = Instant::now();
let result = handle.join().expect("rm server 线程应正常退出");
assert!(result.is_ok(), "rm server 应正常退出:{result:?}");
assert!(
started.elapsed() >= config.idle_timeout - Duration::from_millis(20),
"应在读超时后退出(留就绪探测裕量):{:?}",
started.elapsed()
);
assert!(!config.socket_path.exists(), "socket 文件应被删除");
drop(stream);
}
/// --rm 单连接模型:首个连接正常应答后,第二个连接得连接被拒绝
/// (监听器已丢弃)。
#[test]
fn rm_second_connection_refused() {
let config = rm_config("second_refused");
let handle = start_rm_server(config.clone());
let mut stream = UnixStream::connect(&config.socket_path).expect("首个连接应成功");
let response = send_and_recv(&mut stream, &ServerRequest::Health);
assert!(
matches!(response, ServerResponse::Health { healthy: true }),
"应得 Health 帧:{response:?}"
);
// 监听器已丢弃:第二个连接得连接被拒绝。轮询至拒绝,容忍连接线程
// 建立与监听器丢弃之间的极小时序窗口(并发套件下调度延迟放大窗口)。
let deadline = Instant::now() + Duration::from_secs(5);
let mut second = UnixStream::connect(&config.socket_path);
while second.is_ok() {
assert!(Instant::now() < deadline, "第二个连接应被拒绝");
std::thread::sleep(Duration::from_millis(10));
second = UnixStream::connect(&config.socket_path);
}
assert!(second.is_err(), "第二个连接应被拒绝:{second:?}");
// 清理:发关闭请求停 server。
stop_server(&mut stream, handle);
}
/// --rm 关闭请求:发 Shutdown 控制帧得确认帧,server 正常退出,
/// socket 文件被删除(控制请求经共享分派置关闭标志)。
#[test]
fn rm_shutdown_request_exits() {
let config = rm_config("shutdown_req");
let handle = start_rm_server(config.clone());
let mut stream = UnixStream::connect(&config.socket_path).expect("连接 rm server 应成功");
stop_server(&mut stream, handle);
assert!(!config.socket_path.exists(), "socket 文件应被删除");
}
}