Files
udmin/backend/src/utils/scheduler.rs
ayou 8c06849254 feat(调度任务): 实现调度任务管理功能
新增调度任务模块,支持任务的增删改查、启停及手动执行
- 后端添加 schedule_job 模型、服务、路由及调度器工具类
- 前端新增调度任务管理页面
- 修改 flow 相关接口将 id 类型从 String 改为 i64
- 添加 tokio-cron-scheduler 依赖实现定时任务调度
- 初始化时加载已启用任务并注册到调度器
2025-09-24 00:21:30 +08:00

99 lines
3.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use once_cell::sync::OnceCell;
use tokio::sync::Mutex;
use tokio_cron_scheduler::{JobScheduler, Job};
use tracing::{error, info};
use uuid::Uuid;
use crate::models::schedule_job;
static SCHEDULER: OnceCell<Mutex<JobScheduler>> = OnceCell::new();
static JOB_GUIDS: OnceCell<Mutex<HashMap<String, Uuid>>> = OnceCell::new();
pub type JobExecutor = Arc<dyn Fn() -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
fn scheduler() -> &'static Mutex<JobScheduler> {
SCHEDULER
.get()
.expect("Scheduler not initialized. Call init_and_start() early in main.")
}
fn job_guids() -> &'static Mutex<HashMap<String, Uuid>> {
JOB_GUIDS
.get()
.expect("JOB_GUIDS not initialized. Call init_and_start() early in main.")
}
pub async fn init_and_start() -> anyhow::Result<()> {
if SCHEDULER.get().is_some() {
return Ok(());
}
let js = JobScheduler::new().await?;
SCHEDULER.set(Mutex::new(js)).ok().expect("set scheduler once");
JOB_GUIDS.set(Mutex::new(HashMap::new())).ok().expect("set job_guids once");
// 仅启动调度器,不进行任何数据库加载,数据库加载应由 service 层负责
let lock = scheduler().lock().await;
lock.start().await?;
drop(lock);
info!(target = "udmin", "scheduler started");
Ok(())
}
/// 新增或更新一个任务(根据 model.id 作为唯一标识)。
/// - 如果已存在,则先移除旧任务再按最新 cron 重新创建;
/// - 当 enabled=false 时,仅执行移除逻辑,不会重新添加。
pub async fn add_or_update_job_by_model(m: &schedule_job::Model, executor: JobExecutor) -> anyhow::Result<()> {
// 如果已有旧的 job先移除
if let Some(old) = { job_guids().lock().await.get(&m.id).cloned() } {
let js = scheduler().lock().await;
if let Err(e) = js.remove(&old).await {
error!(target = "udmin", id = %m.id, guid = %old, error = %e, "scheduler.remove old failed");
}
job_guids().lock().await.remove(&m.id);
}
if !m.enabled {
return Ok(());
}
// 构造异步 Job仅负责调度触发不做数据库操作
let cron_expr = m.cron_expr.clone();
let name = m.name.clone();
let job_id = m.id.clone();
let exec_arc = executor.clone();
let job = Job::new_async(cron_expr.as_str(), move |_uuid, _l| {
let job_name = name.clone();
let job_id = job_id.clone();
let exec = exec_arc.clone();
Box::pin(async move {
info!(target = "udmin", job = %job_name, id = %job_id, "scheduler.tick: start");
exec().await;
})
})?;
let guid = job.guid();
{
let js = scheduler().lock().await;
js.add(job).await?;
}
job_guids().lock().await.insert(m.id.clone(), guid);
info!(target = "udmin", id = %m.id, guid = %guid, "scheduler.add: ok");
Ok(())
}
pub async fn remove_job_by_id(id: &str) -> anyhow::Result<()> {
if let Some(g) = { job_guids().lock().await.get(id).cloned() } {
let js = scheduler().lock().await;
if let Err(e) = js.remove(&g).await {
error!(target = "udmin", id = %id, guid = %g, error = %e, "scheduler.remove: failed");
}
job_guids().lock().await.remove(id);
info!(target = "udmin", id = %id, guid = %g, "scheduler.remove: ok");
}
Ok(())
}