//! 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` 是 Send/Sync);池本体不进上下文 /// (Vm 不跨线程,池归各 worker 线程所有),状态响应读共享聚合计数器。 struct ServerContext { // 只持不读:余留连接线程(在途排水超时后仍存活)持本上下文,内核句柄 // 随上下文存活,不悬垂。 #[allow(dead_code)] kernel: Arc, started_at: Instant, shutdown: Arc, in_flight: Arc, pool_counters: Arc, 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, 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, router: &Arc, worker_handles: Vec>, kernel: Arc, 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) { 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) -> 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) -> String { if let Some(s) = payload.downcast_ref::<&str>() { return (*s).to_string(); } if let Some(s) = payload.downcast_ref::() { return s.clone(); } "non-string payload".into() } /// 绑定监听器:失败且路径存在时删 stale socket 文件重试一次。 /// /// # 边界与前提 /// - stale socket 文件是崩溃残留(文件存在但无监听者)。 /// /// # 副作用 /// - 可能删除 socket 文件。 fn bind_listener_path(path: &Path) -> Result { 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 { 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, router: &Arc, ) -> 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, 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> { 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>) { 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> { 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 文件应被删除"); } }