feat(调度任务): 实现调度任务管理功能
新增调度任务模块,支持任务的增删改查、启停及手动执行 - 后端添加 schedule_job 模型、服务、路由及调度器工具类 - 前端新增调度任务管理页面 - 修改 flow 相关接口将 id 类型从 String 改为 i64 - 添加 tokio-cron-scheduler 依赖实现定时任务调度 - 初始化时加载已启用任务并注册到调度器
This commit is contained in:
99
backend/src/utils/scheduler.rs
Normal file
99
backend/src/utils/scheduler.rs
Normal file
@ -0,0 +1,99 @@
|
||||
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(())
|
||||
}
|
||||
Reference in New Issue
Block a user