Rust 中实现定时任务管理

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 的用法,有时候我们使用了某个库、某个框后,自然的以为都封装好了,不需要二次封装。其实不是,可以说大部分的库和框架,都需要基于任务进行二次封装,有利于程序的可扩展性。

相关推荐
用户8599681677692 小时前
Go技术专家进阶营 从代码开发到架构设计,开启Go技术专家之路
后端
Java水解2 小时前
MySQL定时任务详解 - Event Scheduler 事件调度器从基础到实战
后端·mysql
该用户已不存在2 小时前
7个构建高性能后端的 Rust 必备库
后端·rust
Darenm1112 小时前
JWT鉴权的实现:从原理到 Django + Vue3
后端·python·django
毕设源码-赖学姐2 小时前
【开题答辩全过程】以 基于Springboot的智慧养老系统的设计与实现为例,包含答辩的问题和答案
java·spring boot·后端
无限大63 小时前
为什么"虚拟现实"和"增强现实"不同?——从虚拟到混合的视觉革命
架构
最贪吃的虎3 小时前
什么是开源?小白如何快速学会开源协作流程并参与项目
java·前端·后端·开源
2401_832298103 小时前
一云多芯时代:云服务器如何打破芯片架构壁垒
运维·服务器·架构
Thomas游戏开发3 小时前
Unity3D IL2CPP如何调用Burst
前端·后端·架构