use crate::SCHEDULER_LOG_TARGET;
use dashmap::DashMap;
use snafu::{ResultExt, Snafu};
use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, LazyLock};
use std::time::Duration;
use tibba_error::Error as BaseError;
pub use tokio_cron_scheduler::Job;
use tokio_cron_scheduler::{JobScheduler, JobSchedulerError};
use tracing::{error, info, warn};
type Result<T> = std::result::Result<T, BaseError>;
#[derive(Debug, Snafu)]
enum Error {
#[snafu(display("create scheduler failed: {source}"))]
Create { source: JobSchedulerError },
#[snafu(display("add job {name} failed: {source}"))]
AddJob {
name: String,
source: JobSchedulerError,
},
#[snafu(display("start scheduler failed: {source}"))]
Start { source: JobSchedulerError },
}
impl From<Error> for BaseError {
fn from(val: Error) -> Self {
let err = match val {
Error::Create { source } => BaseError::new(source),
Error::AddJob { name, source } => BaseError::new(source).with_sub_category(name),
Error::Start { source } => BaseError::new(source),
};
err.with_category("scheduler")
}
}
pub type LockFuture = Pin<Box<dyn Future<Output = bool> + Send + 'static>>;
pub type TryLock = Arc<dyn Fn(String, Duration) -> LockFuture + Send + Sync>;
pub fn singleton_cron_job<F, Fut>(
schedule: &str,
lock: TryLock,
lock_key: impl Into<String>,
lock_ttl: Duration,
body: F,
) -> std::result::Result<Job, JobSchedulerError>
where
F: Fn() -> Fut + Send + Sync + 'static,
Fut: Future<Output = ()> + Send + 'static,
{
let lock_key = lock_key.into();
let body = Arc::new(body);
Job::new_async(schedule, move |_uuid, _scheduler| {
let lock = lock.clone();
let lock_key = lock_key.clone();
let body = body.clone();
Box::pin(async move {
if lock(lock_key, lock_ttl).await {
body().await;
}
})
})
}
static JOB_TASKS: LazyLock<DashMap<String, Job>> = LazyLock::new(DashMap::new);
pub fn register_job_task(name: impl Into<String>, job: Job) {
let name = name.into();
if JOB_TASKS.insert(name.clone(), job).is_some() {
warn!(
target: SCHEDULER_LOG_TARGET,
name,
"job task name conflict; previous registration overwritten"
);
}
}
pub async fn run_scheduler_jobs() -> Result<JobScheduler> {
let scheduler = JobScheduler::new().await.context(CreateSnafu)?;
let mut added = 0_usize;
for item in JOB_TASKS.iter() {
let (name, job) = item.pair();
if let Err(err) = scheduler.add(job.clone()).await {
error!(
target: SCHEDULER_LOG_TARGET,
name,
error = %err,
"add job failed",
);
return Err(BaseError::from(Error::AddJob {
name: name.clone(),
source: err,
}));
}
info!(target: SCHEDULER_LOG_TARGET, name, "add job success");
added += 1;
}
scheduler.shutdown_on_ctrl_c();
scheduler.start().await.context(StartSnafu)?;
info!(target: SCHEDULER_LOG_TARGET, jobs = added, "scheduler started");
Ok(scheduler)
}