mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
feat: server start 改守护形态,spawn 前台子进程加就绪探测
This commit is contained in:
Generated
+18
-18
@@ -1160,7 +1160,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxide_builtins"
|
||||
version = "0.8.38"
|
||||
version = "0.8.39"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"fast-float",
|
||||
@@ -1185,7 +1185,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxide_bytecode"
|
||||
version = "0.8.38"
|
||||
version = "0.8.39"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"oxide_log",
|
||||
@@ -1193,7 +1193,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxide_cfg"
|
||||
version = "0.8.38"
|
||||
version = "0.8.39"
|
||||
dependencies = [
|
||||
"oxide_bytecode",
|
||||
"oxide_emit",
|
||||
@@ -1204,7 +1204,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxide_cli"
|
||||
version = "0.8.38"
|
||||
version = "0.8.39"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"bumpalo",
|
||||
@@ -1225,7 +1225,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxide_code_cache"
|
||||
version = "0.8.38"
|
||||
version = "0.8.39"
|
||||
dependencies = [
|
||||
"lru",
|
||||
"oxide_bytecode",
|
||||
@@ -1234,7 +1234,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxide_compiler"
|
||||
version = "0.8.38"
|
||||
version = "0.8.39"
|
||||
dependencies = [
|
||||
"oxide_bytecode",
|
||||
"oxide_cfg",
|
||||
@@ -1253,7 +1253,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxide_dce"
|
||||
version = "0.8.38"
|
||||
version = "0.8.39"
|
||||
dependencies = [
|
||||
"oxide_bytecode",
|
||||
"oxide_cfg",
|
||||
@@ -1267,7 +1267,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxide_emit"
|
||||
version = "0.8.38"
|
||||
version = "0.8.39"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"oxc_allocator",
|
||||
@@ -1282,7 +1282,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxide_ir"
|
||||
version = "0.8.38"
|
||||
version = "0.8.39"
|
||||
dependencies = [
|
||||
"oxide_bytecode",
|
||||
"oxide_log",
|
||||
@@ -1291,7 +1291,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxide_kernel"
|
||||
version = "0.8.38"
|
||||
version = "0.8.39"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"dashmap",
|
||||
@@ -1306,7 +1306,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxide_liveness"
|
||||
version = "0.8.38"
|
||||
version = "0.8.39"
|
||||
dependencies = [
|
||||
"oxide_bytecode",
|
||||
"oxide_cfg",
|
||||
@@ -1318,7 +1318,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxide_log"
|
||||
version = "0.8.38"
|
||||
version = "0.8.39"
|
||||
dependencies = [
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
@@ -1327,7 +1327,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxide_parser"
|
||||
version = "0.8.38"
|
||||
version = "0.8.39"
|
||||
dependencies = [
|
||||
"oxc_allocator",
|
||||
"oxc_ast",
|
||||
@@ -1342,7 +1342,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxide_regalloc"
|
||||
version = "0.8.38"
|
||||
version = "0.8.39"
|
||||
dependencies = [
|
||||
"oxide_bytecode",
|
||||
"oxide_cfg",
|
||||
@@ -1358,7 +1358,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxide_runtime_api"
|
||||
version = "0.8.38"
|
||||
version = "0.8.39"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
@@ -1370,7 +1370,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxide_test262"
|
||||
version = "0.8.38"
|
||||
version = "0.8.39"
|
||||
dependencies = [
|
||||
"oxide_bytecode",
|
||||
"oxide_compiler",
|
||||
@@ -1387,7 +1387,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxide_types"
|
||||
version = "0.8.38"
|
||||
version = "0.8.39"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"hashbrown 0.14.5",
|
||||
@@ -1399,7 +1399,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxide_vm"
|
||||
version = "0.8.38"
|
||||
version = "0.8.39"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"chrono",
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.8.38"
|
||||
version = "0.8.39"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
rust-version = "1.80"
|
||||
|
||||
+107
-31
@@ -110,7 +110,7 @@ enum Commands {
|
||||
/// server 子命令:持久 server 进程管理与 --rm 独立模式。
|
||||
#[derive(Subcommand)]
|
||||
enum ServerCommands {
|
||||
/// 启动 server。持久形态前台常驻(阻塞至关闭请求、信号或 yield 触发退出);
|
||||
/// 启动 server。持久形态为守护形态(spawn 脱离的后台进程,就绪探测通过后返回);
|
||||
/// --rm 为独立形态(进程唯一 socket、不注册 sidecar、空闲超时或断开即退出)。
|
||||
Start {
|
||||
/// 独立模式:进程唯一 socket 路径,不注册 sidecar,空闲超时或断开即自动退出。
|
||||
@@ -122,6 +122,10 @@ enum ServerCommands {
|
||||
/// 常驻 worker 线程数;缺省取宿主核数。
|
||||
#[arg(long)]
|
||||
workers: Option<u32>,
|
||||
/// 前台形态:当前进程直接运行持久 server(守护形态 spawn 的子进程用,
|
||||
/// 不向用户暴露)。
|
||||
#[arg(long, hide = true)]
|
||||
foreground: bool,
|
||||
},
|
||||
/// 向持久 server 发送关闭请求。
|
||||
Stop,
|
||||
@@ -212,8 +216,10 @@ fn main() -> ExitCode {
|
||||
/// 其余两臂为 not_implemented 占位。
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. Start:rm 为假调持久 server 入口(well-known 路径),rm 为真调独立
|
||||
/// 模式入口(进程唯一路径);`--workers` 为 Some 时覆盖 worker 数。
|
||||
/// 1. Start:rm 为真调独立模式入口(进程唯一路径);foreground 为真当前进程
|
||||
/// 直接运行持久 server(守护形态 spawn 的子进程用);否则 spawn 脱离的
|
||||
/// server 子进程并就绪探测通过后返回(守护形态,well-known 路径);
|
||||
/// `--workers` 为 Some 时覆盖 worker 数。
|
||||
/// 2. Cleanup:调 well-known 路径清理入口,四态结果映射退出码(无残留与
|
||||
/// 已清理退 0,拒绝与杀进程失败退 1)并打印结果。
|
||||
/// 3. 五个控制臂(version/status/health/info/stop):发对应控制请求,
|
||||
@@ -227,12 +233,13 @@ fn main() -> ExitCode {
|
||||
/// - 全局旗标 -v / -q / --profile 在 server 臂为空操作。
|
||||
///
|
||||
/// # 副作用
|
||||
/// - start 阻塞当前进程至 server 退出(持久形态为前台常驻)。
|
||||
/// - start 持久形态 spawn 脱离进程,就绪探测通过后返回(server 由 init
|
||||
/// 收领继续运行)。
|
||||
/// - cleanup 可能向 sidecar 记录的进程发 SIGTERM 并删除残留文件。
|
||||
/// - 控制臂向 server 建立并关闭一条 Unix socket 连接。
|
||||
fn server_command(command: ServerCommands) -> ExitCode {
|
||||
match command {
|
||||
ServerCommands::Start { rm, idle_timeout, workers } => {
|
||||
ServerCommands::Start { rm, idle_timeout, workers, foreground } => {
|
||||
if rm {
|
||||
let mut config = RmServerConfig {
|
||||
idle_timeout: Duration::from_secs(idle_timeout),
|
||||
@@ -242,12 +249,14 @@ fn server_command(command: ServerCommands) -> ExitCode {
|
||||
config.worker_count = n as usize;
|
||||
}
|
||||
start_result(run_server_rm(&config))
|
||||
} else {
|
||||
} else if foreground {
|
||||
let mut config = ServerConfig::well_known();
|
||||
if let Some(n) = workers {
|
||||
config.worker_count = n as usize;
|
||||
}
|
||||
start_result(run_server(&config))
|
||||
} else {
|
||||
start_daemon(workers)
|
||||
}
|
||||
}
|
||||
ServerCommands::Cleanup => match well_known_cleanup() {
|
||||
@@ -322,6 +331,95 @@ fn control_arm(request: &ServerRequest, render: fn(&ServerResponse) -> Option<St
|
||||
}
|
||||
}
|
||||
|
||||
/// start 持久形态:spawn 脱离的前台 server 子进程(`server start
|
||||
/// --foreground`),就绪探测通过后打印返回。
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. spawn 脱离的前台 server 子进程(标准流全空、不等待);spawn 失败以
|
||||
/// 红色打印退 1。
|
||||
/// 2. 就绪探测:轮询健康请求直至得 healthy(100 毫秒间隔、10 秒截止);
|
||||
/// 超时以红色打印退 1。
|
||||
/// 3. 打印「server 已启动」退 0。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - sidecar 文件由被拉起的 server 进程自己写(先认领再绑定 socket 的启动
|
||||
/// 顺序契约在 server 进程内执行),本命令不写 sidecar。
|
||||
/// - 已有存活 server 时再跑本命令:第二个 spawn 的子进程在认领阶段被拒
|
||||
/// (同版本不触碰文件),探测看到第一个 server 即通过,命令退 0(天然幂等)。
|
||||
///
|
||||
/// # 副作用
|
||||
/// - server 为脱离进程,CLI 退出后由 init 收领继续运行。
|
||||
fn start_daemon(workers: Option<u32>) -> ExitCode {
|
||||
// spawn:脱离子进程,标准流全空,不等待。
|
||||
if let Err(err) = spawn_detached_server(workers) {
|
||||
eprintln!("{}", Red.paint(format!("spawn server 进程失败:{err}")));
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
// 就绪探测:单循环全覆盖「sidecar 出现 + 健康请求通过」。
|
||||
if let Err(msg) = wait_server_ready(Duration::from_secs(10)) {
|
||||
eprintln!("{}", Red.paint(format!("{msg}(可用 `oxide server cleanup` 人工检查)")));
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
println!("server 已启动");
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
|
||||
/// spawn 脱离的前台 server 子进程(`server start --foreground`):标准流全空,不等待。
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. 取当前可执行文件路径。
|
||||
/// 2. 构造命令 `server start --foreground` 加可选 `--workers N`。
|
||||
/// 3. 三个标准流置空并 spawn。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - 必须带 `--foreground`:守护形态 spawn 的是前台 server 入口,不带该旗标会
|
||||
/// 再次进入守护形态形成递归 spawn。
|
||||
///
|
||||
/// # 副作用
|
||||
/// - 创建脱离的子进程,调用方退出后由 init 收领继续运行。
|
||||
fn spawn_detached_server(workers: Option<u32>) -> std::io::Result<std::process::Child> {
|
||||
let exe = std::env::current_exe()?;
|
||||
let mut cmd = Command::new(exe);
|
||||
cmd.args(["server", "start", "--foreground"]);
|
||||
if let Some(n) = workers {
|
||||
cmd.args(["--workers", &n.to_string()]);
|
||||
}
|
||||
cmd.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
}
|
||||
|
||||
/// 就绪探测:轮询健康请求直至得 healthy。
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. 循环发健康请求,得 healthy 即返回成功。
|
||||
/// 2. 超过截止返回含超时秒数的消息。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - 控制客户端发请求前先读 sidecar 取 socket 路径,sidecar 缺失时自然走
|
||||
/// 连接失败分支,「sidecar 出现」与「健康请求通过」一个循环全覆盖
|
||||
/// (socket 文件出现蕴含监听器已绑定,健康请求通过蕴含 accept 循环在运行)。
|
||||
///
|
||||
/// # 副作用
|
||||
/// - 每轮建立并关闭一条 Unix socket 连接。
|
||||
fn wait_server_ready(deadline: Duration) -> Result<(), String> {
|
||||
let end = Instant::now() + deadline;
|
||||
loop {
|
||||
let ready =
|
||||
matches!(send_control_request(&ServerRequest::Health), Ok(ServerResponse::Health { healthy: true }));
|
||||
if ready {
|
||||
return Ok(());
|
||||
}
|
||||
if Instant::now() >= end {
|
||||
return Err(format!("server 未在 {} 秒内就绪", deadline.as_secs()));
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
|
||||
/// restart 子命令:向存活 server 发关闭请求,等其退出后拉起新 server 进程。
|
||||
///
|
||||
/// # 步骤
|
||||
@@ -360,38 +458,16 @@ fn restart_command() -> ExitCode {
|
||||
}
|
||||
|
||||
// 拉起新 server:脱离进程,标准流全空,不等待。
|
||||
let exe = match std::env::current_exe() {
|
||||
Ok(path) => path,
|
||||
Err(err) => {
|
||||
eprintln!("{}", Red.paint(format!("获取当前可执行文件路径失败:{err}")));
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
if let Err(err) = Command::new(exe)
|
||||
.args(["server", "start"])
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
{
|
||||
if let Err(err) = spawn_detached_server(None) {
|
||||
eprintln!("{}", Red.paint(format!("拉起新 server 失败:{err}")));
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
// 就绪探测:健康请求通过即新 server 的监听与 accept 循环在运行。
|
||||
let deadline = Instant::now() + Duration::from_secs(10);
|
||||
loop {
|
||||
let ready =
|
||||
matches!(send_control_request(&ServerRequest::Health), Ok(ServerResponse::Health { healthy: true }));
|
||||
if ready {
|
||||
break;
|
||||
}
|
||||
if Instant::now() >= deadline {
|
||||
eprintln!("{}", Red.paint("新 server 未在 10 秒内就绪"));
|
||||
if let Err(msg) = wait_server_ready(Duration::from_secs(10)) {
|
||||
eprintln!("{}", Red.paint(format!("新 {msg}")));
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
|
||||
println!("已重启");
|
||||
ExitCode::SUCCESS
|
||||
|
||||
@@ -99,8 +99,9 @@ fn server_cleanup_exit_0() {
|
||||
assert!(!sidecar::well_known_sidecar_path().exists(), "sidecar 文件应不存在");
|
||||
}
|
||||
|
||||
/// start 端到端:起真实子进程,well-known 路径连接,版本帧断言构建期版本,
|
||||
/// 关闭帧后子进程退 0,socket 与 sidecar 文件被删。
|
||||
/// start 端到端:CLI 即刻返回(server 是脱离的孙进程),断言退 0 且打印
|
||||
/// 「已启动」,well-known 路径连接,版本帧断言构建期版本,关闭帧后 server
|
||||
/// 进程退出,socket 与 sidecar 文件被删。
|
||||
#[test]
|
||||
fn server_start_e2e() {
|
||||
let _guard = WELL_KNOWN_LOCK.lock().expect("全局路径锁不应中毒");
|
||||
@@ -110,18 +111,15 @@ fn server_start_e2e() {
|
||||
// 探活先行:存活 server 占用全局路径时 panic,不抢占。
|
||||
assert!(!sidecar::is_server_alive(&socket), "存活 server 占用 well-known 路径,测试不抢占");
|
||||
|
||||
// worker 数压到 2:避免按宿主核数预热的成本。
|
||||
let mut child = Command::new(env!("CARGO_BIN_EXE_oxide"))
|
||||
.args(["server", "start", "--workers", "2"])
|
||||
.spawn()
|
||||
.expect("oxide server start 应可启动");
|
||||
// start 为守护形态:CLI 即刻返回,server 是脱离的孙进程;worker 数压到 2
|
||||
// 避免按宿主核数预热的成本。
|
||||
let output = oxide(&["server", "start", "--workers", "2"]);
|
||||
assert_eq!(output.status.code(), Some(0), "start 应退 0");
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(stdout.contains("已启动"), "start 应打印「已启动」:{stdout}");
|
||||
|
||||
// 轮询 socket 文件出现(10 秒截止)判就绪。
|
||||
let deadline = Instant::now() + Duration::from_secs(10);
|
||||
while !socket.exists() {
|
||||
assert!(Instant::now() < deadline, "socket 文件未出现");
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
// 读 sidecar 取 server 进程号。
|
||||
let id = sidecar::read_identity(&sidecar_path).expect("读 sidecar 应成功");
|
||||
|
||||
// 版本请求帧:版本应等于构建期版本。
|
||||
let mut stream = UnixStream::connect(&socket).expect("连接 server 应成功");
|
||||
@@ -138,22 +136,52 @@ fn server_start_e2e() {
|
||||
assert!(matches!(response, ServerResponse::Shutdown), "应得关闭确认帧:{response:?}");
|
||||
drop(stream);
|
||||
|
||||
// 等子进程以退出码 0 退出(30 秒截止)。
|
||||
// 轮询 sidecar 文件消失(30 秒截止、50 毫秒间隔)判 server 进程已退出。
|
||||
let deadline = Instant::now() + Duration::from_secs(30);
|
||||
loop {
|
||||
match child.try_wait() {
|
||||
Ok(Some(status)) => {
|
||||
assert_eq!(status.code(), Some(0), "server 应以退出码 0 退出:{status:?}");
|
||||
break;
|
||||
}
|
||||
Ok(None) => {
|
||||
while sidecar_path.exists() {
|
||||
assert!(Instant::now() < deadline, "server 应在有界时间内退出");
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
Err(err) => panic!("等待子进程失败:{err}"),
|
||||
}
|
||||
|
||||
assert!(!socket.exists(), "socket 文件应被删除");
|
||||
assert!(!sidecar_path.exists(), "sidecar 文件应被删除");
|
||||
assert!(!sidecar::pid_alive(id.pid), "server 进程应已退出");
|
||||
}
|
||||
|
||||
/// start 双启动幂等:已有存活 server 时再跑 start,断言第二次退 0 且打印
|
||||
/// 「已启动」(第二个 spawn 的子进程认领被拒退 1 不触碰文件,探测看到第一个
|
||||
/// server 即通过)。
|
||||
#[test]
|
||||
fn server_start_when_running() {
|
||||
let _guard = WELL_KNOWN_LOCK.lock().expect("全局路径锁不应中毒");
|
||||
let socket = sidecar::well_known_socket_path();
|
||||
let sidecar_path = sidecar::well_known_sidecar_path();
|
||||
|
||||
// 探活先行:存活 server 占用全局路径时 panic,不抢占。
|
||||
assert!(!sidecar::is_server_alive(&socket), "存活 server 占用 well-known 路径,测试不抢占");
|
||||
|
||||
// 首次 start:CLI 即刻返回,server 是脱离的孙进程。
|
||||
let output = oxide(&["server", "start", "--workers", "2"]);
|
||||
assert_eq!(output.status.code(), Some(0), "首次 start 应退 0");
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(stdout.contains("已启动"), "首次 start 应打印「已启动」:{stdout}");
|
||||
|
||||
// 二次 start:第二个 spawn 的子进程认领被拒,探测看到第一个 server 即通过。
|
||||
let output = oxide(&["server", "start", "--workers", "2"]);
|
||||
assert_eq!(output.status.code(), Some(0), "二次 start 应退 0");
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(stdout.contains("已启动"), "二次 start 应打印「已启动」:{stdout}");
|
||||
|
||||
// 收尾:发关闭请求,等 sidecar 消失。
|
||||
let mut stream = UnixStream::connect(&socket).expect("连接 server 应成功");
|
||||
let response = send_and_recv(&mut stream, &ServerRequest::Shutdown);
|
||||
assert!(matches!(response, ServerResponse::Shutdown), "应得关闭确认帧:{response:?}");
|
||||
drop(stream);
|
||||
let deadline = Instant::now() + Duration::from_secs(30);
|
||||
while sidecar_path.exists() {
|
||||
assert!(Instant::now() < deadline, "server 应在有界时间内退出");
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
assert!(!socket.exists(), "socket 文件应被删除");
|
||||
assert!(!sidecar_path.exists(), "sidecar 文件应被删除");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user