Skip to main content

sz_rust_core/runtime/
spawn.rs

1//! tokio::spawn 异步任务
2//!
3//! ## PHP 对齐
4//!
5//! 对齐 PHP Swoole 的异步任务 spawn 机制:
6//!
7//! | PHP Swoole | Rust | 说明 |
8//! |------------|------|------|
9//! | `swoole_event::defer($callback)` | `tokio::spawn(fut)` | 延迟执行异步任务 |
10//! | `Swoole\Coroutine::create($callback)` | `tokio::spawn(fut)` | 创建协程 |
11//! | `Swoole\Coroutine::go()` | `tokio::spawn(fut)` | go() 别名 |
12//! | `Swoole\Timer::tick($ms, $callback)` | `tokio::time::interval + spawn` | 定时任务 |
13//! | `Swoole\Timer::after($ms, $callback)` | `tokio::time::sleep + spawn` | 一次性延迟任务 |
14//!
15//! ## 设计
16//!
17//! 提供 `spawn_with_token` helper:自动注入 `CancellationToken`,任务可监听关闭信号优雅退出。
18
19use std::future::Future;
20use std::time::Duration;
21
22use tokio_util::sync::CancellationToken;
23
24/// spawn 一个异步任务并注入 CancellationToken
25///
26/// 对齐 `Swoole\Coroutine::create()`,额外提供关闭信号监听。
27///
28/// ## 参数
29///
30/// - `token`:关闭令牌,任务通过 `token.cancelled().await` 监听关闭
31/// - `future`:异步任务 future
32///
33/// ## 返回
34///
35/// `JoinHandle<T>`,调用方可 await 获取结果
36///
37/// ## 用法
38///
39/// ```rust,ignore
40/// use sz_rust_core::runtime::spawn_with_token;
41/// use tokio_util::sync::CancellationToken;
42///
43/// let token = CancellationToken::new();
44/// let handle = spawn_with_token(token.clone(), async move {
45///     // 任务执行
46///     42
47/// });
48/// ```
49pub fn spawn_with_token<F, T>(token: CancellationToken, future: F) -> tokio::task::JoinHandle<T>
50where
51    F: Future<Output = T> + Send + 'static,
52    T: Send + 'static,
53{
54    tokio::spawn(async move {
55        let _ = token; // token 移动到任务内,但不主动 cancel
56        future.await
57    })
58}
59
60/// spawn 一个延迟执行的任务(对齐 `Swoole\Timer::after($ms, $callback)`)
61///
62/// 在 `delay` 后执行 `future`,返回 `JoinHandle<T>`。
63pub fn spawn_after<F, T>(delay: Duration, future: F) -> tokio::task::JoinHandle<T>
64where
65    F: Future<Output = T> + Send + 'static,
66    T: Send + 'static,
67{
68    tokio::spawn(async move {
69        tokio::time::sleep(delay).await;
70        future.await
71    })
72}
73
74/// spawn 一个周期性任务(对齐 `Swoole\Timer::tick($ms, $callback)`)
75///
76/// 每 `interval_ms` 毫秒执行一次 `future`,直到 `token` 被 cancel。
77///
78/// ## 返回
79///
80/// `JoinHandle<()>`:任务返回 `()`,调用方可 await 等待任务退出(通常在 cancel 后)。
81pub fn spawn_tick<F>(
82    interval_ms: u64,
83    token: CancellationToken,
84    mut future: F,
85) -> tokio::task::JoinHandle<()>
86where
87    F: FnMut() + Send + 'static,
88{
89    tokio::spawn(async move {
90        let mut ticker = tokio::time::interval(Duration::from_millis(interval_ms));
91        loop {
92            tokio::select! {
93                _ = token.cancelled() => break,
94                _ = ticker.tick() => future(),
95            }
96        }
97    })
98}
99
100/// spawn 一个带超时的任务(对齐 PHP `Swoole\Coroutine::select()` 超时控制)
101///
102/// 如果 `future` 在 `timeout` 内完成,返回 `Ok(T)`;否则返回 `Err(TimeoutError)`。
103pub fn spawn_with_timeout<F, T>(
104    timeout: Duration,
105    future: F,
106) -> tokio::task::JoinHandle<Result<T, TimeoutError>>
107where
108    F: Future<Output = T> + Send + 'static,
109    T: Send + 'static,
110{
111    tokio::spawn(async move {
112        match tokio::time::timeout(timeout, future).await {
113            Ok(result) => Ok(result),
114            Err(_) => Err(TimeoutError),
115        }
116    })
117}
118
119/// 为外部 IO 操作添加默认超时保护(P1-SEC-06)
120///
121/// 默认超时 **5 秒**,对齐项目规则 "任何外部 IO 必须包裹在 `tokio::time::timeout`(默认 5s)中"。
122///
123/// # 用法
124///
125/// ```rust,ignore
126/// let result = with_timeout(async {
127///     pool.acquire().await?.query(sql).await
128/// }).await;
129/// ```
130pub async fn with_timeout<F, T>(future: F) -> Result<T, TimeoutError>
131where
132    F: Future<Output = T>,
133{
134    tokio::time::timeout(DEFAULT_IO_TIMEOUT, future)
135        .await
136        .map_err(|_| TimeoutError)
137}
138
139/// 外部 IO 默认超时(5 秒)
140const DEFAULT_IO_TIMEOUT: Duration = Duration::from_secs(5);
141
142/// 超时错误
143#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
144#[error("task timed out")]
145pub struct TimeoutError;
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150    use std::sync::atomic::{AtomicUsize, Ordering};
151    use std::sync::Arc;
152    use std::time::Instant;
153
154    #[tokio::test]
155    async fn test_spawn_with_token_basic() {
156        let token = CancellationToken::new();
157        let handle = spawn_with_token(token, async { 42 });
158        assert_eq!(handle.await.unwrap(), 42);
159    }
160
161    #[tokio::test]
162    async fn test_spawn_with_token_cancellation() {
163        let token = CancellationToken::new();
164        let token_clone = token.clone();
165        let token_for_closure = token.clone();
166        let handle = spawn_with_token(token_clone, async move {
167            token_for_closure.cancelled().await;
168            99
169        });
170        token.cancel();
171        assert_eq!(handle.await.unwrap(), 99);
172    }
173
174    #[tokio::test]
175    async fn test_spawn_after_basic() {
176        let start = Instant::now();
177        let handle = spawn_after(Duration::from_millis(50), async { 7 });
178        let result = handle.await.unwrap();
179        assert_eq!(result, 7);
180        assert!(start.elapsed() >= Duration::from_millis(40));
181    }
182
183    #[tokio::test]
184    async fn test_spawn_after_zero_delay() {
185        let handle = spawn_after(Duration::from_millis(0), async { 1 });
186        assert_eq!(handle.await.unwrap(), 1);
187    }
188
189    #[tokio::test]
190    async fn test_spawn_tick_fires_multiple_times() {
191        let counter = Arc::new(AtomicUsize::new(0));
192        let token = CancellationToken::new();
193        let counter_clone = counter.clone();
194        let handle = spawn_tick(10, token.clone(), move || {
195            counter_clone.fetch_add(1, Ordering::SeqCst);
196        });
197
198        // 等待足够时间让 tick 触发多次
199        tokio::time::sleep(Duration::from_millis(100)).await;
200        token.cancel();
201        let _ = handle.await;
202
203        // 至少触发 1 次(interval 首次 tick 立即触发)
204        assert!(counter.load(Ordering::SeqCst) >= 1);
205    }
206
207    #[tokio::test]
208    async fn test_spawn_tick_stops_on_cancel() {
209        let counter = Arc::new(AtomicUsize::new(0));
210        let token = CancellationToken::new();
211        let counter_clone = counter.clone();
212        let handle = spawn_tick(10, token.clone(), move || {
213            counter_clone.fetch_add(1, Ordering::SeqCst);
214        });
215
216        tokio::time::sleep(Duration::from_millis(30)).await;
217        token.cancel();
218        let _ = handle.await;
219        let count_after_cancel = counter.load(Ordering::SeqCst);
220
221        // 等待一段时间确认计数不再增长
222        tokio::time::sleep(Duration::from_millis(50)).await;
223        assert_eq!(counter.load(Ordering::SeqCst), count_after_cancel);
224    }
225
226    #[tokio::test]
227    async fn test_spawn_with_timeout_success() {
228        let handle = spawn_with_timeout(Duration::from_millis(100), async { 42 });
229        assert_eq!(handle.await.unwrap().unwrap(), 42);
230    }
231
232    #[tokio::test]
233    async fn test_spawn_with_timeout_failure() {
234        let handle = spawn_with_timeout(Duration::from_millis(10), async {
235            tokio::time::sleep(Duration::from_millis(100)).await;
236            42
237        });
238        let result = handle.await.unwrap();
239        assert!(result.is_err());
240        assert_eq!(result.unwrap_err(), TimeoutError);
241    }
242
243    #[tokio::test]
244    async fn test_spawn_with_timeout_zero_timeout() {
245        // 零超时仍然允许 future 至少 poll 一次
246        let handle = spawn_with_timeout(Duration::from_millis(0), async { 1 });
247        let result = handle.await.unwrap();
248        // 零超时可能成功也可能失败,取决于 race,但不应 panic
249        let _ = result;
250    }
251
252    #[tokio::test]
253    async fn test_timeout_error_display() {
254        let err = TimeoutError;
255        assert_eq!(format!("{}", err), "task timed out");
256    }
257
258    #[tokio::test]
259    async fn test_spawn_multiple_concurrent_tasks() {
260        let token1 = CancellationToken::new();
261        let token2 = CancellationToken::new();
262        let h1 = spawn_with_token(token1, async { 1 });
263        let h2 = spawn_with_token(token2, async { 2 });
264        assert_eq!(h1.await.unwrap(), 1);
265        assert_eq!(h2.await.unwrap(), 2);
266    }
267
268    #[tokio::test]
269    async fn test_spawn_tick_immediate_first_fire() {
270        // tokio::time::interval 首次 tick 立即完成
271        let counter = Arc::new(AtomicUsize::new(0));
272        let token = CancellationToken::new();
273        let counter_clone = counter.clone();
274        let handle = spawn_tick(1, token.clone(), move || {
275            counter_clone.fetch_add(1, Ordering::SeqCst);
276        });
277
278        // 轮询等待计数器达到 1(超时 5s),避免固定 sleep 的调度竞态
279        let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
280        while counter.load(Ordering::SeqCst) < 1 {
281            tokio::time::sleep(Duration::from_millis(5)).await;
282            if tokio::time::Instant::now() > deadline {
283                token.cancel();
284                let _ = handle.await;
285                panic!("spawn_tick 首次 tick 未在 5s 内触发");
286            }
287        }
288        token.cancel();
289        let _ = handle.await;
290        assert!(
291            counter.load(Ordering::SeqCst) >= 1,
292            "spawn_tick 首次 tick 应在创建后立即触发,实际计数为 {}",
293            counter.load(Ordering::SeqCst)
294        );
295    }
296}