chronon_runtime/
coordinator_service.rs1use std::sync::Arc;
4
5use chrono::Utc;
6use chronon_core::models::{Job, JobRevision, Run, RunStatus, ScheduleKind};
7use chronon_core::store::SchedulerStore;
8use chronon_core::{ChrononError, Result};
9use chronon_scheduler::{partition_hash_i64_for_job_id, CronExpr, job_execution_pool_id};
10use serde_json::Value;
11
12pub struct CoordinatorService {
17 store: Arc<dyn SchedulerStore>,
18}
19
20impl CoordinatorService {
21 pub fn new(store: Arc<dyn SchedulerStore>) -> Self {
23 Self { store }
24 }
25
26 pub fn store(&self) -> Arc<dyn SchedulerStore> {
28 Arc::clone(&self.store)
29 }
30
31 pub async fn upsert_job(&self, mut job: Job) -> Result<()> {
58 if job.partition_hash.is_none() {
59 job.partition_hash = Some(partition_hash_i64_for_job_id(&job.job_id));
60 }
61 if job.schedule_kind == ScheduleKind::Cron {
62 if let Some(ref cron_expr) = job.cron_expr {
63 let cron = CronExpr::parse(cron_expr, job.timezone.as_deref())?;
64 job.next_run_at = cron.next_from_now();
65 }
66 }
67 job.updated_at = Utc::now();
68
69 if let Some(existing) = self.store.get_job(&job.job_id).await? {
70 if existing.current_revision != job.current_revision {
71 let revision = JobRevision::new(
72 &job.job_id,
73 job.current_revision,
74 job.actor_json.clone(),
75 serde_json::to_value(&job)?,
76 );
77 self.store.append_revision(&revision).await?;
78 }
79 } else {
80 let revision = JobRevision::new(
81 &job.job_id,
82 1,
83 job.actor_json.clone(),
84 serde_json::to_value(&job)?,
85 );
86 self.store.append_revision(&revision).await?;
87 }
88
89 self.store.upsert_job(&job).await
90 }
91
92 pub async fn get_job(&self, job_id: &str) -> Option<Job> {
94 self.store.get_job(job_id).await.ok().flatten()
95 }
96
97 pub async fn get_job_by_name(&self, job_name: &str) -> Option<Job> {
99 self.store
100 .get_job_by_name(job_name)
101 .await
102 .ok()
103 .flatten()
104 }
105
106 pub async fn list_jobs(&self) -> Result<Vec<Job>> {
112 self.store.list_jobs().await
113 }
114
115 pub async fn pause_job(&self, job_id: &str) -> Result<()> {
117 self.store.pause_job(job_id).await
118 }
119
120 pub async fn resume_job(&self, job_id: &str) -> Result<()> {
122 self.store.resume_job(job_id).await
123 }
124
125 pub async fn list_runs(
129 &self,
130 job_id: Option<&str>,
131 status: Option<&str>,
132 offset: usize,
133 limit: usize,
134 ) -> Result<Vec<Run>> {
135 let status_filter = status.and_then(parse_run_status);
136 self.store
137 .list_runs_filtered(job_id, status_filter, offset, limit)
138 .await
139 }
140
141 pub async fn get_run(&self, run_id: &str) -> Result<Option<Run>> {
143 self.store.get_run(run_id).await
144 }
145
146 pub async fn list_revisions(&self, job_id: &str) -> Result<Vec<JobRevision>> {
148 self.store.list_revisions(job_id).await
149 }
150
151 pub async fn run_now(&self, job_id: &str) -> Result<String> {
160 self.run_now_with_params(job_id, None).await
161 }
162
163 pub async fn run_now_with_params(
165 &self,
166 job_id: &str,
167 params_override: Option<Value>,
168 ) -> Result<String> {
169 let Some(job) = self.store.get_job(job_id).await? else {
170 return Err(ChrononError::JobNotFound(job_id.to_string()));
171 };
172 let now = Utc::now();
173 let mut run = Run::for_job(&job.job_id, &job.script_name, now);
174 run.actor_json = job.actor_json.clone();
175 run.params_json = params_override.unwrap_or_else(|| job.params_json.clone());
176 run.pool_id = Some(job_execution_pool_id(&job));
177 let run_id = run.run_id.clone();
178 self.store.create_run(&run).await?;
179 Ok(run_id)
180 }
181}
182
183fn parse_run_status(s: &str) -> Option<RunStatus> {
184 match s.to_ascii_lowercase().as_str() {
185 "queued" => Some(RunStatus::Queued),
186 "claimed" => Some(RunStatus::Claimed),
187 "running" => Some(RunStatus::Running),
188 "success" => Some(RunStatus::Success),
189 "failed" => Some(RunStatus::Failed),
190 "canceled" => Some(RunStatus::Canceled),
191 "timeout" => Some(RunStatus::Timeout),
192 _ => None,
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199 use chronon_backend_mem::InMemorySchedulerStore;
200
201 #[tokio::test]
202 async fn upsert_and_run_now() {
203 let store = Arc::new(InMemorySchedulerStore::new());
204 let svc = CoordinatorService::new(store);
205 let job = Job::new("j1", "script_a");
206 svc.upsert_job(job.clone()).await.unwrap();
207 let run_id = svc.run_now(&job.job_id).await.unwrap();
208 assert!(!run_id.is_empty());
209 }
210}