use std::sync::Arc;
use chrono::Utc;
use chronon_core::models::{Job, JobRevision, Run, RunStatus, ScheduleKind};
use chronon_core::store::SchedulerStore;
use chronon_core::{ChrononError, Result};
use chronon_scheduler::{partition_hash_i64_for_job_id, CronExpr, job_execution_pool_id};
use serde_json::Value;
pub struct CoordinatorService {
store: Arc<dyn SchedulerStore>,
}
impl CoordinatorService {
pub fn new(store: Arc<dyn SchedulerStore>) -> Self {
Self { store }
}
pub fn store(&self) -> Arc<dyn SchedulerStore> {
Arc::clone(&self.store)
}
pub async fn upsert_job(&self, mut job: Job) -> Result<()> {
if job.partition_hash.is_none() {
job.partition_hash = Some(partition_hash_i64_for_job_id(&job.job_id));
}
if job.schedule_kind == ScheduleKind::Cron {
if let Some(ref cron_expr) = job.cron_expr {
let cron = CronExpr::parse(cron_expr, job.timezone.as_deref())?;
job.next_run_at = cron.next_from_now();
}
}
job.updated_at = Utc::now();
if let Some(existing) = self.store.get_job(&job.job_id).await? {
if existing.current_revision != job.current_revision {
let revision = JobRevision::new(
&job.job_id,
job.current_revision,
job.actor_json.clone(),
serde_json::to_value(&job)?,
);
self.store.append_revision(&revision).await?;
}
} else {
let revision = JobRevision::new(
&job.job_id,
1,
job.actor_json.clone(),
serde_json::to_value(&job)?,
);
self.store.append_revision(&revision).await?;
}
self.store.upsert_job(&job).await
}
pub async fn get_job(&self, job_id: &str) -> Option<Job> {
self.store.get_job(job_id).await.ok().flatten()
}
pub async fn get_job_by_name(&self, job_name: &str) -> Option<Job> {
self.store
.get_job_by_name(job_name)
.await
.ok()
.flatten()
}
pub async fn list_jobs(&self) -> Result<Vec<Job>> {
self.store.list_jobs().await
}
pub async fn pause_job(&self, job_id: &str) -> Result<()> {
self.store.pause_job(job_id).await
}
pub async fn resume_job(&self, job_id: &str) -> Result<()> {
self.store.resume_job(job_id).await
}
pub async fn list_runs(
&self,
job_id: Option<&str>,
status: Option<&str>,
offset: usize,
limit: usize,
) -> Result<Vec<Run>> {
let status_filter = status.and_then(parse_run_status);
self.store
.list_runs_filtered(job_id, status_filter, offset, limit)
.await
}
pub async fn get_run(&self, run_id: &str) -> Result<Option<Run>> {
self.store.get_run(run_id).await
}
pub async fn list_revisions(&self, job_id: &str) -> Result<Vec<JobRevision>> {
self.store.list_revisions(job_id).await
}
pub async fn run_now(&self, job_id: &str) -> Result<String> {
self.run_now_with_params(job_id, None).await
}
pub async fn run_now_with_params(
&self,
job_id: &str,
params_override: Option<Value>,
) -> Result<String> {
let Some(job) = self.store.get_job(job_id).await? else {
return Err(ChrononError::JobNotFound(job_id.to_string()));
};
let now = Utc::now();
let mut run = Run::for_job(&job.job_id, &job.script_name, now);
run.actor_json = job.actor_json.clone();
run.params_json = params_override.unwrap_or_else(|| job.params_json.clone());
run.pool_id = Some(job_execution_pool_id(&job));
let run_id = run.run_id.clone();
self.store.create_run(&run).await?;
Ok(run_id)
}
}
fn parse_run_status(s: &str) -> Option<RunStatus> {
match s.to_ascii_lowercase().as_str() {
"queued" => Some(RunStatus::Queued),
"claimed" => Some(RunStatus::Claimed),
"running" => Some(RunStatus::Running),
"success" => Some(RunStatus::Success),
"failed" => Some(RunStatus::Failed),
"canceled" => Some(RunStatus::Canceled),
"timeout" => Some(RunStatus::Timeout),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use chronon_backend_mem::InMemorySchedulerStore;
#[tokio::test]
async fn upsert_and_run_now() {
let store = Arc::new(InMemorySchedulerStore::new());
let svc = CoordinatorService::new(store);
let job = Job::new("j1", "script_a");
svc.upsert_job(job.clone()).await.unwrap();
let run_id = svc.run_now(&job.job_id).await.unwrap();
assert!(!run_id.is_empty());
}
}