feat: server 信号处理,SIGINT/SIGTERM 置位关闭标志走优雅退出

This commit is contained in:
2026-10-06 15:53:21 +00:00
parent a08652252b
commit 20e71417ee
9 changed files with 423 additions and 126 deletions
@@ -0,0 +1,71 @@
//! 信号处理集成测试(SIGINT):真实信号路径触发优雅退出。
//!
//! 独立进程、进程内唯一 server 与唯一信号处理器:`ctrlc` 的处理器注册是
//! 进程级一次性的,信号投递无歧义,测试确定。SIGTERM 路径见
//! `server_signal_term.rs`(同型,独立进程)。
use std::process::Command;
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
use oxide_cli::server::server::{run_server, ServerConfig, ServerError};
/// 唯一临时路径(进程号加测试名):单进程内唯一,进程间以进程号隔离。
/// worker 数取小值 2:与既有单测同口径,大值徒增预热成本。
fn unique_paths(test_name: &str) -> ServerConfig {
let dir = std::env::temp_dir().join(format!(
"oxide_server_signal_int_{}_{}",
std::process::id(),
test_name
));
std::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
}
/// 给本测试进程发 SIGINT(`kill` 二进制与 sidecar 的 `pid_alive` 同一口径)。
fn send_sigint() {
let status = Command::new("kill")
.args(["-INT", &std::process::id().to_string()])
.status()
.expect("kill 应可执行");
assert!(status.success(), "kill -INT 应成功:{status:?}");
}
/// 等 server 线程退出,断言正常退出与 sidecar、socket 文件均被删除。
fn wait_shutdown(config: &ServerConfig, handle: JoinHandle<Result<(), ServerError>>) {
let deadline = Instant::now() + Duration::from_secs(30);
while !handle.is_finished() {
assert!(Instant::now() < deadline, "server 线程应在有界时间内退出");
std::thread::sleep(Duration::from_millis(10));
}
let result = handle.join().expect("server 线程应正常退出");
assert!(result.is_ok(), "server 应正常退出:{result:?}");
assert!(!config.sidecar_path.exists(), "sidecar 文件应被删除");
assert!(!config.socket_path.exists(), "socket 文件应被删除");
}
/// SIGINT 触发优雅退出:在途归零、删 sidecar 与 socket 文件。
#[test]
fn sigint_graceful_shutdown() {
let config = unique_paths("sigint");
let handle = start_server(config.clone());
send_sigint();
wait_shutdown(&config, handle);
}
@@ -0,0 +1,71 @@
//! 信号处理集成测试(SIGTERM):真实信号路径触发优雅退出。
//!
//! 独立进程、进程内唯一 server 与唯一信号处理器:`ctrlc` 的处理器注册是
//! 进程级一次性的,信号投递无歧义,测试确定。SIGINT 路径见
//! `server_signal_int.rs`(同型,独立进程)。
use std::process::Command;
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
use oxide_cli::server::server::{run_server, ServerConfig, ServerError};
/// 唯一临时路径(进程号加测试名):单进程内唯一,进程间以进程号隔离。
/// worker 数取小值 2:与既有单测同口径,大值徒增预热成本。
fn unique_paths(test_name: &str) -> ServerConfig {
let dir = std::env::temp_dir().join(format!(
"oxide_server_signal_term_{}_{}",
std::process::id(),
test_name
));
std::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
}
/// 给本测试进程发 SIGTERM(`kill` 二进制与 sidecar 的 `pid_alive` 同一口径)。
fn send_sigterm() {
let status = Command::new("kill")
.args(["-TERM", &std::process::id().to_string()])
.status()
.expect("kill 应可执行");
assert!(status.success(), "kill -TERM 应成功:{status:?}");
}
/// 等 server 线程退出,断言正常退出与 sidecar、socket 文件均被删除。
fn wait_shutdown(config: &ServerConfig, handle: JoinHandle<Result<(), ServerError>>) {
let deadline = Instant::now() + Duration::from_secs(30);
while !handle.is_finished() {
assert!(Instant::now() < deadline, "server 线程应在有界时间内退出");
std::thread::sleep(Duration::from_millis(10));
}
let result = handle.join().expect("server 线程应正常退出");
assert!(result.is_ok(), "server 应正常退出:{result:?}");
assert!(!config.sidecar_path.exists(), "sidecar 文件应被删除");
assert!(!config.socket_path.exists(), "socket 文件应被删除");
}
/// SIGTERM 触发优雅退出:在途归零、删 sidecar 与 socket 文件。
#[test]
fn sigterm_graceful_shutdown() {
let config = unique_paths("sigterm");
let handle = start_server(config.clone());
send_sigterm();
wait_shutdown(&config, handle);
}