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> = OnceCell::new(); static JOB_GUIDS: OnceCell>> = OnceCell::new(); pub type JobExecutor = Arc Pin + Send>> + Send + Sync>; fn scheduler() -> &'static Mutex { SCHEDULER .get() .expect("Scheduler not initialized. Call init_and_start() early in main.") } fn job_guids() -> &'static Mutex> { 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(()) }