Skip to main content

sz_rust_core/
runtime.rs

1//! SZ-Rust Runtime — Swoole/Worker 适配主入口
2//!
3//! ## PHP 对齐
4//!
5//! 对齐 PHP `topthink/think-swoole` 的运行时模型:
6//!
7//! | PHP Swoole | Rust SZ-Rust | 说明 |
8//! |------------|--------------|------|
9//! | `Swoole\Runtime::enableCoroutine()` | `SzRuntime::block_on(fut)` | 协程入口 |
10//! | `swoole_cpu_num()` | `num_cpus::get()` | 默认 worker 数 |
11//! | `worker_num` 配置项 | `SzRuntime::with_worker_threads(n)` | 自定义 worker 数 |
12//! | `Swoole\Process::signal()` | `tokio::signal` + `CancellationToken` | 信号处理 |
13//! | `Swoole\Timer::tick()` | `tokio::time::interval` + `tokio::select!` | 定时任务 |
14//! | `swoole_event::defer()` | `SzRuntime::spawn(fut)` | 异步任务 |
15//!
16//! ## 模块结构
17//!
18//! | 模块 | 对齐 PHP | 子任务 |
19//! |------|---------|--------|
20//! | `runtime::worker` | `worker_num` 配置 | 9.2 |
21//! | `runtime::spawn` | `swoole_event::defer` | 9.3 |
22//! | `runtime::queue` | `think-queue` 消费者 | 9.4 |
23//! | `runtime::mqtt` | 长连接 | 9.5 |
24//! | `runtime::websocket` | `think-worker` | 9.6 |
25//! | `runtime::scheduler` | `Crontab` | 9.7 |
26//! | `runtime::shutdown` | 优雅关闭 | 9.8 |
27//! | `runtime::signal` | `SIGTERM/SIGINT` | 9.9 |
28//!
29//! ## 关键决策
30//!
31//! - **不修改 `sz-orm-scheduler` 内部 API**:保持 `CronScheduler::start()`/`stop()` 向后兼容,
32//!   在 sz-rust 侧用 `tokio::time::interval` + `try_fire_due()` 重写循环。
33//! - **使用 `CancellationToken` 统一关闭广播**:替代 `AtomicBool` + `oneshot`,支持父子层级。
34//! - **双平台信号处理**:Unix 用 `SignalKind::terminate/interrupt`,Windows 用 `ctrl_c/ctrl_close`。
35
36pub mod mqtt;
37pub mod queue;
38pub mod scheduler;
39pub mod shutdown;
40pub mod signal;
41pub mod spawn;
42pub mod websocket;
43pub mod worker;
44
45// P2: Addon 热加载探索(可选 feature: hot-reload)
46#[cfg(feature = "hot-reload")]
47pub mod hot_reload;
48
49pub use mqtt::{MqttRuntime, MqttRuntimeConfig};
50pub use queue::{QueueConsumer, QueueRuntime, QueueRuntimeConfig};
51pub use scheduler::SchedulerRuntime;
52pub use shutdown::GracefulShutdown;
53pub use signal::shutdown_signal;
54pub use spawn::spawn_with_token;
55pub use websocket::{WebSocketRuntime, WebSocketRuntimeConfig};
56pub use worker::WorkerConfig;
57
58use std::future::Future;
59use std::time::Duration;
60
61use tokio_util::sync::CancellationToken;
62
63/// SZ-Rust 异步运行时
64///
65/// 对齐 PHP `think-swoole` 的运行时模型:基于 tokio multi_thread runtime,
66/// worker 数量默认 = CPU 核数(对齐 `swoole_cpu_num()`)。
67///
68/// ## 设计
69///
70/// - 封装 `tokio::runtime::Runtime`,对外暴露 `block_on` / `spawn` 入口
71/// - 持有 `CancellationToken`,所有后台任务通过 `shutdown_token()` 监听关闭信号
72/// - `shutdown_timeout` 触发关闭流程:先 `cancel()` 通知所有任务,等待超时后 drop runtime
73///
74/// ## 用法
75///
76/// ```rust,ignore
77/// use sz_rust_core::runtime::SzRuntime;
78/// use std::time::Duration;
79///
80/// let rt = SzRuntime::new();
81/// assert!(rt.worker_threads() > 0);
82///
83/// // spawn 后台任务
84/// let handle = rt.spawn(async { 42 });
85/// assert_eq!(rt.block_on(handle).unwrap(), 42);
86///
87/// // 优雅关闭
88/// assert!(rt.shutdown_timeout(Duration::from_millis(100)));
89/// ```
90pub struct SzRuntime {
91    /// 内部 tokio runtime(multi_thread)
92    runtime: tokio::runtime::Runtime,
93    /// worker 线程数(对齐 swoole `worker_num`)
94    worker_threads: usize,
95    /// 关闭令牌:所有后台任务通过 `shutdown_token()` 获取子 token 监听关闭
96    shutdown_token: CancellationToken,
97}
98
99impl SzRuntime {
100    /// 创建 SzRuntime,worker_threads = `num_cpus::get()`(对齐 `swoole_cpu_num()`)
101    pub fn new() -> Self {
102        Self::with_worker_threads(num_cpus::get())
103    }
104
105    /// 创建 SzRuntime,自定义 worker_threads(对齐 `worker_num` 配置项)
106    ///
107    /// - `worker_threads = 0` 会被强制为 1
108    pub fn with_worker_threads(worker_threads: usize) -> Self {
109        let n = worker_threads.max(1);
110        let runtime = tokio::runtime::Builder::new_multi_thread()
111            .worker_threads(n)
112            .enable_all()
113            .thread_name("sz-rust-worker")
114            .build()
115            .expect("Failed to create tokio runtime");
116        Self {
117            runtime,
118            worker_threads: n,
119            shutdown_token: CancellationToken::new(),
120        }
121    }
122
123    /// 获取 worker 线程数
124    pub fn worker_threads(&self) -> usize {
125        self.worker_threads
126    }
127
128    /// 获取关闭令牌的克隆
129    ///
130    /// 后台任务通过 `rt.shutdown_token().cancelled().await` 监听关闭信号。
131    pub fn shutdown_token(&self) -> CancellationToken {
132        self.shutdown_token.clone()
133    }
134
135    /// 在 runtime 上 spawn 异步任务(对齐 `swoole_event::defer`)
136    ///
137    /// 返回 `JoinHandle`,可 await 获取结果。
138    pub fn spawn<F>(&self, future: F) -> tokio::task::JoinHandle<F::Output>
139    where
140        F: Future + Send + 'static,
141        F::Output: Send + 'static,
142    {
143        self.runtime.spawn(future)
144    }
145
146    /// 在 runtime 上阻塞运行 future(对齐 `Swoole\Runtime::enableCoroutine` 入口)
147    ///
148    /// 调用线程会阻塞直到 future 完成。
149    pub fn block_on<F>(&self, future: F) -> F::Output
150    where
151        F: Future,
152    {
153        self.runtime.block_on(future)
154    }
155
156    /// 触发优雅关闭:`cancel()` 通知所有后台任务,等待 `timeout` 后 drop runtime
157    ///
158    /// - 返回 `true`:runtime 已关闭
159    /// - 注意:Drop runtime 时 tokio 会等待所有任务完成,超时由调用方控制
160    pub fn shutdown_timeout(self, timeout: Duration) -> bool {
161        self.shutdown_token.cancel();
162        // 给后台任务响应关闭信号的时间
163        self.runtime.block_on(async {
164            let _ = tokio::time::timeout(timeout, async {
165                // 等待一小段时间让任务响应 cancel
166                tokio::time::sleep(Duration::from_millis(10)).await;
167            })
168            .await;
169        });
170        // Drop runtime 会等待所有任务完成(可能阻塞)
171        drop(self.runtime);
172        true
173    }
174
175    /// 获取内部 runtime 句柄(用于在不持有 SzRuntime 时 spawn 任务)
176    pub fn handle(&self) -> tokio::runtime::Handle {
177        self.runtime.handle().clone()
178    }
179}
180
181impl Default for SzRuntime {
182    fn default() -> Self {
183        Self::new()
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[test]
192    fn test_new_default_worker_threads() {
193        let rt = SzRuntime::new();
194        assert_eq!(rt.worker_threads(), num_cpus::get());
195    }
196
197    #[test]
198    fn test_with_worker_threads_custom() {
199        let rt = SzRuntime::with_worker_threads(2);
200        assert_eq!(rt.worker_threads(), 2);
201    }
202
203    #[test]
204    fn test_with_worker_threads_zero_falls_back_to_one() {
205        let rt = SzRuntime::with_worker_threads(0);
206        assert_eq!(rt.worker_threads(), 1);
207    }
208
209    #[test]
210    fn test_spawn_and_block_on() {
211        let rt = SzRuntime::with_worker_threads(1);
212        let handle = rt.spawn(async { 42 });
213        let result = rt.block_on(handle).unwrap();
214        assert_eq!(result, 42);
215    }
216
217    #[test]
218    fn test_block_on_directly() {
219        let rt = SzRuntime::with_worker_threads(1);
220        let result = rt.block_on(async { 100 });
221        assert_eq!(result, 100);
222    }
223
224    #[test]
225    fn test_shutdown_token_cancellation() {
226        let rt = SzRuntime::with_worker_threads(1);
227        let token = rt.shutdown_token();
228        assert!(!token.is_cancelled());
229        assert!(rt.shutdown_timeout(Duration::from_millis(50)));
230        // shutdown_timeout 已消费 rt,token 已 cancel
231        assert!(token.is_cancelled());
232    }
233
234    #[test]
235    fn test_spawn_with_token_cancellation() {
236        let rt = SzRuntime::with_worker_threads(1);
237        let token = rt.shutdown_token();
238        let handle = rt.spawn(async move {
239            // 模拟后台任务:监听 cancel
240            token.cancelled().await;
241            99
242        });
243        // 触发关闭
244        let token2 = rt.shutdown_token();
245        token2.cancel();
246        let result = rt.block_on(handle).unwrap();
247        assert_eq!(result, 99);
248    }
249
250    #[test]
251    fn test_handle_can_spawn() {
252        let rt = SzRuntime::with_worker_threads(1);
253        let handle = rt.handle();
254        let task = handle.spawn(async { 7 });
255        let result = rt.block_on(task).unwrap();
256        assert_eq!(result, 7);
257    }
258
259    #[test]
260    fn test_default_impl_equals_new() {
261        let rt1 = SzRuntime::default();
262        let rt2 = SzRuntime::new();
263        assert_eq!(rt1.worker_threads(), rt2.worker_threads());
264    }
265
266    #[test]
267    fn test_multiple_runtime_instances() {
268        // 验证可以创建多个独立的 runtime 实例
269        let rt1 = SzRuntime::with_worker_threads(1);
270        let rt2 = SzRuntime::with_worker_threads(1);
271        let h1 = rt1.spawn(async { 1 });
272        let h2 = rt2.spawn(async { 2 });
273        assert_eq!(rt1.block_on(h1).unwrap(), 1);
274        assert_eq!(rt2.block_on(h2).unwrap(), 2);
275    }
276}