2025 年马上就要过去了,我们要站好牛马最后一班岗,我们要年终奖.....嗯,老板说我们要继续做梦,继续做任务做主线升级...这不任务就来了吗?写一个定时任务:
监控公司里每个人每天都浏览了哪些网页......
需求分析
这个需求其实特别简单,查询监控系统中的网页浏览记录表就行,每分钟查询一次。最简单的做法就是写一个死循环:
css
fn main() {
loop {
sleep(Duration::from_secs(60));
// todo: 查询数据表
}
}
当我把这个方案提交给老板的后,老板来了一句:要是那么简单,我要养你干嘛?!我之后要监控更多的内容呢?你也这样写吗?!
我心里默默地想:既然不简单,你把需求说清楚啊,总是一句话需求做一个"淘宝"!不过也就在心里吐槽,年底了,我不敢和他硬刚。
这时候,我就面临一个选择,是自己写一个定时任务库,还是选择一个别人已经写好的库了。会想过去那么多年,自己含辛茹苦抚养公司长大,最终年年年终年年空,一时间悲上心头!嗯,又不按照代码行数给我算工资,少些点,直接用 tokio-cron-scheduler 这个库。
实现定时任务
首先安装这个库,这个在 Rust 中并不是难事:
toml
[package]
name = "beacon"
version = "0.1.0"
edition = "2024"
[dependencies]
tokio = { version = "1.48.0", features = ["full"] }
tokio-cron-scheduler = "0.15.1"
然后,先写一个 Hello World 吧,每分钟打印一次 Hello World :
rust
use tokio_cron_scheduler::{Job, JobScheduler};
#[tokio::main]
async fn main() {
let scheduler = JobScheduler::new().await.unwrap();
scheduler
.add(
Job::new("* * * * * *", |_uuid, _l| {
println!("Hello World");
}).unwrap(),
)
.await
.unwrap();
scheduler.start().await.unwrap();
tokio::signal::ctrl_c().await.unwrap();
}
任务插件化
很快我提交了代码,给老板 Review。虽然老板不懂技术,但是他可以用 AI 来评审啊!很快老板发了我一长段 AI 的输出,重点就一句话:如果有很多任务,你也这样直接写在 main() 函数里吗?
好吧,继续改。怎么改了?思来想去,那就把任务做成插件化,然后在 main 函数中读取配置,自动注册任务。
首先,创建一个 trait ,编写 src/task.rs ,定义任务的名称、cron 表达式以及具体执行逻辑:
rust
use async_trait::async_trait;
#[async_trait]
pub trait Task: Send + Sync {
fn name(&self) -> &str;
fn cron(&self) -> &str;
async fn execute(&self);
}
接着,来修改 main 函数,新增一个注册任务的方法:
rust
mod task;
mod tasks;
use std::sync::Arc;
use task::Task;
use tasks::{DataSyncTask, ReportGeneratorTask};
fn register_tasks() -> Vec<Arc<dyn Task>> {
vec![
Arc::new(DataSyncTask) as Arc<dyn Task>,
Arc::new(ReportGeneratorTask) as Arc<dyn Task>,
// 在这里添加更多任务...
]
}
上面使用了 Arc 来包装 Task,使得 Task 可以安全的在线程之间传递,采用引用计数。
然后修改 main 函数:
rust
use tokio_cron_scheduler::{Job, JobScheduler};
#[tokio::main]
async fn main() {
let scheduler = JobScheduler::new().await.unwrap();
// 获取注册的任务
let tasks = register_tasks();
// 遍历所有任务统一注册
for task in tasks {
let task_clone = Arc::clone(&task);
let cron = task.cron().to_string();
let name = task.name().to_string();
let job = Job::new_async(cron.as_str(), move |_uuid, _l| {
let task = Arc::clone(&task_clone);
Box::pin(async move {
task.execute().await;
})
})
.unwrap();
scheduler.add(job).await.unwrap();
println!("✓ Registered task: {} ({})", name, task.cron());
}
scheduler.start().await.unwrap();
println!("\n🚀 Scheduler started. Press Ctrl+C to stop.");
tokio::signal::ctrl_c().await.unwrap();
println!("\n👋 Shutting down...");
}
需要说明的是,上面之所以使用 Box::pin 是因为 async 代码块生成的 Future 可能包含自引用,如果 Future 在内存中移动,自引用指针会失效。而 Box::pin 则将其固定在堆上,这样自引用指针在程序的生命周期内就会一直有效。
至此,这个程序就可以支持更多的任务了,只需要实现 Task 这个 trait 就行,例如:
rust
use crate::task::Task;
use async_trait::async_trait;
pub struct ReportGeneratorTask;
#[async_trait]
impl Task for ReportGeneratorTask {
fn name(&self) -> &str {
"Report Generator"
}
fn cron(&self) -> &str {
"0 0 9 * * *" // 每天早上 9 点
}
async fn execute(&self) {
println!("[{}] Generating daily report...", self.name());
match self.generate_report().await {
Ok(path) => println!("[{}] ✓ Report saved to: {}", self.name(), path),
Err(e) => eprintln!("[{}] ✗ Error: {}", self.name(), e),
}
}
}
impl ReportGeneratorTask {
async fn generate_report(&self) -> Result<String, String> {
// 复杂的报表生成逻辑
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
Ok("/tmp/report_2025_12_29.pdf".to_string())
}
}
总结
这篇文章只是简单了描述了 tokio-cron-scheduler 的用法,有时候我们使用了某个库、某个框后,自然的以为都封装好了,不需要二次封装。其实不是,可以说大部分的库和框架,都需要基于任务进行二次封装,有利于程序的可扩展性。