Skip to main content

sz_rust_core/runtime/
scheduler.rs

1//! 定时任务调度接入
2//!
3//! ## PHP 对齐
4//!
5//! 对齐 PHP `think-swoole` 的 Crontab 模型(注:think-swoole 本身不含 Crontab,
6//! 由独立包 `topthink/think-swoole-crontab` 提供):
7//!
8//! ```php
9//! $cron = new \think\swoole\crontab\Crontab();
10//! $cron->add('task-1', '* * * * *', function() { /* ... */ });
11//! $cron->run();
12//! ```
13//!
14//! Rust 端复用 `sz_orm_scheduler::CronScheduler`,但**不使用其 `start()` 方法**
15//! (因为 `CronScheduler::start()` 内部用 `std::thread::spawn`,与 tokio 不兼容)。
16//!
17//! ## 设计
18//!
19//! - `SchedulerRuntime`:封装 `CronScheduler`,用 `tokio::time::interval` + `try_fire_due()` 重写循环
20//! - 监听 `CancellationToken` 优雅退出
21//! - 保留 `CronScheduler` 原有 API(schedule/cancel/pause/resume/list_tasks)
22
23use std::sync::Arc;
24use std::time::Duration;
25
26use tokio_util::sync::CancellationToken;
27
28use sz_orm_scheduler::{CronScheduler, JobHandler, ScheduledTask, Scheduler, SchedulerError};
29
30/// 调度器运行时配置
31#[derive(Debug, Clone)]
32pub struct SchedulerRuntimeConfig {
33    /// 调度器 tick 间隔(毫秒,对齐 `CronScheduler::start(tick_ms)`)
34    pub tick_ms: u64,
35}
36
37impl Default for SchedulerRuntimeConfig {
38    fn default() -> Self {
39        Self { tick_ms: 1000 }
40    }
41}
42
43impl SchedulerRuntimeConfig {
44    /// 创建新配置
45    pub fn new(tick_ms: u64) -> Self {
46        Self {
47            tick_ms: tick_ms.max(1),
48        }
49    }
50}
51
52/// 调度器运行时
53///
54/// 封装 `sz_orm_scheduler::CronScheduler`,提供 tokio 兼容的调度循环。
55///
56/// ## 设计
57///
58/// - **不调用 `CronScheduler::start()`**(因为它用 `std::thread::spawn`,会占用阻塞线程)
59/// - 改用 `tokio::time::interval` + `try_fire_due()` 在 tokio 任务中循环
60/// - 监听 `CancellationToken` 优雅退出
61/// - 保留 `CronScheduler` 的所有 API(schedule/cancel/pause/resume/list_tasks/register_handler)
62///
63/// ## 用法
64///
65/// ```rust,ignore
66/// use sz_rust_core::runtime::scheduler::{SchedulerRuntime, SchedulerRuntimeConfig};
67/// use sz_orm_scheduler::ScheduledTask;
68/// use tokio_util::sync::CancellationToken;
69///
70/// let runtime = SchedulerRuntime::new(SchedulerRuntimeConfig::default());
71/// let task = ScheduledTask::new("task-1", "每分钟任务", "0 * * * *")
72///     .with_callback("demo::minute_task");
73/// runtime.schedule(task).unwrap();
74///
75/// let token = CancellationToken::new();
76/// let handle = runtime.start(token.clone());
77/// // ... 业务运行 ...
78/// token.cancel();
79/// let _ = handle.await;
80/// ```
81pub struct SchedulerRuntime {
82    config: SchedulerRuntimeConfig,
83    scheduler: Arc<CronScheduler>,
84}
85
86impl SchedulerRuntime {
87    /// 创建调度器运行时
88    pub fn new(config: SchedulerRuntimeConfig) -> Self {
89        Self {
90            config,
91            scheduler: Arc::new(CronScheduler::new()),
92        }
93    }
94
95    /// 启动调度循环(返回 JoinHandle,调用方持有)
96    ///
97    /// ## 行为
98    ///
99    /// 1. 每 `tick_ms` 毫秒调用 `scheduler.try_fire_due(now)` 触发到期任务
100    /// 2. 监听 `token.cancelled()`,收到信号后停止循环
101    /// 3. 不调用 `CronScheduler::stop()`(避免 `JoinHandle::join()` 阻塞)
102    pub fn start(&self, token: CancellationToken) -> tokio::task::JoinHandle<()> {
103        let scheduler = self.scheduler.clone();
104        let tick_interval = Duration::from_millis(self.config.tick_ms);
105
106        tokio::spawn(async move {
107            let mut ticker = tokio::time::interval(tick_interval);
108            // 首次 tick 立即完成(tokio::time::interval 默认行为)
109            loop {
110                tokio::select! {
111                    _ = token.cancelled() => break,
112                    _ = ticker.tick() => {
113                        let now = chrono::Utc::now();
114                        let fired = scheduler.try_fire_due(now);
115                        if fired > 0 {
116                            tracing::debug!("scheduler fired {} task(s) at {}", fired, now);
117                        }
118                    }
119                }
120            }
121        })
122    }
123
124    /// 注册调度任务(委托给 `CronScheduler::schedule`)
125    pub fn schedule(&self, task: ScheduledTask) -> Result<(), SchedulerError> {
126        self.scheduler.schedule(task)
127    }
128
129    /// 取消任务(委托给 `CronScheduler::cancel`)
130    pub fn cancel(&self, task_id: &str) -> Result<(), SchedulerError> {
131        self.scheduler.cancel(task_id)
132    }
133
134    /// 暂停任务(委托给 `CronScheduler::pause`)
135    pub fn pause(&self, task_id: &str) -> Result<(), SchedulerError> {
136        self.scheduler.pause(task_id)
137    }
138
139    /// 恢复任务(委托给 `CronScheduler::resume`)
140    pub fn resume(&self, task_id: &str) -> Result<(), SchedulerError> {
141        self.scheduler.resume(task_id)
142    }
143
144    /// 列出所有任务(委托给 `CronScheduler::list_tasks`)
145    pub fn list_tasks(&self) -> Vec<ScheduledTask> {
146        self.scheduler.list_tasks()
147    }
148
149    /// 注册任务处理器(委托给 `CronScheduler::register_handler`)
150    pub fn register_handler(&self, task_id: impl Into<String>, handler: Arc<dyn JobHandler>) {
151        self.scheduler.register_handler(task_id, handler);
152    }
153
154    /// 手动触发一次到期任务检查(对齐 `scheduler:run` 命令)
155    pub fn try_fire_due(&self) -> usize {
156        let now = chrono::Utc::now();
157        self.scheduler.try_fire_due(now)
158    }
159
160    /// 获取任务数量
161    pub fn task_count(&self) -> usize {
162        self.scheduler.list_tasks().len()
163    }
164
165    /// 获取配置
166    pub fn config(&self) -> &SchedulerRuntimeConfig {
167        &self.config
168    }
169
170    /// 获取内部 CronScheduler 引用(用于直接调用未暴露的方法)
171    pub fn scheduler(&self) -> &CronScheduler {
172        &self.scheduler
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179    use std::sync::atomic::{AtomicUsize, Ordering};
180
181    /// 测试用 JobHandler:原子计数器
182    struct CounterHandler {
183        counter: Arc<AtomicUsize>,
184    }
185
186    impl CounterHandler {
187        fn new() -> (Self, Arc<AtomicUsize>) {
188            let counter = Arc::new(AtomicUsize::new(0));
189            let handler = Self {
190                counter: counter.clone(),
191            };
192            (handler, counter)
193        }
194    }
195
196    impl JobHandler for CounterHandler {
197        fn handle(&self, _task: &ScheduledTask) -> Result<(), String> {
198            self.counter.fetch_add(1, Ordering::SeqCst);
199            Ok(())
200        }
201    }
202
203    #[test]
204    fn test_scheduler_runtime_config_default() {
205        let config = SchedulerRuntimeConfig::default();
206        assert_eq!(config.tick_ms, 1000);
207    }
208
209    #[test]
210    fn test_scheduler_runtime_config_custom() {
211        let config = SchedulerRuntimeConfig::new(500);
212        assert_eq!(config.tick_ms, 500);
213    }
214
215    #[test]
216    fn test_scheduler_runtime_config_zero_clamped() {
217        let config = SchedulerRuntimeConfig::new(0);
218        assert_eq!(config.tick_ms, 1);
219    }
220
221    #[test]
222    fn test_schedule_task() {
223        let runtime = SchedulerRuntime::new(SchedulerRuntimeConfig::default());
224        let task = ScheduledTask::new("task-1", "测试任务", "0 * * * *");
225        runtime.schedule(task).unwrap();
226        assert_eq!(runtime.task_count(), 1);
227    }
228
229    #[test]
230    fn test_schedule_multiple_tasks() {
231        let runtime = SchedulerRuntime::new(SchedulerRuntimeConfig::default());
232        runtime
233            .schedule(ScheduledTask::new("t1", "任务1", "0 * * * *"))
234            .unwrap();
235        runtime
236            .schedule(ScheduledTask::new("t2", "任务2", "0 0 * * *"))
237            .unwrap();
238        runtime
239            .schedule(ScheduledTask::new("t3", "任务3", "0 0 0 * *"))
240            .unwrap();
241        assert_eq!(runtime.task_count(), 3);
242    }
243
244    #[test]
245    fn test_cancel_task() {
246        let runtime = SchedulerRuntime::new(SchedulerRuntimeConfig::default());
247        runtime
248            .schedule(ScheduledTask::new("task-1", "测试", "0 * * * *"))
249            .unwrap();
250        assert_eq!(runtime.task_count(), 1);
251
252        runtime.cancel("task-1").unwrap();
253        assert_eq!(runtime.task_count(), 0);
254    }
255
256    #[test]
257    fn test_cancel_nonexistent_task() {
258        let runtime = SchedulerRuntime::new(SchedulerRuntimeConfig::default());
259        let result = runtime.cancel("nonexistent");
260        assert!(result.is_err());
261    }
262
263    #[test]
264    fn test_pause_resume_task() {
265        let runtime = SchedulerRuntime::new(SchedulerRuntimeConfig::default());
266        runtime
267            .schedule(ScheduledTask::new("task-1", "测试", "0 * * * *"))
268            .unwrap();
269
270        runtime.pause("task-1").unwrap();
271        let tasks = runtime.list_tasks();
272        assert!(!tasks[0].enabled);
273
274        runtime.resume("task-1").unwrap();
275        let tasks = runtime.list_tasks();
276        assert!(tasks[0].enabled);
277    }
278
279    #[test]
280    fn test_list_tasks() {
281        let runtime = SchedulerRuntime::new(SchedulerRuntimeConfig::default());
282        runtime
283            .schedule(ScheduledTask::new("t1", "任务1", "0 * * * *"))
284            .unwrap();
285        runtime
286            .schedule(ScheduledTask::new("t2", "任务2", "0 0 * * *"))
287            .unwrap();
288
289        let tasks = runtime.list_tasks();
290        assert_eq!(tasks.len(), 2);
291    }
292
293    #[test]
294    fn test_register_handler() {
295        let runtime = SchedulerRuntime::new(SchedulerRuntimeConfig::default());
296        runtime
297            .schedule(ScheduledTask::new("task-1", "测试", "0 * * * *"))
298            .unwrap();
299
300        let (handler, _counter) = CounterHandler::new();
301        runtime.register_handler("task-1", Arc::new(handler));
302        // 注册成功即可,不验证内部状态
303    }
304
305    #[tokio::test]
306    async fn test_start_and_cancel() {
307        let runtime = SchedulerRuntime::new(SchedulerRuntimeConfig::new(10));
308        let token = CancellationToken::new();
309        let handle = runtime.start(token.clone());
310
311        // 等待几次 tick
312        tokio::time::sleep(Duration::from_millis(50)).await;
313        token.cancel();
314
315        // 任务应该退出
316        let result = tokio::time::timeout(Duration::from_secs(2), handle).await;
317        assert!(result.is_ok(), "scheduler task should stop on cancel");
318    }
319
320    #[tokio::test]
321    async fn test_scheduler_fires_due_task() {
322        let runtime = SchedulerRuntime::new(SchedulerRuntimeConfig::new(10));
323        // 注册一个每秒触发的任务(5 字段 cron:second minute hour day month)
324        // "*" 表示每秒都触发
325        runtime
326            .schedule(ScheduledTask::new("every-second", "每秒任务", "* * * * *"))
327            .unwrap();
328
329        let (handler, counter) = CounterHandler::new();
330        runtime.register_handler("every-second", Arc::new(handler));
331
332        let token = CancellationToken::new();
333        let handle = runtime.start(token.clone());
334
335        // 等待足够时间让任务触发
336        tokio::time::sleep(Duration::from_millis(100)).await;
337        token.cancel();
338        let _ = handle.await;
339
340        // 至少触发 1 次
341        assert!(
342            counter.load(Ordering::SeqCst) >= 1,
343            "task should have fired at least once"
344        );
345    }
346
347    #[test]
348    fn test_try_fire_due_manual() {
349        let runtime = SchedulerRuntime::new(SchedulerRuntimeConfig::default());
350        // 注册一个每秒触发的任务
351        runtime
352            .schedule(ScheduledTask::new("every-second", "每秒任务", "* * * * *"))
353            .unwrap();
354
355        let (handler, counter) = CounterHandler::new();
356        runtime.register_handler("every-second", Arc::new(handler));
357
358        // 手动触发
359        let fired = runtime.try_fire_due();
360        assert!(fired >= 1);
361        assert!(counter.load(Ordering::SeqCst) >= 1);
362    }
363
364    #[test]
365    fn test_try_fire_due_no_tasks() {
366        let runtime = SchedulerRuntime::new(SchedulerRuntimeConfig::default());
367        let fired = runtime.try_fire_due();
368        assert_eq!(fired, 0);
369    }
370
371    #[test]
372    fn test_config_accessor() {
373        let runtime = SchedulerRuntime::new(SchedulerRuntimeConfig::new(250));
374        assert_eq!(runtime.config().tick_ms, 250);
375    }
376
377    #[test]
378    fn test_scheduler_accessor() {
379        let runtime = SchedulerRuntime::new(SchedulerRuntimeConfig::default());
380        let _scheduler = runtime.scheduler();
381        // 验证可以获取内部引用
382    }
383}