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/// 超时错误
120#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
121#[error("task timed out")]
122pub struct TimeoutError;
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use std::sync::atomic::{AtomicUsize, Ordering};
128    use std::sync::Arc;
129    use std::time::Instant;
130
131    #[tokio::test]
132    async fn test_spawn_with_token_basic() {
133        let token = CancellationToken::new();
134        let handle = spawn_with_token(token, async { 42 });
135        assert_eq!(handle.await.unwrap(), 42);
136    }
137
138    #[tokio::test]
139    async fn test_spawn_with_token_cancellation() {
140        let token = CancellationToken::new();
141        let token_clone = token.clone();
142        let token_for_closure = token.clone();
143        let handle = spawn_with_token(token_clone, async move {
144            token_for_closure.cancelled().await;
145            99
146        });
147        token.cancel();
148        assert_eq!(handle.await.unwrap(), 99);
149    }
150
151    #[tokio::test]
152    async fn test_spawn_after_basic() {
153        let start = Instant::now();
154        let handle = spawn_after(Duration::from_millis(50), async { 7 });
155        let result = handle.await.unwrap();
156        assert_eq!(result, 7);
157        assert!(start.elapsed() >= Duration::from_millis(40));
158    }
159
160    #[tokio::test]
161    async fn test_spawn_after_zero_delay() {
162        let handle = spawn_after(Duration::from_millis(0), async { 1 });
163        assert_eq!(handle.await.unwrap(), 1);
164    }
165
166    #[tokio::test]
167    async fn test_spawn_tick_fires_multiple_times() {
168        let counter = Arc::new(AtomicUsize::new(0));
169        let token = CancellationToken::new();
170        let counter_clone = counter.clone();
171        let handle = spawn_tick(10, token.clone(), move || {
172            counter_clone.fetch_add(1, Ordering::SeqCst);
173        });
174
175        // 等待足够时间让 tick 触发多次
176        tokio::time::sleep(Duration::from_millis(100)).await;
177        token.cancel();
178        let _ = handle.await;
179
180        // 至少触发 1 次(interval 首次 tick 立即触发)
181        assert!(counter.load(Ordering::SeqCst) >= 1);
182    }
183
184    #[tokio::test]
185    async fn test_spawn_tick_stops_on_cancel() {
186        let counter = Arc::new(AtomicUsize::new(0));
187        let token = CancellationToken::new();
188        let counter_clone = counter.clone();
189        let handle = spawn_tick(10, token.clone(), move || {
190            counter_clone.fetch_add(1, Ordering::SeqCst);
191        });
192
193        tokio::time::sleep(Duration::from_millis(30)).await;
194        token.cancel();
195        let _ = handle.await;
196        let count_after_cancel = counter.load(Ordering::SeqCst);
197
198        // 等待一段时间确认计数不再增长
199        tokio::time::sleep(Duration::from_millis(50)).await;
200        assert_eq!(counter.load(Ordering::SeqCst), count_after_cancel);
201    }
202
203    #[tokio::test]
204    async fn test_spawn_with_timeout_success() {
205        let handle = spawn_with_timeout(Duration::from_millis(100), async { 42 });
206        assert_eq!(handle.await.unwrap().unwrap(), 42);
207    }
208
209    #[tokio::test]
210    async fn test_spawn_with_timeout_failure() {
211        let handle = spawn_with_timeout(Duration::from_millis(10), async {
212            tokio::time::sleep(Duration::from_millis(100)).await;
213            42
214        });
215        let result = handle.await.unwrap();
216        assert!(result.is_err());
217        assert_eq!(result.unwrap_err(), TimeoutError);
218    }
219
220    #[tokio::test]
221    async fn test_spawn_with_timeout_zero_timeout() {
222        // 零超时仍然允许 future 至少 poll 一次
223        let handle = spawn_with_timeout(Duration::from_millis(0), async { 1 });
224        let result = handle.await.unwrap();
225        // 零超时可能成功也可能失败,取决于 race,但不应 panic
226        let _ = result;
227    }
228
229    #[tokio::test]
230    async fn test_timeout_error_display() {
231        let err = TimeoutError;
232        assert_eq!(format!("{}", err), "task timed out");
233    }
234
235    #[tokio::test]
236    async fn test_spawn_multiple_concurrent_tasks() {
237        let token1 = CancellationToken::new();
238        let token2 = CancellationToken::new();
239        let h1 = spawn_with_token(token1, async { 1 });
240        let h2 = spawn_with_token(token2, async { 2 });
241        assert_eq!(h1.await.unwrap(), 1);
242        assert_eq!(h2.await.unwrap(), 2);
243    }
244
245    #[tokio::test]
246    async fn test_spawn_tick_immediate_first_fire() {
247        // tokio::time::interval 首次 tick 立即完成
248        let counter = Arc::new(AtomicUsize::new(0));
249        let token = CancellationToken::new();
250        let counter_clone = counter.clone();
251        let handle = spawn_tick(1, token.clone(), move || {
252            counter_clone.fetch_add(1, Ordering::SeqCst);
253        });
254
255        // 轮询等待计数器达到 1(超时 5s),避免固定 sleep 的调度竞态
256        let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
257        while counter.load(Ordering::SeqCst) < 1 {
258            tokio::time::sleep(Duration::from_millis(5)).await;
259            if tokio::time::Instant::now() > deadline {
260                token.cancel();
261                let _ = handle.await;
262                panic!("spawn_tick 首次 tick 未在 5s 内触发");
263            }
264        }
265        token.cancel();
266        let _ = handle.await;
267        assert!(
268            counter.load(Ordering::SeqCst) >= 1,
269            "spawn_tick 首次 tick 应在创建后立即触发,实际计数为 {}",
270            counter.load(Ordering::SeqCst)
271        );
272    }
273}