ora_storage_memory/
lib.rs

1//! A simple in-memory storage backend with no persistence.
2
3use std::{sync::Arc, time::SystemTime};
4
5use async_trait::async_trait;
6use eyre::{bail, Context};
7use indexmap::IndexSet;
8use parking_lot::RwLock;
9use uuid::Uuid;
10
11use ora_storage::{
12    CancelledJob, CancelledSchedule, ExecutionDetails, IndexMap, JobExecutionStatus,
13    JobQueryFilters, JobQueryOrder, JobQueryResult, JobRetryPolicy, JobTimeoutPolicy, JobType,
14    NewExecution, NewJob, NewSchedule, PendingExecution, PendingJob, PendingSchedule,
15    ReadyExecution, ScheduleJobCreationPolicy, ScheduleJobTimingPolicy, ScheduleQueryFilters,
16    ScheduleQueryOrder, ScheduleQueryResult, ScheduleTimeRange, Storage,
17};
18
19mod job_query;
20mod schedule_query;
21mod snapshot;
22
23/// A storage backend that holds all data in memory.
24///
25/// It serves as a reference implementation for storage backends
26/// as well as a test platform for the server itself.
27/// It is optimized for simplicity and readability, and is not
28/// intended for production use.
29///
30/// It may be used for small-scale production applications
31/// where persistence is not required and the amount of data
32/// is small enough to fit in memory.
33//
34// Indexes and other helper data structures are omitted
35// on purpose to keep the implementation simple.
36//
37// The implementation also contains more safety checks and
38// assertions that test the server itself.
39#[derive(Debug, Default, Clone)]
40#[must_use]
41pub struct MemoryStorage {
42    job_types: Arc<RwLock<IndexMap<String, JobType>>>,
43
44    // Jobs are partitioned by whether they are schedulable or unschedulable.
45    schedulable_jobs: Arc<RwLock<IndexMap<Uuid, Job>>>,
46    unschedulable_jobs: Arc<RwLock<IndexMap<Uuid, Job>>>,
47
48    // Executions are partitioned by the phase they are in.
49    pending_executions: Arc<RwLock<IndexMap<Uuid, Execution>>>,
50    ready_executions: Arc<RwLock<IndexMap<Uuid, Execution>>>,
51    assigned_executions: Arc<RwLock<IndexMap<Uuid, Execution>>>,
52    started_executions: Arc<RwLock<IndexMap<Uuid, Execution>>>,
53    succeeded_executions: Arc<RwLock<IndexMap<Uuid, Execution>>>,
54    failed_executions: Arc<RwLock<IndexMap<Uuid, Execution>>>,
55
56    // Schedules are partitioned by whether they are schedulable or unschedulable.
57    schedulable_schedules: Arc<RwLock<IndexMap<Uuid, Schedule>>>,
58    unschedulable_schedules: Arc<RwLock<IndexMap<Uuid, Schedule>>>,
59}
60
61impl MemoryStorage {
62    /// Create a new in-memory storage backend.
63    pub fn new() -> Self {
64        Self::default()
65    }
66}
67
68#[async_trait]
69impl Storage for MemoryStorage {
70    async fn job_types_added(&self, job_types: Vec<JobType>) -> eyre::Result<()> {
71        self.job_types.write().extend(
72            job_types
73                .iter()
74                .map(|job_type| (job_type.id.clone(), job_type.clone())),
75        );
76
77        Ok(())
78    }
79
80    async fn jobs_added(&self, new_jobs: Vec<NewJob>) -> eyre::Result<()> {
81        for new_job in new_jobs {
82            let mut jobs = self.schedulable_jobs.write();
83
84            if jobs.contains_key(&new_job.id) {
85                bail!("job with ID {} already exists", new_job.id);
86            }
87
88            jobs.insert(new_job.id, Job::from(new_job));
89        }
90
91        Ok(())
92    }
93
94    async fn jobs_cancelled(
95        &self,
96        job_ids: &[Uuid],
97        timestamp: SystemTime,
98    ) -> eyre::Result<Vec<CancelledJob>> {
99        let mut cancelled_jobs = Vec::with_capacity(job_ids.len());
100
101        for job_id in job_ids {
102            let active_job = self.schedulable_jobs.write().swap_remove(job_id);
103
104            if let Some(mut job) = active_job {
105                debug_assert!(job.cancelled_at.is_none());
106                job.cancelled_at = Some(timestamp);
107
108                let active_execution = self
109                    .pending_executions
110                    .read()
111                    .iter()
112                    .find_map(|(_, execution)| {
113                        if &execution.job_id == job_id {
114                            Some(execution.id)
115                        } else {
116                            None
117                        }
118                    })
119                    .or_else(|| {
120                        self.ready_executions
121                            .read()
122                            .iter()
123                            .find_map(|(_, execution)| {
124                                if &execution.job_id == job_id {
125                                    Some(execution.id)
126                                } else {
127                                    None
128                                }
129                            })
130                    })
131                    .or_else(|| {
132                        self.assigned_executions
133                            .read()
134                            .iter()
135                            .find_map(|(_, execution)| {
136                                if &execution.job_id == job_id {
137                                    Some(execution.id)
138                                } else {
139                                    None
140                                }
141                            })
142                    })
143                    .or_else(|| {
144                        self.started_executions
145                            .read()
146                            .iter()
147                            .find_map(|(_, execution)| {
148                                if &execution.job_id == job_id {
149                                    Some(execution.id)
150                                } else {
151                                    None
152                                }
153                            })
154                    });
155
156                if active_execution.is_some() {
157                    job.marked_unschedulable_at = Some(timestamp);
158                    self.unschedulable_jobs.write().insert(*job_id, job);
159                } else {
160                    self.schedulable_jobs.write().insert(*job_id, job);
161                }
162
163                cancelled_jobs.push(CancelledJob {
164                    id: *job_id,
165                    active_execution,
166                });
167            }
168        }
169
170        Ok(cancelled_jobs)
171    }
172
173    async fn executions_added(
174        &self,
175        executions: Vec<NewExecution>,
176        timestamp: SystemTime,
177    ) -> eyre::Result<()> {
178        for execution in executions {
179            let mut pending_executions = self.pending_executions.write();
180            if pending_executions.contains_key(&execution.id) {
181                bail!("execution with ID {} already exists", execution.id);
182            }
183
184            pending_executions.insert(
185                execution.id,
186                Execution {
187                    id: execution.id,
188                    job_id: execution.job_id,
189                    target_execution_time: execution.target_execution_time,
190                    created_at: timestamp,
191                    executor_id: None,
192                    ready_at: None,
193                    assigned_at: None,
194                    started_at: None,
195                    succeeded_at: None,
196                    failed_at: None,
197                    output_payload_json: None,
198                    failure_reason: None,
199                },
200            );
201        }
202
203        Ok(())
204    }
205
206    async fn executions_ready(
207        &self,
208        execution_ids: &[Uuid],
209        timestamp: SystemTime,
210    ) -> eyre::Result<()> {
211        for execution_id in execution_ids {
212            let execution = self.pending_executions.write().swap_remove(execution_id);
213
214            if let Some(mut execution) = execution {
215                debug_assert!(execution.ready_at.is_none());
216                execution.ready_at = Some(timestamp);
217                self.ready_executions
218                    .write()
219                    .insert(*execution_id, execution);
220            }
221        }
222
223        Ok(())
224    }
225
226    async fn execution_assigned(
227        &self,
228        execution_id: Uuid,
229        executor_id: Uuid,
230        timestamp: SystemTime,
231    ) -> eyre::Result<()> {
232        let execution = self.ready_executions.write().swap_remove(&execution_id);
233
234        if let Some(mut execution) = execution {
235            debug_assert!(execution.assigned_at.is_none());
236            execution.assigned_at = Some(timestamp);
237            execution.executor_id = Some(executor_id);
238
239            self.assigned_executions
240                .write()
241                .insert(execution_id, execution);
242        }
243
244        Ok(())
245    }
246
247    async fn execution_started(
248        &self,
249        execution_id: Uuid,
250        timestamp: SystemTime,
251    ) -> eyre::Result<()> {
252        let execution = self.assigned_executions.write().swap_remove(&execution_id);
253        if let Some(mut execution) = execution {
254            debug_assert!(execution.started_at.is_none());
255            execution.started_at = Some(timestamp);
256
257            self.started_executions
258                .write()
259                .insert(execution_id, execution);
260        }
261
262        Ok(())
263    }
264
265    async fn execution_succeeded(
266        &self,
267        execution_id: Uuid,
268        timestamp: SystemTime,
269        output_payload_json: String,
270    ) -> eyre::Result<()> {
271        // executions may succeed at any phase, so we need to check all phases
272        let mut execution = if let Some(execution) =
273            self.pending_executions.write().swap_remove(&execution_id)
274        {
275            execution
276        } else if let Some(execution) = self.ready_executions.write().swap_remove(&execution_id) {
277            execution
278        } else if let Some(execution) = self.assigned_executions.write().swap_remove(&execution_id)
279        {
280            execution
281        } else if let Some(execution) = self.started_executions.write().swap_remove(&execution_id) {
282            execution
283        } else {
284            return Ok(());
285        };
286
287        debug_assert!(execution.succeeded_at.is_none());
288        debug_assert!(execution.failed_at.is_none());
289
290        execution.succeeded_at = Some(timestamp);
291        execution.output_payload_json = Some(output_payload_json);
292
293        let job = self.schedulable_jobs.write().swap_remove(&execution.job_id);
294        if let Some(mut job) = job {
295            debug_assert!(job.marked_unschedulable_at.is_none());
296            job.marked_unschedulable_at = Some(timestamp);
297
298            self.unschedulable_jobs
299                .write()
300                .insert(execution.job_id, job);
301        }
302
303        self.succeeded_executions
304            .write()
305            .insert(execution_id, execution);
306
307        Ok(())
308    }
309
310    async fn executions_failed(
311        &self,
312        execution_ids: &[Uuid],
313        timestamp: SystemTime,
314        reason: String,
315        mark_job_inactive: bool,
316    ) -> eyre::Result<()> {
317        // executions may fail at any phase, so we need to check all phases
318        for execution_id in execution_ids {
319            let mut execution = if let Some(execution) =
320                self.pending_executions.write().swap_remove(execution_id)
321            {
322                execution
323            } else if let Some(execution) = self.ready_executions.write().swap_remove(execution_id)
324            {
325                execution
326            } else if let Some(execution) =
327                self.assigned_executions.write().swap_remove(execution_id)
328            {
329                execution
330            } else if let Some(execution) =
331                self.started_executions.write().swap_remove(execution_id)
332            {
333                execution
334            } else {
335                return Ok(());
336            };
337
338            debug_assert!(execution.succeeded_at.is_none());
339            debug_assert!(execution.failed_at.is_none());
340
341            execution.failed_at = Some(timestamp);
342            execution.failure_reason = Some(reason.clone());
343
344            if mark_job_inactive {
345                let job = self.schedulable_jobs.write().swap_remove(&execution.job_id);
346                if let Some(mut job) = job {
347                    debug_assert!(job.marked_unschedulable_at.is_none());
348                    job.marked_unschedulable_at = Some(timestamp);
349
350                    self.unschedulable_jobs
351                        .write()
352                        .insert(execution.job_id, job);
353                }
354            }
355
356            self.failed_executions
357                .write()
358                .insert(*execution_id, execution);
359        }
360
361        Ok(())
362    }
363
364    async fn orphan_execution_ids(&self, executor_ids: &[Uuid]) -> eyre::Result<Vec<Uuid>> {
365        Ok(self
366            .assigned_executions
367            .read()
368            .iter()
369            .filter_map(|(id, execution)| {
370                if executor_ids.contains(&execution.executor_id.unwrap()) {
371                    None
372                } else {
373                    Some(*id)
374                }
375            })
376            .chain(
377                self.started_executions
378                    .read()
379                    .iter()
380                    .filter_map(|(id, execution)| {
381                        if executor_ids.contains(&execution.executor_id.unwrap()) {
382                            None
383                        } else {
384                            Some(*id)
385                        }
386                    }),
387            )
388            .collect())
389    }
390
391    async fn jobs_unschedulable(
392        &self,
393        job_ids: &[Uuid],
394        timestamp: SystemTime,
395    ) -> eyre::Result<()> {
396        for job_id in job_ids {
397            let job = self.schedulable_jobs.write().swap_remove(job_id);
398            if let Some(mut job) = job {
399                debug_assert!(job.marked_unschedulable_at.is_none());
400                job.marked_unschedulable_at = Some(timestamp);
401
402                self.unschedulable_jobs.write().insert(*job_id, job);
403            } else {
404                debug_assert!(false, "active job with ID {job_id} not found");
405            }
406        }
407
408        Ok(())
409    }
410
411    async fn pending_executions(&self, after: Option<Uuid>) -> eyre::Result<Vec<PendingExecution>> {
412        Ok(self
413            .pending_executions
414            .read()
415            .iter()
416            .filter_map(|(id, execution)| {
417                if let Some(after) = after {
418                    if *id <= after {
419                        return None;
420                    }
421                }
422
423                Some(PendingExecution {
424                    id: *id,
425                    target_execution_time: execution.target_execution_time,
426                })
427            })
428            .collect())
429    }
430
431    async fn ready_executions(&self, after: Option<Uuid>) -> eyre::Result<Vec<ReadyExecution>> {
432        Ok({
433            let mut executions = self
434                .ready_executions
435                .read()
436                .iter()
437                .filter_map(|(id, execution)| {
438                    let jobs = self.schedulable_jobs.read();
439                    let Some(job) = jobs.get(&execution.job_id) else {
440                        debug_assert!(false, "active job with ID {} not found", execution.job_id);
441                        return None;
442                    };
443
444                    Some(ReadyExecution {
445                        id: *id,
446                        target_execution_time: execution.target_execution_time,
447                        job_id: execution.job_id,
448                        input_payload_json: job.input_payload_json.clone(),
449                        attempt_number: 0,
450                        job_type_id: job.job_type_id.clone(),
451                        timeout_policy: job.timeout_policy,
452                    })
453                })
454                .collect::<Vec<_>>();
455
456            executions.sort_by_key(|execution| execution.id);
457
458            if let Some(after) = after {
459                executions.retain(|execution| execution.id > after);
460            }
461
462            for execution in &mut executions {
463                execution.attempt_number = u64::try_from(
464                    self.executions_by_job_id(execution.job_id)
465                        .position(|id| id == execution.id)
466                        .unwrap(),
467                )
468                .unwrap()
469                    + 1;
470            }
471
472            executions
473        })
474    }
475
476    async fn pending_jobs(&self, after: Option<Uuid>) -> eyre::Result<Vec<PendingJob>> {
477        let mut jobs: Vec<PendingJob> = {
478            let pending_executions = self.pending_executions.read();
479            let ready_executions = self.ready_executions.read();
480            let assigned_executions = self.assigned_executions.read();
481            let started_executions = self.started_executions.read();
482
483            self.schedulable_jobs
484                .read()
485                .iter()
486                .filter_map(|(job_id, job)| {
487                    if pending_executions
488                        .values()
489                        .any(|execution| execution.job_id == *job_id)
490                    {
491                        return None;
492                    }
493
494                    if ready_executions
495                        .values()
496                        .any(|execution| execution.job_id == *job_id)
497                    {
498                        return None;
499                    }
500
501                    if assigned_executions
502                        .values()
503                        .any(|execution| execution.job_id == *job_id)
504                    {
505                        return None;
506                    }
507
508                    if started_executions
509                        .values()
510                        .any(|execution| execution.job_id == *job_id)
511                    {
512                        return None;
513                    }
514
515                    if let Some(after) = after {
516                        if job.id <= after {
517                            return None;
518                        }
519                    }
520
521                    Some(PendingJob {
522                        id: job.id,
523                        target_execution_time: job.target_execution_time,
524                        execution_count: 0,
525                        retry_policy: job.retry_policy,
526                        timeout_policy: job.timeout_policy,
527                    })
528                })
529                .collect::<Vec<_>>()
530        };
531
532        for job in &mut jobs {
533            job.execution_count = u64::try_from(self.executions_by_job_id(job.id).len()).unwrap();
534        }
535
536        Ok(jobs)
537    }
538
539    async fn query_jobs(
540        &self,
541        cursor: Option<String>,
542        limit: usize,
543        order: JobQueryOrder,
544        filters: JobQueryFilters,
545    ) -> eyre::Result<JobQueryResult> {
546        let cursor: Option<job_query::Cursor> = match cursor {
547            Some(cursor) => serde_json::from_str(&cursor).wrap_err("invalid cursor")?,
548            None => None,
549        };
550
551        Ok(self.query_jobs_impl(cursor, limit, order, filters))
552    }
553
554    async fn query_job_ids(&self, filters: JobQueryFilters) -> eyre::Result<Vec<Uuid>> {
555        Ok(self.query_job_ids_impl(filters))
556    }
557
558    async fn count_jobs(&self, filters: JobQueryFilters) -> eyre::Result<u64> {
559        Ok(self.count_jobs_impl(filters))
560    }
561
562    async fn query_job_types(&self) -> eyre::Result<Vec<JobType>> {
563        Ok(self.job_types.read().values().cloned().collect())
564    }
565
566    async fn delete_jobs(&self, filters: JobQueryFilters) -> eyre::Result<Vec<Uuid>> {
567        let job_ids = self.query_job_ids_impl(filters);
568
569        let mut executions_to_remove = Vec::new();
570
571        for job_id in &job_ids {
572            self.unschedulable_jobs
573                .write()
574                .swap_remove(job_id)
575                .or_else(|| self.schedulable_jobs.write().swap_remove(job_id));
576
577            executions_to_remove.extend(self.executions_by_job_id(*job_id));
578        }
579
580        self.pending_executions
581            .write()
582            .retain(|id, _| !executions_to_remove.contains(id));
583        self.ready_executions
584            .write()
585            .retain(|id, _| !executions_to_remove.contains(id));
586        self.assigned_executions
587            .write()
588            .retain(|id, _| !executions_to_remove.contains(id));
589        self.started_executions
590            .write()
591            .retain(|id, _| !executions_to_remove.contains(id));
592        self.succeeded_executions
593            .write()
594            .retain(|id, _| !executions_to_remove.contains(id));
595        self.failed_executions
596            .write()
597            .retain(|id, _| !executions_to_remove.contains(id));
598
599        Ok(job_ids)
600    }
601
602    async fn schedules_added(&self, schedules: Vec<NewSchedule>) -> eyre::Result<()> {
603        for schedule in schedules {
604            let mut active_schedules = self.schedulable_schedules.write();
605
606            if active_schedules.contains_key(&schedule.id) {
607                bail!("schedule with ID {} already exists", schedule.id);
608            }
609
610            active_schedules.insert(schedule.id, Schedule::from(schedule));
611        }
612
613        Ok(())
614    }
615
616    async fn schedules_cancelled(
617        &self,
618        schedule_ids: &[Uuid],
619        timestamp: SystemTime,
620    ) -> eyre::Result<Vec<CancelledSchedule>> {
621        let mut cancelled_schedules = Vec::with_capacity(schedule_ids.len());
622
623        for schedule_id in schedule_ids {
624            let schedule = self.schedulable_schedules.write().swap_remove(schedule_id);
625
626            if let Some(mut schedule) = schedule {
627                debug_assert!(schedule.cancelled_at.is_none());
628                schedule.cancelled_at = Some(timestamp);
629
630                debug_assert!(schedule.marked_unschedulable_at.is_none());
631                schedule.marked_unschedulable_at = Some(timestamp);
632
633                let schedule_id = schedule.id;
634
635                self.unschedulable_schedules
636                    .write()
637                    .insert(schedule_id, schedule);
638
639                cancelled_schedules.push(CancelledSchedule { id: schedule_id });
640            }
641        }
642
643        Ok(cancelled_schedules)
644    }
645
646    async fn pending_schedules(&self, after: Option<Uuid>) -> eyre::Result<Vec<PendingSchedule>> {
647        Ok(self
648            .schedulable_schedules
649            .read()
650            .iter()
651            .filter_map(|(id, schedule)| {
652                if self
653                    .schedulable_jobs
654                    .read()
655                    .values()
656                    .any(|job| job.schedule_id == Some(schedule.id))
657                {
658                    return None;
659                }
660
661                if let Some(after) = after {
662                    if *id <= after {
663                        return None;
664                    }
665                }
666
667                Some(PendingSchedule {
668                    id: *id,
669                    job_timing_policy: schedule.job_timing_policy.clone(),
670                    job_creation_policy: schedule.job_creation_policy.clone(),
671                    last_target_execution_time: self.last_target_execution_time(schedule.id),
672                    time_range: schedule.time_range,
673                })
674            })
675            .collect::<Vec<_>>())
676    }
677
678    async fn query_schedules(
679        &self,
680        cursor: Option<String>,
681        limit: usize,
682        filters: ScheduleQueryFilters,
683        order: ScheduleQueryOrder,
684    ) -> eyre::Result<ScheduleQueryResult> {
685        let cursor: Option<schedule_query::Cursor> = match cursor {
686            Some(cursor) => serde_json::from_str(&cursor).wrap_err("invalid cursor")?,
687            None => None,
688        };
689
690        Ok(self.query_schedules_impl(cursor, limit, order, filters))
691    }
692
693    async fn query_schedule_ids(&self, filters: ScheduleQueryFilters) -> eyre::Result<Vec<Uuid>> {
694        Ok(self.query_schedule_ids_impl(filters))
695    }
696
697    async fn count_schedules(&self, filters: ScheduleQueryFilters) -> eyre::Result<u64> {
698        Ok(self.count_schedules_impl(filters))
699    }
700
701    async fn schedules_unschedulable(
702        &self,
703        schedule_ids: &[Uuid],
704        timestamp: SystemTime,
705    ) -> eyre::Result<()> {
706        for schedule_id in schedule_ids {
707            let schedule = self.schedulable_schedules.write().swap_remove(schedule_id);
708
709            if let Some(mut schedule) = schedule {
710                debug_assert!(schedule.marked_unschedulable_at.is_none());
711                schedule.marked_unschedulable_at = Some(timestamp);
712
713                let schedule_id = schedule.id;
714
715                self.unschedulable_schedules
716                    .write()
717                    .insert(schedule_id, schedule);
718            }
719        }
720
721        Ok(())
722    }
723
724    async fn delete_schedules(&self, filters: ScheduleQueryFilters) -> eyre::Result<Vec<Uuid>> {
725        let schedule_ids = self
726            .query_schedule_ids_impl(filters)
727            .into_iter()
728            .collect::<IndexSet<_>>();
729
730        // We don't care about orphaned jobs here.
731        self.schedulable_schedules
732            .write()
733            .retain(|id, _| !schedule_ids.contains(id));
734        self.unschedulable_schedules
735            .write()
736            .retain(|id, _| !schedule_ids.contains(id));
737
738        Ok(schedule_ids.into_iter().collect())
739    }
740}
741
742impl MemoryStorage {
743    /// Returns all execution IDs for a job in creation order.
744    fn executions_by_job_id(&self, job_id: Uuid) -> impl ExactSizeIterator<Item = Uuid> {
745        let mut execution_ids = self
746            .pending_executions
747            .read()
748            .values()
749            .chain(self.ready_executions.read().values())
750            .chain(self.assigned_executions.read().values())
751            .chain(self.started_executions.read().values())
752            .chain(self.succeeded_executions.read().values())
753            .chain(self.failed_executions.read().values())
754            .filter(move |execution| execution.job_id == job_id)
755            .map(|execution| execution.id)
756            .collect::<Vec<_>>();
757
758        execution_ids.sort_unstable();
759
760        execution_ids.into_iter()
761    }
762
763    /// Returns the last target execution time of a schedule.
764    fn last_target_execution_time(&self, schedule_id: Uuid) -> Option<SystemTime> {
765        self.schedulable_jobs
766            .read()
767            .values()
768            .filter(|job| job.schedule_id == Some(schedule_id))
769            .map(|job| job.target_execution_time)
770            .max()
771            .max(
772                self.unschedulable_jobs
773                    .read()
774                    .values()
775                    .filter(|job| job.schedule_id == Some(schedule_id))
776                    .map(|job| job.target_execution_time)
777                    .max(),
778            )
779    }
780}
781
782#[derive(Debug, Clone)]
783struct Job {
784    id: Uuid,
785    schedule_id: Option<Uuid>,
786    created_at: SystemTime,
787    job_type_id: String,
788    target_execution_time: SystemTime,
789    retry_policy: JobRetryPolicy,
790    timeout_policy: JobTimeoutPolicy,
791    labels: IndexMap<String, String>,
792    marked_unschedulable_at: Option<SystemTime>,
793    cancelled_at: Option<SystemTime>,
794    input_payload_json: String,
795    metadata_json: Option<String>,
796}
797
798impl From<NewJob> for Job {
799    fn from(job: NewJob) -> Self {
800        Self {
801            id: job.id,
802            schedule_id: job.schedule_id,
803            created_at: job.created_at,
804            job_type_id: job.job_type_id,
805            target_execution_time: job.target_execution_time,
806            retry_policy: job.retry_policy,
807            timeout_policy: job.timeout_policy,
808            labels: job.labels,
809            input_payload_json: job.input_payload_json,
810            marked_unschedulable_at: None,
811            cancelled_at: None,
812            metadata_json: job.metadata_json,
813        }
814    }
815}
816
817#[derive(Debug, Clone)]
818struct Execution {
819    id: Uuid,
820    job_id: Uuid,
821    target_execution_time: SystemTime,
822    executor_id: Option<Uuid>,
823    created_at: SystemTime,
824    ready_at: Option<SystemTime>,
825    assigned_at: Option<SystemTime>,
826    started_at: Option<SystemTime>,
827    succeeded_at: Option<SystemTime>,
828    failed_at: Option<SystemTime>,
829    output_payload_json: Option<String>,
830    failure_reason: Option<String>,
831}
832
833impl From<&Execution> for ExecutionDetails {
834    fn from(value: &Execution) -> Self {
835        Self {
836            id: value.id,
837            job_id: value.job_id,
838            executor_id: value.executor_id,
839            status: if value.succeeded_at.is_some() {
840                JobExecutionStatus::Succeeded
841            } else if value.failed_at.is_some() {
842                JobExecutionStatus::Failed
843            } else if value.started_at.is_some() {
844                JobExecutionStatus::Running
845            } else if value.assigned_at.is_some() {
846                JobExecutionStatus::Assigned
847            } else if value.ready_at.is_some() {
848                JobExecutionStatus::Ready
849            } else {
850                JobExecutionStatus::Pending
851            },
852            created_at: value.created_at,
853            ready_at: value.ready_at,
854            assigned_at: value.assigned_at,
855            started_at: value.started_at,
856            succeeded_at: value.succeeded_at,
857            failed_at: value.failed_at,
858            output_payload_json: value.output_payload_json.clone(),
859            failure_reason: value.failure_reason.clone(),
860        }
861    }
862}
863
864#[derive(Debug, Clone)]
865struct Schedule {
866    id: Uuid,
867    created_at: SystemTime,
868    job_type_id: Option<String>,
869    labels: IndexMap<String, String>,
870    marked_unschedulable_at: Option<SystemTime>,
871    cancelled_at: Option<SystemTime>,
872    job_timing_policy: ScheduleJobTimingPolicy,
873    job_creation_policy: ScheduleJobCreationPolicy,
874    time_range: Option<ScheduleTimeRange>,
875    metadata_json: Option<String>,
876}
877
878impl From<NewSchedule> for Schedule {
879    fn from(schedule: NewSchedule) -> Self {
880        Self {
881            id: schedule.id,
882            created_at: schedule.created_at,
883            job_type_id: match &schedule.job_creation_policy {
884                ScheduleJobCreationPolicy::JobDefinition(schedule_new_job_definition) => {
885                    Some(schedule_new_job_definition.job_type_id.clone())
886                }
887            },
888            labels: schedule.labels,
889            marked_unschedulable_at: None,
890            cancelled_at: None,
891            job_timing_policy: schedule.job_timing_policy,
892            job_creation_policy: schedule.job_creation_policy,
893            time_range: schedule.time_range,
894            metadata_json: schedule.metadata_json,
895        }
896    }
897}